diff --git a/src/main/html/Part1Ex1.html b/src/main/html/Part1Ex1.html index d359632..b32d8f5 100644 --- a/src/main/html/Part1Ex1.html +++ b/src/main/html/Part1Ex1.html @@ -1,41 +1,47 @@ - + - - Exercise 1 - instantiating TinyMCE + + Exercise 1 - instantiating TinyMCE - - - - - - - - + + - - - - - - - - - - - + + + diff --git a/src/main/ts/Part2Ex1.ts b/src/main/ts/Part2Ex1.ts index b83350e..893a492 100644 --- a/src/main/ts/Part2Ex1.ts +++ b/src/main/ts/Part2Ex1.ts @@ -15,7 +15,10 @@ Let's model the x,y of the top-left and bottom-right corners. *Remember* we like immutable data, so mark the fields readonly. */ export interface Boundz { - // TODO: add fields: x1, y1, x2, y2 + readonly x1: number; + readonly y1: number; + readonly x2: number; + readonly y2: number; } /* @@ -27,15 +30,17 @@ We tell tsc to transpile to ES5, so IE works. Notice also that we have an explicit return type. This lets the compiler check that our code matches the type signature. */ -export const width = (b: Boundz): number => - /* TODO */ -1; -// TODO implement height function +/* DONE */ +export const width = (b: Boundz): number => b.x2 - b.x1; + +// DONE implement height function +export const height = (b: Boundz): number => b.y2 - b.y1; /* 3. Compiling. -TODO Run `yarn build` at your shell to make sure everything compiles. +DONE Run `yarn build` at your shell to make sure everything compiles. `build` is a script defined in the package.json. Have a look at package.json to see what it does. diff --git a/src/main/ts/Part2Ex2ArrayFunctions.ts b/src/main/ts/Part2Ex2ArrayFunctions.ts index 36fa837..9e28bd2 100644 --- a/src/main/ts/Part2Ex2ArrayFunctions.ts +++ b/src/main/ts/Part2Ex2ArrayFunctions.ts @@ -1,4 +1,4 @@ -import { Arr, Optional } from '@ephox/katamari'; +import { Arr, Optional } from "@ephox/katamari"; /* Katamari is our library for general-purpose functions and FP basics. @@ -9,8 +9,8 @@ Its array module "Arr" is very handy, so let's explore it. We don't write loops if we can help it. Instead, we go up a level, and call functions that do the looping for us. The simplest of these is 'each' which just iterates. -TODO: Run the following code using this command: -yarn bedrock-auto -b chrome-headless -f src/test/ts/Exercise2ArrayFunctionsTest.ts +DONE: Run the following code using this command: +yarn bedrock-auto -b chrome-headless -f src/test/ts/part2/Exercise2ArrayFunctionsTest.ts */ export const runEach1 = (): void => { @@ -33,14 +33,16 @@ export interface Frog { } export const myFrogs: Frog[] = [ - { name: 'frog1', ribbits: true, age: 3 }, - { name: 'frog2', ribbits: false, age: 4 }, - { name: 'loudfrog', ribbits: true, age: 1 }, - { name: 'quietfrog', ribbits: false, age: 10 }, + { name: "frog1", ribbits: true, age: 3 }, + { name: "frog2", ribbits: false, age: 4 }, + { name: "loudfrog", ribbits: true, age: 1 }, + { name: "quietfrog", ribbits: false, age: 10 }, ]; export const runEach2 = (): void => { - // TODO: Use Arr.each and console.log to print the name of each frog + Arr.each(myFrogs, (frog) => { + console.log(frog.name); + }); }; /* @@ -56,20 +58,19 @@ Let's go through some examples of using Arr.map, then see if you can get the fro */ // add 2 to each element -export const runMap1 = (xs: number[]): number[] => - Arr.map(xs, (x) => x + 2); +export const runMap1 = (xs: number[]): number[] => Arr.map(xs, (x) => x + 2); // prepend a string to each element export const runMap2 = (xs: number[]): string[] => Arr.map(xs, (x) => "the number is " + x); -// TODO: Return the frog's names and check it by running -// yarn bedrock-auto -b chrome-headless -f src/test/ts/Exercise2ArrayFunctionsTest.ts -export const frogNames = (fs: Frog[]): string[] => - []; +// DONE: Return the frog's names and check it by running +// yarn bedrock-auto -b chrome-headless -f src/test/ts/part2/Exercise2ArrayFunctionsTest.ts +export const frogNames = (fs: Frog[]): string[] => Arr.map(fs, (f) => f.name); -// TODO: Return the frog's ages -// TODO: Write a test for this in Exercise2ArrayFunctionsTest +// DONE: Return the frog's ages +export const frogAges = (fs: Frog[]): number[] => Arr.map(fs, (f) => f.age); +// DONE: Write a test for this in Exercise2ArrayFunctionsTest /* 4. Arr.filter @@ -81,14 +82,14 @@ e.g. to get all the even numbers out of a list: export const evens = (xs: number[]): number[] => Arr.filter(xs, (x) => x % 2 === 0); -// TODO: Write a function that returns all the frogs that ribbit -// TODO: Run the provided test to check your answer. +// DONE: Write a function that returns all the frogs that ribbit +// DONE: Run the provided test to check your answer. export const ribbitting = (frogs: Frog[]): Frog[] => - []; + Arr.filter(frogs, (frog) => frog.ribbits); -// TODO: Write a function that returns all frogs aged 8 or older +// DONE: Write a function that returns all frogs aged 8 or older export const olderFrogs = (frogs: Frog[]): Frog[] => - []; + Arr.filter(frogs, (frog) => frog.age >= 8); /* 5. Arr.exists @@ -96,10 +97,13 @@ export const olderFrogs = (frogs: Frog[]): Frog[] => Arr.exists returns true if there is one or more element that matches a predicate. */ -// TODO: Write a function that returns true if there's one or more ribbiting frogs - -// TODO: Write a function that takes an array of numbers, and returns true if there are any negative numbers +// DONE: Write a function that returns true if there's one or more ribbiting frogs +export const oneOrMoreRibbiting = (frogs: Frog[]): boolean => + Arr.exists(frogs, (frog) => frog.ribbits); +// DONE: Write a function that takes an array of numbers, and returns true if there are any negative numbers +export const hasNegativeNumber = (xs: number[]): boolean => + Arr.exists(xs, (x) => x < 0); /* 6. Arr.bind @@ -108,11 +112,10 @@ This results in an array of arrays, which is then flattened. This behaviour of running map then flatten is why this function is sometimes called "flatmap". -TODO: Write a function that takes a list of strings, each string containing a comma-separated list of values, and returns all of the values as an array. +DONE: Write a function that takes a list of strings, each string containing a comma-separated list of values, and returns all of the values as an array. */ export const splitCsvs = (csvs: string[]): string[] => - []; - + Arr.bind(csvs, (csv) => csv.split(",")); /* 7. Arr.find @@ -132,4 +135,4 @@ Even if you have used them in the past, there's a trick to how we test these. We'll go into Option in more detail in Exercise 3... -*/ \ No newline at end of file +*/ diff --git a/src/main/ts/Part2Ex3Optional.ts b/src/main/ts/Part2Ex3Optional.ts index 1ca39ff..a99c03e 100644 --- a/src/main/ts/Part2Ex3Optional.ts +++ b/src/main/ts/Part2Ex3Optional.ts @@ -1,4 +1,4 @@ -import { Optional } from '@ephox/katamari'; +import { Optional } from "@ephox/katamari"; /* Optional @@ -34,16 +34,24 @@ const parseIntOpt = (s: string): Optional => { export const toPositiveInteger = (n: number): Optional => n > 0 ? Optional.some(n) : Optional.none(); -// TODO: create a function which takes a string and returns some if the string is non-empty +// DONE: create a function which takes a string and returns some if the string is non-empty +export const nonEmptyString = (s: string): Optional => { + return s.length > 0 ? Optional.some(s) : Optional.none(); +}; -// TODO: create a function which takes a url as a string and returns the protocol part as an Optional. +// DONE: create a function which takes a url as a string and returns the protocol part as an Optional. // The string may or may not actually have a protocol. For the protocol to be valid, it needs to be all alpha characters. // You can use a regex. // Have a look at Exercise3OptionTest.ts for example input. Make sure the tests pass. export const getProtocol = (url: string): Optional => { - throw new Error("TODO"); + const protocol = url.match(/^[^\:\/\/]+/); + return protocol && isAlpha(protocol[0]) + ? Optional.some(protocol[0]) + : Optional.none(); }; +const isAlpha = (s: string): boolean => /^[a-zA-Z]+$/i.test(s); + /* The other way we construct Optionals, is using Optional.from. @@ -53,14 +61,15 @@ Optional.from take a value which may be null, undefined or an actual value. Optional.from is useful for taking values from the "nullable" world to the Optional world. -TODO: use Optional.from to implement the following DOM function +DONE: use Optional.from to implement the following DOM function */ -export const getNextSibling = (e: Element): Optional => { - throw new Error("TODO"); -}; +export const getNextSibling = (e: Element): Optional => + Optional.from(e.nextSibling); -// TODO: use Optional.from to implement a similar wrapper for Element.getAttributeNode(string) +// DONE: use Optional.from to implement a similar wrapper for Element.getAttributeNode(string) +export const getAttribute = (e: Element, attrName: string): Optional => + Optional.from(e.getAttributeNode(attrName)); /* How do we get data out of an Optional? Well, that's a bit tricky since there isn't always @@ -79,12 +88,19 @@ export const message = (e: Optional): string => (s) => "The value was " + s ); -// TODO: Implement a function using fold, that takes an Optional. If it's some, double it. If it's none, return 0; +// DONE: Implement a function using fold, that takes an Optional. If it's some, double it. If it's none, return 0; +export const optionalDouble = (n: Optional): number => + n.fold( + () => 0, + (num) => num * 2 + ); -// TODO: Implement a function that takes an Optional for any type T. Return true if it's some, and false if it's none. -const trueIfSome = (x: Optional): boolean => { - throw new Error("TODO"); -}; +// DONE: Implement a function that takes an Optional for any type T. Return true if it's some, and false if it's none. +export const trueIfSome = (x: Optional): boolean => + x.fold( + () => false, + (_) => true + ); /* The last function you implemented is already part of the Optional type, and is called isSome(). @@ -106,10 +122,19 @@ A common way to handle an Optional value is to provide a default value if in the You can do this with fold, but getOr is a shortcut. */ -// TODO: Using getOr, take an Optional<{age: number}> and turn it into an {age: number}, using a default value of 0. - -// TODO: Write the same function using fold - +// DONE: Using getOr, take an Optional<{age: number}> and turn it into an {age: number}, using a default value of 0. +export const getAgeOrDefault = ( + a: Optional<{ age: number }> +): { age: number } => a.getOr({ age: 0 }); + +// DONE: Write the same function using fold +export const getAgeOrDefaultFold = ( + a: Optional<{ age: number }> +): { age: number } => + a.fold( + () => ({ age: 0 }), + (s) => s + ); /* Another way of thinking about an Optional, is that it's an array that contains either 0 or 1 elements. @@ -117,10 +142,16 @@ Another way of thinking about an Optional, is that it's an array that contains e Let's explore this by converting Optionals to and from Arrays. */ -// TODO: Write a function that converts an Optional to an A[] for any type A. - -// TODO: Write a function that converts an A[] to an Optional. If the array has more than one element, only consider the first element. +// DONE: Write a function that converts an Optional to an A[] for any type A. +export const optionalToArray = (opt: Optional): A[] => + opt.fold( + () => [], + (s) => [s] + ); +// DONE: Write a function that converts an A[] to an Optional. If the array has more than one element, only consider the first element. +export const arrayToOptional = (arr: A[]): Optional => + arr.length === 0 ? Optional.none() : Optional.some(arr[0]); /* One of the most useful functions on Optional is "map". We say this function "maps a function over the Optional". @@ -138,13 +169,18 @@ const x: Optional = Optional.some(3).map((x) => String(x)); // returns O const y: Optional = Optional.none().map((x) => String(x)); // returns Optional.none() -// TODO: Write a function that takes an Optional and adds 3 to the number +// DONE: Write a function that takes an Optional and adds 3 to the number +export const addThreeToOptional = (num: Optional): Optional => + num.map((x) => x + 3); -// TODO: Write a function that takes an Optional and prefixes the string with "hello" +// DONE: Write a function that takes an Optional and prefixes the string with "hello" +export const prefixStringOptional = (str: Optional): Optional => + str.map((x) => "hello" + x); /* -TODO: If the below function is called, does it return a value or throw an exception? Why should it behave one way or the other? -Answer: ... +DONE: If the below function is called, does it return a value or throw an exception? Why should it behave one way or the other? +Answer: Returns Optional.none(). Based on definitions, map called on an Optional.none() will return Optional.none() immediately. + */ const willItKersplode = (): Optional => { const z = Optional.none(); @@ -157,10 +193,9 @@ const willItKersplode = (): Optional => { Well done! You've tackled the basis of Optionals. We'll dig into them a bit more in future exercises, but everything builds on what we've done here. -TODO: head over to Exercise3OptionTest to write some test cases for the above. +DONE: head over to Exercise3OptionTest to write some test cases for the above. */ - /* Below are some explanatory notes on some more advanced topics. Feel free to skip them if you're still learning. @@ -210,9 +245,8 @@ Below are some explanatory notes on some more advanced topics. Feel free to skip + taking an object and destructing it into its constituent parts. You'll also find "destructuring assignment" + as a feature of many languages, including TypeScript. e.g. const { x } = { x: 3, y: 'foo' }; + -+ Our "fold" function destructures the Option type. In category theory, "fold" is known as a "catamorphism". ++ Our "fold" function destructures the Optional type. In category theory, "fold" is known as a "catamorphism". + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ - diff --git a/src/main/ts/Part2Ex4FP.ts b/src/main/ts/Part2Ex4FP.ts index 199bcfe..9d71942 100644 --- a/src/main/ts/Part2Ex4FP.ts +++ b/src/main/ts/Part2Ex4FP.ts @@ -74,33 +74,46 @@ export const printMessage2 = (e: Optional): void => /* Now, printMessage2 is still tricky to test, but getMessage is very easy to test. We've improved the testability of our code. -TODO: Extract a pure function for the logic hiding in this (impure) function +DONE: Extract a pure function for the logic hiding in this (impure) function */ -type Mode = 'code' | 'design' | 'markdown'; +type Mode = "code" | "design" | "markdown"; const switchMode = (m: Mode): void => { // pretend that something useful happens here that causes a side effect }; -const nextMode = (m: Mode): void => { - if (m === 'code') { - switchMode('design'); - } else if (m === 'design') { - switchMode('markdown'); +// const nextMode = (m: Mode): void => { +// if (m === "code") { +// switchMode("design"); +// } else if (m === "design") { +// switchMode("markdown"); +// } else { +// switchMode("code"); +// } +// }; + +const nextModePure = (m: Mode): void => { + switchMode(getNextMode(m)); +}; + +const getNextMode = (m: Mode): Mode => { + if (m === "code") { + return "design"; + } else if (m === "design") { + return "markdown"; } else { - switchMode('code'); + return "code"; } }; - /* The identity function. This is a very simple function that takes an argument and returns it. */ -const identity = (a: A): A => a; +const identity = (a: A): A => a; /* You can find this function in katamari as Fun.identity. @@ -111,7 +124,7 @@ const identity = (a: A): A => a; on Optional. You can write a similar function like this: */ -const getOrElse1 = (oa: Optional, other: A): A => +const getOrElse1 = (oa: Optional, other: A): A => oa.fold( () => other, (a) => a @@ -119,13 +132,16 @@ const getOrElse1 = (oa: Optional, other: A): A => // Hang on - that looks familiar. The function we pass as the "some" case is the identity function. -// TODO: write a version of getOrElse1 using Fun.identity. +// DONE: write a version of getOrElse1 using Fun.identity. + +const getOrElse1Id = (oa: Optional, other: A): A => + oa.fold(() => other, Fun.identity); -// TODO: What happens if you map the identity function over an Optional? -// Answer: ... +// DONE: What happens if you map the identity function over an Optional? +// Answer: Return value is identical to the Optional value. -// TODO: What happens if you map the identity function over an Array? -// Answer: ... +// DONE: What happens if you map the identity function over an Array? +// Answer: Return value is identical to the Array value. /* In FP, we use a lot of little functions like identity, that seem insignificant on their own, but they come in handy @@ -138,7 +154,10 @@ You can find this as Fun.constant in katamari. One way of writing it is below: */ -const constant = (a: A) => (...args: unknown[]): A => a; +const constant = + (a: A) => + (...args: unknown[]): A => + a; const always3 = constant(3); @@ -147,20 +166,23 @@ So, constant ignores whatever is passed for input parameters, and just returns t Again, this looks familiar from our getOrElse1 function above. -TODO: rewrite getOrElse1 using both Fun.identity and the "constant" function defined above. +DONE: rewrite getOrElse1 using both Fun.identity and the "constant" function defined above. */ - +const getOrElse1Constant = (oa: Optional, other: A): A => + oa.fold(constant(other), Fun.identity); /* -TODO: use katamari's Fun.constant in your getOrElse and see if it compiles. +DONE: use katamari's Fun.constant in your getOrElse and see if it compiles. */ +const getOrElse1KataConstant = (oa: Optional, other: A): A => + oa.fold(Fun.constant(other), Fun.identity); -// TODO: Write a function that takes an array of numbers and replaces each value with 9. - - -// TODO: In the previous question, what's the *same* between the input and output values -// Answer: +// DONE: Write a function that takes an array of numbers and replaces each value with 9. +const replaceWithNines = (nums: number[]): number[] => + Arr.map(nums, Fun.constant(9)); +// DONE: In the previous question, what's the *same* between the input and output values +// Answer: Both input and output will be the arrays of the same length. /* Function composition @@ -189,7 +211,10 @@ This is function composition. In TypeScript, it looks a bit like this: */ -const compose = (f: (a: B) => C, g: (a: A) => B) => (a: A): C => f(g(a)); +const compose = + (f: (a: B) => C, g: (a: A) => B) => + (a: A): C => + f(g(a)); /* The below function "dblS" doubles a number then converts it to a string. @@ -197,8 +222,7 @@ The below function "dblS" doubles a number then converts it to a string. const dbl = (x: number): number => x * 2; -const dblS: (s: number) => string = - compose(String, dbl); +const dblS: (s: number) => string = compose(String, dbl); /* It can read a bit funny, since it does the dbl, then the String. But, the order comes from the fact, e.g. @@ -209,8 +233,9 @@ Now, katamari has a Fun.compose1, which is like our compose here. It also has a signature and handling for n-ary functions. Your rule-of-thumb is to use Fun.compose1 unless you really need Fun.compose. */ -// TODO: use Fun.compose1 to write a function that doubles a number twice +// DONE: use Fun.compose1 to write a function that doubles a number twice +const dblTwice: (x: number) => number = Fun.compose1(dbl, dbl); -// TODO: Rewrite this function to use a single map call and function composition +// DONE: Rewrite this function to use a single map call and function composition const dblOs = (oa: Optional): Optional => - oa.map(dbl).map(String); + oa.map(Fun.compose1(String, dbl)); diff --git a/src/main/ts/Part2Ex5Sugar.ts b/src/main/ts/Part2Ex5Sugar.ts index 27412cc..6b02b16 100644 --- a/src/main/ts/Part2Ex5Sugar.ts +++ b/src/main/ts/Part2Ex5Sugar.ts @@ -1,4 +1,4 @@ -import { SugarElement, SugarDocument, Traverse, Insert } from '@ephox/sugar'; +import { SugarElement, SugarDocument, Traverse, Insert } from "@ephox/sugar"; /* Sugar @@ -14,8 +14,7 @@ Wrapping The first thing we need to know is how to wrap and unwrap a sugar element. */ - -const e1: HTMLSpanElement = document.createElement('span'); +const e1: HTMLSpanElement = document.createElement("span"); // wrapping const se1: SugarElement = SugarElement.fromDom(e1); @@ -29,49 +28,61 @@ Pretty simple so far. Now, above, we used `document.createElement`. We want to use Sugar functions everywhere, so we should do this instead: */ -const se2: SugarElement = SugarElement.fromTag('span'); +const se2: SugarElement = SugarElement.fromTag("span"); // or -const se3: SugarElement = SugarElement.fromTag('span', document); +const se3: SugarElement = SugarElement.fromTag( + "span", + document +); /* It's useful to be able to pass in a document parameter to this function, since we're often dealing with iframes as well as the main document. -TODO: Use SugarElement's fromHtml and fromText functions to create a few elements. +DONE: Use SugarElement's fromHtml and fromText functions to create a few elements. */ +const se4: SugarElement = SugarElement.fromHtml( + '', + document +); +const se5: SugarElement = SugarElement.fromText("hello", document); /* We often have to traverse from an element to its relatives. The Traverse module has useful functions for this. */ () => { - const parent: SugarElement = SugarElement.fromTag('div'); - const kid: SugarElement = SugarElement.fromTag('span'); + const parent: SugarElement = SugarElement.fromTag("div"); + const kid: SugarElement = SugarElement.fromTag("span"); Insert.append(parent, kid); const parent2 = Traverse.parent(kid); -// TODO: inspect the type of Traverse.parent and explain why that type was used. -// Answer: + // DONE: inspect the type of Traverse.parent and explain why that type was used. + // Answer: Optional of intersection of Node and ParentNode types is used. Optional as not every element is guaranteed to + // have a parent, and intersection of Node and ParentNode as the parent of an element is common to both types. }; - - () => { - const parent: SugarElement = SugarElement.fromTag('div'); - const kid1: SugarElement = SugarElement.fromTag('span'); - const kid2: SugarElement = SugarElement.fromTag('div'); + const parent: SugarElement = SugarElement.fromTag("div"); + const kid1: SugarElement = SugarElement.fromTag("span"); + const kid2: SugarElement = SugarElement.fromTag("div"); Insert.append(parent, kid1); Insert.append(parent, kid2); - // TODO: starting at kid1, find kid2 + // DONE: starting at kid1, find kid2 + const kid2Found = Traverse.nextSibling(kid1); - // TODO: starting at kid2, find kid1 + // DONE: starting at kid2, find kid1 + const kid1Found = Traverse.prevSibling(kid2); - // TODO: starting at parent, find both kids + // DONE: starting at parent, find both kids + const kids = Traverse.children(parent); - // TODO: kid2 grew up - give it its own child node + // DONE: kid2 grew up - give it its own child node + const kid2child: SugarElement = + SugarElement.fromTag("audio"); + Insert.append(kid2, kid2child); }; - diff --git a/src/test/ts/part2/Exercise1CodeStyleTest.ts b/src/test/ts/part2/Exercise1CodeStyleTest.ts index 5477a9c..5515c9d 100644 --- a/src/test/ts/part2/Exercise1CodeStyleTest.ts +++ b/src/test/ts/part2/Exercise1CodeStyleTest.ts @@ -1,6 +1,6 @@ -import { describe, it } from '@ephox/bedrock-client'; -import { assert } from 'chai'; -import * as CodeStyle from '../../../main/ts/Part2Ex1'; +import { describe, it } from "@ephox/bedrock-client"; +import { assert } from "chai"; +import * as CodeStyle from "../../../main/ts/Part2Ex1"; type Boundz = CodeStyle.Boundz; @@ -24,7 +24,7 @@ yarn bedrock-auto -b chrome-headless -f src/test/ts/part2/Exercise1CodeStyleTest If you want to run this in a full browser, try: yarn bedrock-auto -b chrome -f src/test/ts/part2/Exercise1CodeStyleTest.ts -TODO: Run bedrock in all modes shown above. +DONE: Run bedrock in all modes shown above. 2. Defining tests @@ -34,15 +34,17 @@ https://mochajs.org/#getting-started We start with a definition like this... */ -describe('Exercise1CodeStyleTests', () => { - it('width', () => { +describe("Exercise1CodeStyleTests", () => { + it("width", () => { // ... and then we write some test cases // We use chai assertions https://www.chaijs.com/api/assert/ - const b: Boundz = ({ x1: 3, y1: 4, x2: 7, y2: 8 }); - assert.deepEqual(CodeStyle.width(b), 4, 'Width'); + const b: Boundz = { x1: 3, y1: 4, x2: 7, y2: 8 }; + assert.deepEqual(CodeStyle.width(b), 4, "Width"); - // TODO: write another test case for width + // DONE: write another test case for width + const c: Boundz = { x1: 0, y1: 0, x2: -4, y2: -10 }; + assert.deepEqual(CodeStyle.width(c), -4, "Width negative"); }); /* @@ -52,20 +54,27 @@ describe('Exercise1CodeStyleTests', () => { In this case, we'll call "it" again, to write a test for 'height' */ - // TODO: write a simple test case for height + // DONE: write a simple test case for height + it("height", () => { + const b: Boundz = { x1: 3, y1: 2, x2: 7, y2: 10 }; + assert.deepEqual(CodeStyle.height(b), 8, "Height"); + + const c: Boundz = { x1: 3, y1: -2, x2: 7, y2: -10 }; + assert.deepEqual(CodeStyle.height(c), -8, "Height negative"); + }); /* 4. Test output The below test should fail. - TODO: remove the ".skip" to enable this test. Run it using the commands above. + DONE: remove the ".skip" to enable this test. Run it using the commands above. Notice that the output shows a diff. - TODO: Correct the test and run it again. + DONE: Correct the test and run it again. */ - it.skip('failing test', () => { - assert.deepEqual({ a: 1, b: 2 }, { a: 1, b: 7, c: 8 }); + it("failing test", () => { + assert.deepEqual({ a: 1, b: 7, c: 8 }, { a: 1, b: 7, c: 8 }); }); }); diff --git a/src/test/ts/part2/Exercise2ArrayFunctionsTest.ts b/src/test/ts/part2/Exercise2ArrayFunctionsTest.ts index 902f391..85e9807 100644 --- a/src/test/ts/part2/Exercise2ArrayFunctionsTest.ts +++ b/src/test/ts/part2/Exercise2ArrayFunctionsTest.ts @@ -1,38 +1,86 @@ -import { describe, it } from '@ephox/bedrock-client'; -import { assert } from 'chai'; -import * as Ex from '../../../main/ts/Part2Ex2ArrayFunctions'; +import { describe, it } from "@ephox/bedrock-client"; +import { assert } from "chai"; +import * as Ex from "../../../main/ts/Part2Ex2ArrayFunctions"; -describe('Exercise2ArrayFunctionsTest', () => { - it('runEach1', () => { +describe("Exercise2ArrayFunctionsTest", () => { + it("runEach1", () => { Ex.runEach1(); }); - it('frog names', () => { - assert.deepEqual(Ex.frogNames(Ex.myFrogs), [ 'frog1', 'frog2', 'loudfrog', 'quietfrog' ], 'frog names'); + it("frog names", () => { + assert.deepEqual( + Ex.frogNames(Ex.myFrogs), + ["frog1", "frog2", "loudfrog", "quietfrog"], + "frog names" + ); }); - it('frog ages', () => { - // TODO: write a test for your frog ages function + it("frog ages", () => { + assert.deepEqual(Ex.frogAges(Ex.myFrogs), [3, 4, 1, 10]); }); - it('ribbitting frogs', () => { - assert.deepEqual(Ex.ribbitting(Ex.myFrogs), [ - { name: 'frog1', ribbits: true, age: 3 }, - { name: 'loudfrog', ribbits: true, age: 1 } - ], 'ribbitting'); + it("ribbitting frogs", () => { + assert.deepEqual( + Ex.ribbitting(Ex.myFrogs), + [ + { name: "frog1", ribbits: true, age: 3 }, + { name: "loudfrog", ribbits: true, age: 1 }, + ], + "ribbitting" + ); }); - it('older frogs', () => { - assert.deepEqual(Ex.olderFrogs(Ex.myFrogs), [ - { name: 'quietfrog', ribbits: false, age: 10 } - ], 'older frogs'); + it("older frogs", () => { + assert.deepEqual( + Ex.olderFrogs(Ex.myFrogs), + [{ name: "quietfrog", ribbits: false, age: 10 }], + "older frogs" + ); }); - it('csvs', () => { - assert.deepEqual(Ex.splitCsvs([]), [], 'empty array'); - assert.deepEqual(Ex.splitCsvs(['a']), ['a'], 'single string'); - assert.deepEqual(Ex.splitCsvs(['a,b']), ['a', 'b'], 'single csv string'); - assert.deepEqual(Ex.splitCsvs(['a', 'b']), ['a', 'b'], 'several strings'); - assert.deepEqual(Ex.splitCsvs(['a', 'b,d,a']), ['a', 'b', 'd', 'a'], 'several csv strings'); + it("one or more ribbits", () => { + assert.deepEqual( + Ex.oneOrMoreRibbiting(Ex.myFrogs), + true, + "one or more ribbits" + ); + + const nonRibbitFrogs = [ + { name: "frog1", ribbits: false, age: 3 }, + { name: "frog2", ribbits: false, age: 4 }, + { name: "loudfrog", ribbits: false, age: 1 }, + { name: "quietfrog", ribbits: false, age: 10 }, + ]; + assert.deepEqual( + Ex.oneOrMoreRibbiting(nonRibbitFrogs), + false, + "no ribbits" + ); + }); + + it("has negative number", () => { + assert.deepEqual( + Ex.hasNegativeNumber([1, 2, 3, 4]), + false, + "no negative number" + ); + + assert.deepEqual( + Ex.hasNegativeNumber([-1, 2, 3, 4]), + true, + "has negative number" + ); + }); + + it("csvs", () => { + assert.deepEqual(Ex.splitCsvs([]), [], "empty array"); + assert.deepEqual(Ex.splitCsvs(["a"]), ["a"], "single string"); + assert.deepEqual(Ex.splitCsvs(["a,b"]), ["a", "b"], "single csv string"); + assert.deepEqual(Ex.splitCsvs(["a", "b"]), ["a", "b"], "several strings"); + assert.deepEqual( + Ex.splitCsvs(["a", "b,d,a"]), + ["a", "b", "d", "a"], + "several csv strings" + ); }); }); diff --git a/src/test/ts/part2/Exercise3OptionTest.ts b/src/test/ts/part2/Exercise3OptionTest.ts index ca0c397..0162918 100644 --- a/src/test/ts/part2/Exercise3OptionTest.ts +++ b/src/test/ts/part2/Exercise3OptionTest.ts @@ -1,20 +1,124 @@ -import { describe, it } from '@ephox/bedrock-client'; -import { assert } from 'chai'; -import * as Ex from '../../../main/ts/Part2Ex3Optional'; +import { describe, it } from "@ephox/bedrock-client"; +import { assert } from "chai"; +import * as Ex from "../../../main/ts/Part2Ex3Optional"; +import { Optional } from "@ephox/katamari"; -describe('Exercise3OptionTest', () => { - it('getProtocol', () => { - assert.equal(Ex.getProtocol('https://frog.com').getOrDie(), 'https'); - assert.equal(Ex.getProtocol('http://frog.com').getOrDie(), 'http'); - assert.isTrue(Ex.getProtocol('frog.com').isNone(), 'no protocol should be found'); - assert.isTrue(Ex.getProtocol('://frog.com').isNone(), 'no protocol should be found'); - assert.isTrue(Ex.getProtocol('3ttp://frog.com').isNone(), 'malformed protocol should not be registered'); +describe("Exercise3OptionTest", () => { + it("nonEmptyString", () => { + assert.isTrue(Ex.nonEmptyString("").isNone(), "emptystring"); + assert.equal( + Ex.nonEmptyString("hello").getOrDie(), + "hello", + "nonempty string" + ); }); - it('toPositiveInteger', () => { - // TODO: write a few test cases + it("getProtocol", () => { + assert.equal(Ex.getProtocol("https://frog.com").getOrDie(), "https"); + assert.equal(Ex.getProtocol("http://frog.com").getOrDie(), "http"); + assert.isTrue( + Ex.getProtocol("frog.com").isNone(), + "no protocol should be found" + ); + assert.isTrue( + Ex.getProtocol("://frog.com").isNone(), + "no protocol should be found" + ); + assert.isTrue( + Ex.getProtocol("3ttp://frog.com").isNone(), + "malformed protocol should not be registered" + ); + }); + + it("getNextSibling", () => { + const child1 = document.createElement("div"); + child1.setAttribute("id", "child1"); + const child2 = document.createElement("div"); + child2.setAttribute("id", "child2"); + const nestedChild = document.createElement("div"); + nestedChild.setAttribute("id", "nestedChild"); + child1.appendChild(nestedChild); + + document.body.append(child1); + document.body.append(child2); + + /* +
+
+
+ +
+ */ + + assert.equal( + Ex.getNextSibling(child1).getOrDie(), + document.getElementById("child2")! as ChildNode + ); + + assert.isTrue(Ex.getNextSibling(nestedChild).isNone()); + }); + + it("getAttribute", () => { + const el = document.createElement("div"); + el.setAttribute("id", "element"); + + assert.equal( + Ex.getAttribute(el, "id").getOrDie(), + el.getAttributeNode("id") + ); + assert.isTrue(Ex.getAttribute(el, "di").isNone()); + }); + + it("optionalDouble", () => { + assert.equal(Ex.optionalDouble(Optional.some(5)), 10); + assert.equal(Ex.optionalDouble(Optional.none()), 0); + }); + + it("trueIfSome", () => { + assert.isTrue(Ex.trueIfSome(Optional.some("hello"))); + assert.isFalse(Ex.trueIfSome(Optional.none())); + }); + + it("getAgeOrDefault", () => { + assert.deepEqual(Ex.getAgeOrDefault(Optional.some({ age: 5 })), { age: 5 }); + assert.deepEqual(Ex.getAgeOrDefault(Optional.none()), { age: 0 }); + }); + + it("toPositiveInteger", () => { + // DONE: write a few test cases + assert.isTrue(Ex.toPositiveInteger(5).equals(Optional.some(5))); + assert.isTrue(Ex.toPositiveInteger(0).isNone()); + assert.isTrue(Ex.toPositiveInteger(-1).isNone()); + }); + + it("optionalToArray", () => { + assert.deepEqual(Ex.optionalToArray(Optional.some(10)), [10]); + assert.deepEqual(Ex.optionalToArray(Optional.some("hello")), ["hello"]); + assert.deepEqual(Ex.optionalToArray(Optional.none()), []); + }); + + it("arrayToOptional", () => { + assert.isTrue(Ex.arrayToOptional([10]).equals(Optional.some(10))); + assert.isTrue(Ex.arrayToOptional(["hello"]).equals(Optional.some("hello"))); + assert.isTrue(Ex.arrayToOptional([]).isNone()); + }); + + it("addThreeToOptional", () => { + assert.isTrue( + Ex.addThreeToOptional(Optional.some(10)).equals(Optional.some(13)) + ); + assert.isTrue(Ex.addThreeToOptional(Optional.none()).isNone()); + }); + + it("prefixStrinigOptional", () => { + assert.isTrue( + Ex.prefixStringOptional(Optional.some(" world")).equals( + Optional.some("hello world") + ) + ); + assert.isTrue(Ex.prefixStringOptional(Optional.none()).isNone()); }); }); -// TODO: Now that you have finished all the test files in this directory, +// DONE: Now that you have finished all the test files in this directory, // try running all tests in the "part2" folder all using the `-d` argument in bedrock and specifying the parent directory. diff --git a/src/test/ts/part3/Part3Ex1Test.ts b/src/test/ts/part3/Part3Ex1Test.ts index b4e8bd7..a8f508c 100644 --- a/src/test/ts/part3/Part3Ex1Test.ts +++ b/src/test/ts/part3/Part3Ex1Test.ts @@ -1,7 +1,7 @@ -import { UiFinder } from '@ephox/agar'; -import { describe, it } from '@ephox/bedrock-client'; -import { TinyAssertions, TinyDom, TinyHooks } from '@ephox/mcagar'; -import { Editor } from 'tinymce'; +import { UiFinder } from "@ephox/agar"; +import { describe, it } from "@ephox/bedrock-client"; +import { TinyAssertions, TinyDom, TinyHooks } from "@ephox/mcagar"; +import { Editor } from "tinymce"; /* In this part we're going to discuss working with the editor in a little more @@ -9,14 +9,14 @@ detail. We'll look at this under the pretense of writing tests for editor functionality, and try to sneak in some knowledge about the editor itself as we go. */ -describe('Part3Ex1Test', () => { +describe("Part3Ex1Test", () => { /* Enter the TinyHooks module, from mcagar (mc - Tiny[MC]E, agar - another in-house testing library, more on that later). It lets you write the following. */ const hook = TinyHooks.bddSetup({ // And then you put your settings in here - toolbar: 'bold', + toolbar: "bold", }); /* NOTE: Even though we have assigned the result of our call to TinyHooks to a variable, @@ -29,7 +29,7 @@ describe('Part3Ex1Test', () => { TinyHooks takes care of a lot of important setup and teardown logic. It's important to understand the lifecycle hooks and how TinyHooks utilises them. - TODO: Take a look through Bedrock's docs and the BDD APIs available. + DONE: Take a look through Bedrock's docs and the BDD APIs available. McAgar's BDD docs: https://github.com/tinymce/tinymce/blob/develop/modules/mcagar/docs/bdd.md @@ -38,7 +38,7 @@ describe('Part3Ex1Test', () => { https://github.com/tinymce/tinymce/tree/develop/modules/mcagar/src/main/ts/ephox/mcagar/api/bdd */ - it('looks like an editor', () => { + it("looks like an editor", () => { // it is safe to access the hook inside an "it" block. const editor = hook.editor(); // Now what? @@ -56,10 +56,16 @@ describe('Part3Ex1Test', () => { UiFinder.exists(container, 'button[title="Bold"]'); }); - it('has content like an editor', () => { + it("has content like an editor", () => { const editor = hook.editor(); - editor.setContent(/* TODO */ "

Hello world

"); + editor.setContent( + /* DONE - tinymce removes unstyled spans by default */ + ` +

A paragraph with a span

+

A second paragraph

+ ` + ); /* Another useful module from mcagar, TinyAssertions is full of ways to make @@ -68,11 +74,11 @@ describe('Part3Ex1Test', () => { are numbers saying how many elements should match that selector inside the editor. - TODO: Edit the setContent call above to make this test pass. + DONE: Edit the setContent call above to make this test pass. */ TinyAssertions.assertContentPresence(editor, { p: 2, - span: 1 + span: 1, }); }); }); diff --git a/src/test/ts/part3/Part3Ex2Test.ts b/src/test/ts/part3/Part3Ex2Test.ts index 3ed86e6..feb10e8 100644 --- a/src/test/ts/part3/Part3Ex2Test.ts +++ b/src/test/ts/part3/Part3Ex2Test.ts @@ -1,26 +1,33 @@ -import { Cursors } from '@ephox/agar'; -import { beforeEach, describe, it } from '@ephox/bedrock-client'; -import { TinyAssertions, TinyDom, TinyHooks, TinySelections } from '@ephox/mcagar'; -import { Traverse } from '@ephox/sugar'; -import { assert } from 'chai'; -import { Editor } from 'tinymce'; - -describe('Part3Ex3Test', () => { - const hook = TinyHooks.bddSetup({ height: '100vh' }); +import { Cursors } from "@ephox/agar"; +import { beforeEach, describe, it } from "@ephox/bedrock-client"; +import { + TinyAssertions, + TinyDom, + TinyHooks, + TinySelections, +} from "@ephox/mcagar"; +import { Traverse } from "@ephox/sugar"; +import { assert } from "chai"; +import { Editor } from "tinymce"; + +describe("Part3Ex3Test", () => { + const hook = TinyHooks.bddSetup({ height: "100vh" }); beforeEach(() => { // Fun fact, hook.editor() also works inside of before / beforeEach // callbacks, as long as you declare the hook first. const editor = hook.editor(); - editor.setContent([ - '

Here is a bit of content

', - '

And some bolded content

', - '

A cat picture

' - ].join('')); + editor.setContent( + [ + "

Here is a bit of content

", + "

And some bolded content

", + '

A cat picture

', + ].join("") + ); }); - it('selects content like an editor', () => { + it("selects content like an editor", () => { const editor = hook.editor(); /* @@ -49,21 +56,27 @@ describe('Part3Ex3Test', () => { const contentBody = TinyDom.body(editor); // Let's get the

And some bolded content

- const paragraph = Traverse.child(contentBody, 1).getOrDie('Unable to find second child of editor body'); + const paragraph = Traverse.child(contentBody, 1).getOrDie( + "Unable to find second child of editor body" + ); // And then get the bolded (child 0 would be the text node "And some ") - const strong = Traverse.child(paragraph, 1).getOrDie('Unable to find second child of paragraph'); + const strong = Traverse.child(paragraph, 1).getOrDie( + "Unable to find second child of paragraph" + ); // And finally let's get the text node "bolded" - const text = Traverse.child(strong, 0).getOrDie('Unable to find text inside strong element'); + const text = Traverse.child(strong, 0).getOrDie( + "Unable to find text inside strong element" + ); range.setStart(text.dom, 0); - range.setEnd(text.dom, 'bolded'.length); + range.setEnd(text.dom, "bolded".length); editor.selection.setRng(range); - assert.equal(editor.selection.getContent(), 'bolded'); + assert.equal(editor.selection.getContent(), "bolded"); }); - it('selects content like an editor, take II', () => { + it("selects content like an editor, take II", () => { const editor = hook.editor(); /* Now in the last test, we put together a block of code that's a really good @@ -80,7 +93,7 @@ describe('Part3Ex3Test', () => { And here's how you'd do all of this with an agar API. */ const contentBody = TinyDom.body(editor); - const text = Cursors.follow(contentBody, [ 1, 1, 0 ]).getOrDie(); + const text = Cursors.follow(contentBody, [1, 1, 0]).getOrDie(); // Note that Cursors.follow only deals with DOM nodes, not offsets, and it uses the same node // for both the selection start and end node /* @@ -93,24 +106,50 @@ describe('Part3Ex3Test', () => { https://github.com/tinymce/tinymce/blob/develop/modules/mcagar/docs/bdd.md#tinyselections */ - TinySelections.setSelection(editor, [ 1, 1, 0 ], 0, [ 1, 1, 0 ], 'bolded'.length); - - assert.equal(editor.selection.getContent(), 'bolded'); + TinySelections.setSelection( + editor, + [1, 1, 0], + 0, + [1, 1, 0], + "bolded".length + ); + + assert.equal(editor.selection.getContent(), "bolded"); }); - it('operates on the selection', () => { + it("operates on the selection", () => { const editor = hook.editor(); - // TODO: move the selection to around the words "a bit of" - - // TODO: use an editor command to underline that content (https://www.tiny.cloud/docs/advanced/editor-command-identifiers/) - - TinyAssertions.assertContent(editor, [ - '

Here is a bit of content

', - '

And some bolded content

', - '

A cat picture

' - ].join('\n')); - - // TODO: Write an assertion to test your changes (hint: TinyAssertions) + // DONE: move the selection to around the words "a bit of" + TinySelections.setSelection( + editor, + [0, 0], + 8, + [0, 0], + 8 + "a bit of".length + ); + + // DONE: use an editor command to underline that content (https://www.tiny.cloud/docs/advanced/editor-command-identifiers/) + editor.execCommand("Underline"); + + TinyAssertions.assertContent( + editor, + [ + '

Here is a bit of content

', + "

And some bolded content

", + '

A cat picture

', + ].join("\n") + ); + + // DONE: Write an assertion to test your changes (hint: TinyAssertions) + editor.execCommand("Bold"); + TinyAssertions.assertContent( + editor, + [ + '

Here is a bit of content

', + "

And some bolded content

", + '

A cat picture

', + ].join("\n") + ); }); });