From eef2e333aebbf81a865e4b7f57f959d0bad5c1d4 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 16 Jul 2024 15:19:02 +0200 Subject: [PATCH] Added lots of content to the tutorials --- .vscode/settings.json | 4 +- README.md | 2 + tutorial/1 - intro.test.ts | 13 +- tutorial/2 - deriving.test.ts | 34 ++-- tutorial/3 - reacting.test.ts | 178 ++++++++++++++--- tutorial/4 - inner workings.test.ts | 61 +++--- tutorial/5 - unresolved.test.ts | 33 ++-- tutorial/6 - errors.test.ts | 260 +++++++++++++++++++++++- tutorial/7 - utils.test.ts | 297 ++++++++++++++++++++++++---- tutorial/8 - advanced.test.ts | 119 +++++++---- tutorial/9 - expert.test.ts | 59 +++--- 11 files changed, 859 insertions(+), 201 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 7e4750a..6e33d9d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,8 +2,8 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.organizeImports": true, - "source.fixAll": true + "source.organizeImports": "explicit", + "source.fixAll": "explicit" }, "json.schemas": [ { diff --git a/README.md b/README.md index 9ae9c59..47c5448 100644 --- a/README.md +++ b/README.md @@ -185,3 +185,5 @@ _Coming soon_ ### Cyclic reactors _Coming soon_ + +TODO: FIX THE README! diff --git a/tutorial/1 - intro.test.ts b/tutorial/1 - intro.test.ts index c7671f5..58a6fac 100644 --- a/tutorial/1 - intro.test.ts +++ b/tutorial/1 - intro.test.ts @@ -31,7 +31,8 @@ describe('intro', () => { --- Welcome to the tutorial! --- - Please look in \`./tutorial/1 - intro.ts\` to see what to do next.`, () => { + Please look in \`./tutorial/1 - intro.test.ts\` to see what to do next.`, () => { + // TODO: // At the start of the spec, there will be some setup. let bool = false; @@ -43,7 +44,7 @@ describe('intro', () => { * This can also be indicated with the `__YOUR_TURN__` variable. * * It should be clear what to do here... */ - bool = __YOUR_TURN__; + bool = true; expect(bool).toBeTrue(); // We use expectations like this to verify the result. }); @@ -55,7 +56,7 @@ describe('intro', () => { * ** Your Turn ** * Remove the `.skip` so this part of the tutorial will run. */ -describe.skip('the basics', () => { +describe('the basics', () => { /** * The `Atom` is the basic building block of `@skunkteam/sherlock`. * It holds a value which you can `get()` and `set()`. @@ -71,7 +72,7 @@ describe.skip('the basics', () => { // the `Atom`. expect(myValue$.get()).toEqual(1); - // ** Your Turn ** + myValue$.set(2); // Use the `.set()` method to change the value of the `Atom`. expect(myValue$.get()).toEqual(2); }); @@ -97,7 +98,7 @@ describe.skip('the basics', () => { * negative to a positive number and vice versa) of the original `Atom`. */ // Use `myValue$.derive(val => ...)` to implement `myInverse$`. - const myInverse$ = myValue$.derive(__YOUR_TURN__ => __YOUR_TURN__); + const myInverse$ = myValue$.derive(val => -val); expect(myInverse$.get()).toEqual(-1); // So if we set `myValue$` to -2: myValue$.set(-2); @@ -122,7 +123,7 @@ describe.skip('the basics', () => { * * Now react to `myCounter$`. In every `react()`. * Increase the `reacted` variable by one. */ - myCounter$.react(() => __YOUR_TURN__); + myCounter$.react(() => reacted++); expect(reacted).toEqual(1); // `react()` will react immediately, more on that later. diff --git a/tutorial/2 - deriving.test.ts b/tutorial/2 - deriving.test.ts index 5691dd9..1a51956 100644 --- a/tutorial/2 - deriving.test.ts +++ b/tutorial/2 - deriving.test.ts @@ -13,7 +13,7 @@ export const __YOUR_TURN__ = {} as any; * * There are a couple of ways to do this. */ -describe.skip('deriving', () => { +describe('deriving', () => { /** * In the 'intro' we have created a derivable by using the `.derive()` method. * This method allows the state of that `Derivable` to be used to create a @@ -37,7 +37,7 @@ describe.skip('deriving', () => { */ // We can combine txt with `repeat$.get()` here. - const lyric$ = text$.derive(txt => txt /* __YOUR_TURN__ */); + const lyric$ = text$.derive(txt => txt.repeat(repeat$.get())); expect(lyric$.get()).toEqual(`It won't be long`); @@ -74,12 +74,14 @@ describe.skip('deriving', () => { */ // Should return 'Fizz' when `myCounter$` is a multiple of 3 and '' otherwise. - const fizz$: Derivable = myCounter$.derive(__YOUR_TURN__); + const fizz$: Derivable = myCounter$.derive(v => (v % 3 === 0 ? 'Fizz' : '')); // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. - const buzz$: Derivable = myCounter$.derive(__YOUR_TURN__); + const buzz$: Derivable = myCounter$.derive(v => (v % 5 === 0 ? 'Buzz' : '')); - const fizzBuzz$: Derivable = derive(__YOUR_TURN__); + const fizzBuzz$: Derivable = derive( + () => (fizz$.get() + buzz$.get() === '' ? myCounter$.get() : fizz$.get() + buzz$.get()), // TODO: why not put it on the counter then? + ); expect(fizz$.get()).toEqual(''); expect(buzz$.get()).toEqual(''); @@ -153,9 +155,9 @@ describe.skip('deriving', () => { const tweetCount = pastTweets.length; const lastTweet = pastTweets[tweetCount - 1]; - expect(tweetCount).toEqual(__YOUR_TURN__); // Is there a new tweet? - expect(lastTweet).toContain(__YOUR_TURN__); // Who sent it? Donald? Or Barack? - expect(lastTweet).toContain(__YOUR_TURN__); // What did he tweet? + expect(tweetCount).toEqual(3); // Is there a new tweet? + expect(lastTweet).toContain('Donald'); // Who sent it? Donald? Or Barack? + expect(lastTweet).toContain('race'); // What did he tweet? /** * As you can see, this is something to look out for. @@ -200,22 +202,22 @@ describe.skip('deriving', () => { */ const fizz$ = myCounter$ .derive(count => count % 3) - .is(__YOUR_TURN__) - .and(__YOUR_TURN__) - .or(__YOUR_TURN__) as Derivable; + .is(0) + .and('Fizz') + .or(''); const buzz$ = myCounter$ .derive(count => count % 5) - .is(__YOUR_TURN__) - .and(__YOUR_TURN__) - .or(__YOUR_TURN__) as Derivable; + .is(0) + .and('Buzz') + .or(''); - const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(__YOUR_TURN__); + const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(myCounter$); // TODO: for (let count = 1; count <= 100; count++) { // Set the value of the `Atom`, myCounter$.set(count); - + // console.log(myCounter$.get() + ', ' + fizzBuzz$.get()); // and check if the output changed accordingly. checkFizzBuzz(count, fizzBuzz$.get()); } diff --git a/tutorial/3 - reacting.test.ts b/tutorial/3 - reacting.test.ts index 162d4d3..d7dbd05 100644 --- a/tutorial/3 - reacting.test.ts +++ b/tutorial/3 - reacting.test.ts @@ -11,7 +11,7 @@ export const __YOUR_TURN__ = {} as any; * In the intro we have seen a basic usage of the `.react()` method. * Let's dive a bit deeper into the details of this method. */ -describe.skip('reacting', () => { +describe('reacting', () => { // For easy testing we can count the number of times a reactor was called, let wasCalledTimes: number; // and record the last value it reacted to. @@ -69,10 +69,13 @@ describe.skip('reacting', () => { * Time to react to `myAtom$` with the `reactor()` function defined * above. */ + myAtom$.react((val, _) => reactor(val)); + // myAtom$.react(reactor); // OR this. TS will ignore any additional arguments you might give it. expectReact(1, 'initial value'); // Now set a 'new value' to `myAtom$`. + myAtom$.set('new value'); expectReact(2, 'new value'); }); @@ -99,8 +102,13 @@ describe.skip('reacting', () => { * * catch the returned `stopper` in a variable */ - myAtom$.react(reactor); + // let stopFunc: () => void = () => {}; // dummy initial value + // myAtom$.react((val, stop) => { + // reactor(val); + // stopFunc = stop; + // }); + const stopFunc = myAtom$.react((val, _) => reactor(val)); expectReact(1, 'initial value'); /** @@ -108,6 +116,7 @@ describe.skip('reacting', () => { * * Call the `stopper`. */ + stopFunc(); myAtom$.set('new value'); @@ -130,9 +139,9 @@ describe.skip('reacting', () => { * In the reaction below, use the stopper callback to stop the * reaction */ - myAtom$.react((val, __YOUR_TURN___) => { + myAtom$.react((val, stop) => { reactor(val); - __YOUR_TURN___; + stop(); }); expectReact(1, 'initial value'); @@ -185,7 +194,7 @@ describe.skip('reacting', () => { * * Try giving `boolean$` as `until` option. */ - string$.react(reactor, __YOUR_TURN__); + string$.react(reactor, { until: boolean$ }); // It should react directly as usual. expectReact(1, 'Value'); @@ -233,7 +242,7 @@ describe.skip('reacting', () => { * Use `!string$.get()` to return `true` when the `string` is * empty. */ - string$.react(reactor, __YOUR_TURN__); + string$.react(reactor, { until: () => !string$.get() }); // It should react as usual: string$.set('New value'); @@ -250,9 +259,8 @@ describe.skip('reacting', () => { /** * Since the example above where the `until` is based on the parent - * `Derivable` occurs very frequently. - * - * This `Derivable` is given as a parameter to the `until` function. + * `Derivable` occurs very frequently, this `Derivable` is given as + * a parameter to the `until` function. */ it('the parent `Derivable`', () => { /** @@ -261,7 +269,7 @@ describe.skip('reacting', () => { * Try using the first parameter of the `until` function to do * the same as above. */ - string$.react(reactor, __YOUR_TURN__); + string$.react(reactor, { until: s => !s.get() }); // It should react as usual. string$.set('New value'); @@ -276,6 +284,31 @@ describe.skip('reacting', () => { // was never given to the reactor: expectReact(3, 'Newer Value'); }); + + /** + * Sometimes, the syntax may leave you confused. + */ + it('syntax issues', () => { + // It looks this will start reacting until `boolean$`s value is false... + let stopper = boolean$.react(reactor, { until: b => !b }); + + // ...but does it? (Remember: `boolean$` starts out as `false`) + expect(boolean$.connected).toBe(__YOUR_TURN__); + + // The `b` it obtains as argument is a `Derivable`. This is a + // reference value which will evaluate to `true` as it is not `undefined`. + // Thus, the negation will evaluate to `false`, independent of the value of + // the boolean. You can get the boolean value our of the `Derivable` using `.get()`: + stopper(); + stopper = boolean$.react(reactor, { until: b => !b.get() }); + expect(boolean$.connected).toBe(__YOUR_TURN__); + + // You can also return the `Derivable` and apply the negation + // with the method designed for it: + stopper(); + boolean$.react(reactor, { until: b => b.not() }); + expect(boolean$.connected).toBe(__YOUR_TURN__); + }); }); /** @@ -291,7 +324,7 @@ describe.skip('reacting', () => { * the parent derivable as first parameter when it's called.) * * * Note: when using `from`, `.react()` will (most often) not react - * synchronously any more. As that is the function of this option.* + * synchronously any more. As that is the function of this option.* // TODO: word differently... is not a `note`, but the intended effect. */ it('reacting `from`', () => { const sherlock$ = atom(''); @@ -305,7 +338,7 @@ describe.skip('reacting', () => { * * *Hint: remember the `.is()` method from tutorial 2?* */ - sherlock$.react(reactor, __YOUR_TURN__); + sherlock$.react(reactor, { from: sherlock$.is('dear') }); expectReact(0); ['Elementary,', 'my', 'dear', 'Watson'].forEach(txt => sherlock$.set(txt)); @@ -321,9 +354,6 @@ describe.skip('reacting', () => { * Where `until` and `from` can only be triggered once to stop or start * reacting, `when` can be flipped as often as you like and the reactor * will respect the current state of the `when` function/Derivable. - * - * *Note: as with `from` this can prevent `.react()` from reacting - * synchronously.* */ it('reacting `when`', () => { const count$ = atom(0); @@ -334,7 +364,10 @@ describe.skip('reacting', () => { * Now, let's react to all even numbers. * Except 4, we don't want to make it too easy now. */ - count$.react(reactor, __YOUR_TURN__); + count$.react(reactor, { when: v => v.get() % 2 === 0 && v.is(4).not() }); + count$.react(reactor, { when: v => v.get() % 2 === 0 && v.get() !== 4 }); + // TODO: why can I apply `&&` to `number` and Derivable?? + // >>> e.g. `when` kan zowel booleans and Derivable vanwege Unwrappable type xD expectReact(1, 0); @@ -359,9 +392,26 @@ describe.skip('reacting', () => { /** * ** Your Turn ** * - * Say you want to react when `done$` is true. But not right away.. + * Say you want to react when `done$` is true. But not right away.. // TODO: change to use number? */ - done$.react(reactor, __YOUR_TURN__); + done$.react(reactor, { when: d => d.is(true) }); // TODO: true expected answer given description: the test case needs asjustment! + // SKIPFIRST negeert de eerste keer dat WHEN true is! Niet de eerste keer in general. + // `// Doesn't react, because the new value equals the previous value that was seen by the reactor.` + // libs/sherlock/src/lib/reactor/reactor.test.ts:136 + // Hij accepteert alleen waardes die anders zijn dan zijn huidige. Omdat hij alleen `true` accepteert, kan hij nooit meer updaten! + // => false accepteert de `when` niet; + // => true is zelfde als voorheen. + // Ik denk dat hij, ondanks dat `skipFirst` de eerste true genegeerd heeft, hij hem wel onthouden heeft als last seen value. Expected! + // Zie libs/sherlock/src/lib/derivable/mixins/take.ts voor volgorde van events? + // Als je `events` wilt, kan je beter Observables ofzo gebruiken. Je wilt dit patroon van "elke keer dat je true ziet, pas aan" eigenlijk niet hier. + // kan beter numbers gebruiken om dit te testen! `<= 4` ofzo + // En extra testje hiervoor! + expectReact(0); + + done$.set(true); + expectReact(0); + + done$.set(false); expectReact(0); done$.set(true); @@ -387,7 +437,8 @@ describe.skip('reacting', () => { * * *Hint: you will need to combine `once` with another option* */ - finished$.react(reactor, __YOUR_TURN__); + finished$.react(reactor, { once: true, when: f => f.get() }); // TODO: make sure the test captures the diff between `f` and `f.get()` here! + // see next `challenge` for a case where there is a difference. expectReact(0); // When finished it should react once. @@ -401,9 +452,71 @@ describe.skip('reacting', () => { }); }); + describe('order of execution', () => { + // the interactions between `from`, `until`, `when`, `skipFirst`, `once`... - that order! + // als het goed is nog niet behandeld (libs/sherlock/src/lib/derivable/mixins/take.ts) + + /** + * The options `from`, `until`, `when`, `skipFirst` and `once` are tested in this specific order: + * 1) firstly, `from` is checked. If `from` is/was true (or is not set in the options), we continue: + * 2) secondly, `until` is checked. If `until` is false (or is not set in the options), we continue: + * 3) thirdly, `when` is checked. If `when` is true (or is not set in the options), we continue: + * 4) fourthly, `skipFirst` is checked. If `skipFirst` is false (or is not set in the options), we continue: + * 5) lastly, `once` is checked. + * + * This means, for example, that `skipFirst` is only checked when `from` is true or unset, `until` is false or unset, + * and `when` is true or unset. If e.g. `when` evaluates to false, `skipFirst` cannot trigger. + */ + it('`from` and `until`', () => { + const myAtom$ = atom(0); + myAtom$.react(reactor, { from: v => v.is(3), until: v => v.is(2) }); + + for (let i = 1; i <= 5; i++) { + myAtom$.set(i); + } + + // the reactor starts reacting when `myAtom` gets the value 3, but stops when it gets the value 2. + // But because `myAtom` obtains the value 2 before it obtains 3... + // ...how many times was the reactor called, if any? + expectReact(__YOUR_TURN__); + }); + + it('`when` and `skipFirst`', () => { + const myAtom$ = atom(0); + myAtom$.react(reactor, { when: v => v.is(1), skipFirst: true }); + + myAtom$.set(1); + + // the reactor reacts when `myAtom` is 1 but skips the first number. + // `myAtom` starts at 0. Does the reactor skip the 0 or the 1? + expectReact(__YOUR_TURN__); + }); + + it('`from`, `until`, `when`, `skipFirst`, and `once`', () => { + const myAtom$ = atom(0); + myAtom$.react(reactor, { + from: v => v.is(5), + until: v => v.is(1), + when: v => [2, 3, 4].includes(v.get()), + skipFirst: true, + once: true, + }); + + for (let v of [1, 2, 3, 5, 4, 3, 2, 1, 2, 3]) { + myAtom$.set(v); + } + + // `from` and `until` allow the reactor to respectively start when `myAtom` has value 5, and stop when it has value 1. + // Meanwhile, `when` allows neither of those values and only allows the values 2, 3, and 4. + // `skipFirst` and `once` are also added, just to bring the whole group together. + // so, how many times is the reactor called, and what was the last argument (if any)? + expectReact(__YOUR_TURN__); + }); + }); + describe('challenge', () => { it('onDisconnect', () => { - const connected$ = atom(false); + const connected$ = atom(false); // TODO: change to use number /** * ** Your Turn ** @@ -413,8 +526,23 @@ describe.skip('reacting', () => { * * `connected$` indicates the current connection status. * This should be possible with three simple ReactorOptions + * Hint: do not use `when`! */ - connected$.react(reactor, __YOUR_TURN__); + connected$.react(reactor, { from: c => c, skipFirst: true, once: true }); // WORKS, and intended + connected$.react(reactor, { from: _ => connected$, skipFirst: true, once: true }); // WORKS, and intended + connected$.react(reactor, { from: connected$, skipFirst: true, once: true }); // WORKS, and intended + + // TODO: + // `when: c => !c.get()` gets the boolean out of the Derivable, applies `not`, and returns + // `when: c => !c` coerces the Derivable to a boolean (whether it exists: true), applies `not` to this boolean, and returns false. + // `when: c => c.not()` takes the boolean out of the Derivable, applies `not`, puts it back in a Derivable, and `when` is overloaded + // ...to also be able to take the boolean out of the Derivable! So that is how you can also pass a Derivable - `when` takes the boolean out! + // connected$.react(reactor, { when: c => !c.get(), from: c => c.get() }); // 1. DOES NOT WORK - the connection is not false afterwards + // connected$.react(reactor, { when: c => !c, from: c => c }); // 2. DOES NOT WORK - see above + // connected$.react(reactor, { when: c => !c.get(), skipFirst: true }); // 3. DOES NOT WORK... + // ...as the first time c is false, this is accepted in the system even though skipfirst is true. Then... + // ...the second time that c is false, it is seen as the same value and thus not accepted (only changes are accepted)! Hence: + // setting a Derivable with a value it already has does not trigger it. It does not even go to `when`. // It starts as 'not connected' expectReact(0); @@ -427,8 +555,14 @@ describe.skip('reacting', () => { connected$.set(false); expectReact(1, false); + // After that, nothing should change anymore. + connected$.set(true); + expectReact(1, false); + connected$.set(false); + expectReact(1, false); + // It should not react again after this. - expect(connected$.connected).toBeFalse; + expect(connected$.connected).toBeFalse(); // * Note: this `.connected` refers to whether this `Derivable` // is being (indirectly) observed by a reactor. }); diff --git a/tutorial/4 - inner workings.test.ts b/tutorial/4 - inner workings.test.ts index 9daf82b..faf51cc 100644 --- a/tutorial/4 - inner workings.test.ts +++ b/tutorial/4 - inner workings.test.ts @@ -11,7 +11,7 @@ export const __YOUR_TURN__ = {} as any; /** * Time to dive a bit deeper into the inner workings of `@skunkteam/sherlock`. */ -describe.skip('inner workings', () => { +describe('inner workings', () => { /** * What if there is a derivation that reads from one of two `Derivable`s * dynamically? Will both of those `Derivable`s be tracked for changes? @@ -43,8 +43,9 @@ describe.skip('inner workings', () => { * * What do you expect? */ - expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(reacted).toHaveBeenCalledTimes(1); + expect(reacted).toHaveBeenLastCalledWith(1, expect.toBeFunction()); // TODO: NIET omdat hij weet dat de string niets verandert, + // maar juist omdat hij dezelfde waarde binnen krijgt en dus niet nogmaals triggered!! // `switch$` is still set to true (number) number$.set(2); @@ -54,8 +55,8 @@ describe.skip('inner workings', () => { * * What do you expect? */ - expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(reacted).toHaveBeenCalledTimes(2); + expect(reacted).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // Now it gets a different value!! // Now let's reset the mock function, so the call count should // be 0 again. @@ -71,8 +72,8 @@ describe.skip('inner workings', () => { * * What do you expect now? */ - expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(reacted).toHaveBeenCalledTimes(1); + expect(reacted).toHaveBeenLastCalledWith('two', expect.toBeFunction()); // It gets a different value than last time again. No need for the reset...? TODO: }); /** @@ -85,7 +86,7 @@ describe.skip('inner workings', () => { const hasDerived = jest.fn(); const myAtom$ = atom(true); - const myDerivation$ = myAtom$.derive(hasDerived); + const myDerivation$ = myAtom$.derive(hasDerived); // NOTE: React causes an immediate update. Derive does not! /** * ** Your Turn ** @@ -98,17 +99,17 @@ describe.skip('inner workings', () => { */ // Well, what do you expect? - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(0); myDerivation$.get(); // And after a `.get()`? - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(1); myDerivation$.get(); // And after the second `.get()`? Is there an extra call? - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(2); /** * The state of any `Derivable` can change at any moment. @@ -147,27 +148,27 @@ describe.skip('inner workings', () => { * * Ok, it's your turn to complete the expectations. */ - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(1); // because of the react. myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(1); // no update because someone is reacting, and there has been no update in value. myAtom$.set(false); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(2); // `myDerivation`s value has changed, so update. myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(2); // no update. stopper(); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(2); // stopping doesn't change the value... myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(3); // ...but now, it is not being reacted to, so it goes back to updating every time `.get()` is called! /** * Since the `.react()` already listens to the value(changes) there is @@ -176,6 +177,9 @@ describe.skip('inner workings', () => { * But when the reactor has stopped, the derivation has to be calculated * again. */ + // Okay, clear, but why? + // I see... because we don't want to keep internal states and such and track changes when no-one is listening! It is a waste of effort. + // So we only keep track of changes when a react is listening. }); /** @@ -212,23 +216,23 @@ describe.skip('inner workings', () => { // Note that this is the same value as it was initialized with myAtom$.set(1); - expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(first).toHaveBeenCalledTimes(1); + expect(second).toHaveBeenCalledTimes(1); myAtom$.set(2); - expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(first).toHaveBeenCalledTimes(2); // different INPUT [2], so call again + expect(second).toHaveBeenCalledTimes(1); // same INPUT [false], so no change myAtom$.set(3); - expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(first).toHaveBeenCalledTimes(3); // different INPUT [3] + expect(second).toHaveBeenCalledTimes(2); // different INPUT [true] myAtom$.set(4); - expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(first).toHaveBeenCalledTimes(4); // different INPUT [4] + expect(second).toHaveBeenCalledTimes(2); // same INPUT [true] /** * Can you explain the behavior above? @@ -256,6 +260,7 @@ describe.skip('inner workings', () => { const hasReacted = jest.fn(); atom$.react(hasReacted, { skipFirst: true }); + expect(hasReacted).toHaveBeenCalledTimes(0); // added for clarity, in case people missed the `skipFirst` or its implication atom$.set({}); @@ -265,7 +270,7 @@ describe.skip('inner workings', () => { * The `Atom` is set with exactly the same object as before. Will the * `.react()` fire? */ - expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasReacted).toHaveBeenCalledTimes(1); // not considered equal (`{} !== {}`) /** * But what if you use an object, that can be easily compared through a @@ -284,7 +289,7 @@ describe.skip('inner workings', () => { * * Do you think the `.react()` fired with this new value? */ - expect(hasReacted).toHaveBeenCalledTimes(0); + expect(hasReacted).toHaveBeenCalledTimes(0); // TODO: answer already given - is considered equal atom$.set(Seq.Indexed.of(1, 2)); @@ -293,7 +298,7 @@ describe.skip('inner workings', () => { * * And now? */ - expect(hasReacted).toHaveBeenCalledTimes(1); + expect(hasReacted).toHaveBeenCalledTimes(1); // TODO: answer already given - obviously unequal /** * In `@skunkteam/sherlock` equality is a bit complex: diff --git a/tutorial/5 - unresolved.test.ts b/tutorial/5 - unresolved.test.ts index 998b121..24c9091 100644 --- a/tutorial/5 - unresolved.test.ts +++ b/tutorial/5 - unresolved.test.ts @@ -17,7 +17,7 @@ export const __YOUR_TURN__ = {} as any; * state, called `unresolved`. This indicates that the data is not available * yet, but (probably) will be at some point. */ -describe.skip('unresolved', () => { +describe('unresolved', () => { /** * Let's start by creating an `unresolved` `Derivable`. */ @@ -27,13 +27,14 @@ describe.skip('unresolved', () => { // since it can't be inferred by TypeScript this way. const myAtom$ = atom.unresolved(); - expect(myAtom$.resolved).toEqual(__YOUR_TURN__); + expect(myAtom$.resolved).toEqual(false); /** * ** Your Turn ** * * Resolve the atom, it's pretty easy */ + myAtom$.set(1); expect(myAtom$.resolved).toBeTrue(); }); @@ -48,7 +49,7 @@ describe.skip('unresolved', () => { * * Time to create an `unresolved` Atom.. */ - const myAtom$: DerivableAtom = __YOUR_TURN__; + const myAtom$: DerivableAtom = atom.unresolved(); expect(myAtom$.resolved).toBeFalse(); @@ -63,17 +64,18 @@ describe.skip('unresolved', () => { * * What do you expect? */ - expect(myAtom$.resolved).toEqual(__YOUR_TURN__); + expect(myAtom$.resolved).toEqual(true); // .toThrow() or .not.toThrow()? ↴ - expect(() => myAtom$.get()) /*__YOUR_TURN__*/; + expect(() => myAtom$.get()).not.toThrow(); }); /** * If a `Derivable` is `unresolved` it can't react yet. But it will * `.react()` if a value becomes available. * - * *Note that this can prevent `.react()` from executing immediately* + * *Note that this can prevent `.react()` from executing immediately* // TODO: what annoying messages... I want to change them. + * It is not a 'Note: side-effect' but the expected intended behavior! */ it('reacting to `unresolved`', () => { const myAtom$ = atom.unresolved(); @@ -86,13 +88,14 @@ describe.skip('unresolved', () => { * * What do you expect? */ - expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasReacted).toHaveBeenCalledTimes(0); /** * ** Your Turn ** * * Now make the last expect succeed */ + myAtom$.set(`woohoow, I was called`); expect(myAtom$.resolved).toBeTrue(); expect(hasReacted).toHaveBeenCalledExactlyOnceWith(`woohoow, I was called`, expect.toBeFunction()); @@ -100,7 +103,7 @@ describe.skip('unresolved', () => { /** * In `@skunkteam/sherlock` there is no reason why a `Derivable` should not - * become `unresolved` again after it has been set. + * be able to become `unresolved` again after it has been set. */ it('can become `unresolved` again', () => { const myAtom$ = atom.unresolved(); @@ -112,6 +115,7 @@ describe.skip('unresolved', () => { * * Set the value.. */ + myAtom$.set(`it's alive!`); expect(myAtom$.get()).toEqual(`it's alive!`); @@ -120,6 +124,7 @@ describe.skip('unresolved', () => { * * Unset the value.. (*Hint: TypeScript is your friend*) */ + myAtom$.unset(); expect(myAtom$.resolved).toBeFalse(); }); @@ -140,14 +145,14 @@ describe.skip('unresolved', () => { * * Combine the two `Atom`s into one `Derivable` */ - const myDerivable$: Derivable = __YOUR_TURN__; + const myDerivable$: Derivable = myString$.derive(s => s + myOtherString$.get()); /** * ** Your Turn ** * * Is `myDerivable$` expected to be `resolved`? */ - expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); + expect(myDerivable$.resolved).toEqual(false); // Now let's set one of the two source `Atom`s myString$.set('some'); @@ -157,12 +162,12 @@ describe.skip('unresolved', () => { * * What do you expect to see in `myDerivable$`. */ - expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); + expect(myDerivable$.resolved).toEqual(false); // And what if we set `myOtherString$`? myOtherString$.set('data'); - expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); - expect(myDerivable$.get()).toEqual(__YOUR_TURN__); + expect(myDerivable$.resolved).toEqual(true); + expect(myDerivable$.get()).toEqual('somedata'); /** * ** Your Turn ** @@ -171,6 +176,6 @@ describe.skip('unresolved', () => { * What do you expect `myDerivable$` to be? */ myString$.unset(); - expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); + expect(myDerivable$.resolved).toEqual(false); }); }); diff --git a/tutorial/6 - errors.test.ts b/tutorial/6 - errors.test.ts index 1c72480..e8ed4b4 100644 --- a/tutorial/6 - errors.test.ts +++ b/tutorial/6 - errors.test.ts @@ -1,5 +1,259 @@ -describe.skip('errors', () => { - it('placeholder', () => { - expect(true).toEqual(true); +import { + atom, + DerivableAtom, + error, + ErrorWrapper, + final, + FinalWrapper, + MaybeFinalState, + unresolved, +} from '@skunkteam/sherlock'; +import { finalGetter, makeFinalMethod, setFinalMethod } from 'libs/sherlock/src/lib/derivable/mixins'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +// In libs/sherlock/src/lib/interfaces.ts:289, the basic states a Derivable can have are shown. +// > `export type State = V | unresolved | ErrorWrapper;` +// A state can be either any type `V` (`number`, `string`, etc.), `unresolved` as we saw in the +// previous tutorial, or `ErrorWrapper`. This last state is explained here. +describe('errors', () => { + let myDerivable: DerivableAtom; + + beforeEach(() => { + myDerivable = atom(1); + }); + + it('basic errors', () => { + // `errored` shows whether the last statement resulted in an error. + // It does NOT show whether the `Derivable` is in an error state. + expect(myDerivable.errored).toBe(false); + expect(myDerivable.error).toBeUndefined; + expect(myDerivable.getState()).toBe(1); // as explained above, any type can be a state + + // We can set errors using the `setError()` function. + myDerivable.setError('my Error'); + + expect(myDerivable.errored).toBe(true); + expect(myDerivable.error).toBe('my Error'); + // The `ErrorWrapper` state only holds an error string. The `error` function returns + // such an `ErrorWrapper` which we can use to compare. + expect(myDerivable.getState()).toMatchObject(error('my Error')); + + // As expected, calling `get()` on `myDerivable` gives an error. + expect(myDerivable.get).toThrow("Cannot read properties of undefined (reading 'getState')"); // TODO: WHAT - normally this works, but internal JEST just fucks with me....? + expect(() => myDerivable.get()).toThrow('my Error'); + expect(myDerivable.errored).toBe(true); + + // ** __YOUR_TURN__ ** + // What will happen if you try to call `set()` on `myDerivable`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myDerivable.set(2)).not.toThrow(); + // expect(() => myDerivable.set(2)) /* __YOUR_TURN__ */ + // expect(myDerivable.errored).toBe(__YOUR_TURN__); + // `.toBe(2)` or `.toMatchObject(error('my Error'))`? ↴ + expect(myDerivable.getState()).toBe(2); + // expect(myDerivable.getState()) /* __YOUR_TURN__ */ + + // Interestingly, calling `set()` does not throw an error. In fact, it removes the error state + // altogether. This means we can call `get()` again. + expect(() => myDerivable.get()).not.toThrow(); + }); + + it('deriving to an error', () => { + const myDerivable2 = myDerivable.derive(v => v + 1); + + // If the original derivable suddenly errors... + myDerivable.setError('division by zero'); + + // ...what happens to `myDerivable2`? + // `.toBe(2)` or `.toMatchObject(error('division by zero'))`? ↴ + expect(myDerivable2.getState()).toMatchObject(error('division by zero')); + // expect(myDerivable2.getState()) /* __YOUR_TURN__ */ + + // EXPLANATION AND MORE TODO: + }); + + it('reacting to an error', () => { + const doNothing: (v: number) => void = _ => {}; + myDerivable.react(doNothing); + + // ** __YOUR_TURN__ ** + // Will an error be thrown when reacting to a Derivable that throws an error? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myDerivable.setError('my Error')).toThrow('my Error'); + // expect(() => myDerivable.setError('my Error')) + + // Reacting to a Derivable that throws an error will make the reactor throw as well. + // Because the reactor will usually fire when it gets connected, it also throws when + // you try to connect it after the error has already been set. + + myDerivable = atom(1); + myDerivable.setError('my second Error'); + + // ** __YOUR_TURN__ ** + // Will an error be thrown when you use `skipFirst`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myDerivable.react(doNothing, { skipFirst: true })).toThrow('my second Error'); + // expect(() => myDerivable.react(doSomething, { skipFirst: true })) + + // And will an error be thrown when `from = false`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myDerivable.react(doNothing, { from: false })).not.toThrow(); + // expect(() => myDerivable.react(doNothing, { from: false })) + + // When `from = false`, the reactor is disconnected, preventing the error message from entering. + // `skipFirst`, on the other hand, does allow the value in, but just does not trigger an update. + // This is similar if you change the boolean afterwards. + + // TODO: This is probably redundant. + let b = false; + expect(() => myDerivable.react(doNothing, { from: b })).not.toThrow(); + expect(() => (b = true)).not.toThrow(); + }); + + // always is `stopOnError` used in a DERIVABLE.TAKE, not a DERIVABLE.REACT...? + // libs/sherlock/src/lib/derivable/mixins/take.tests.ts + // 1034, 825... + + it('`mapState` to reason over errors', () => { + const mapping$ = myDerivable.mapState(state => { + if (state === unresolved) { + return atom('unresolved'); + } + if (state instanceof ErrorWrapper) { + return atom('error'); + } + return atom(myDerivable.get().toString()); + }); + + // You can get the mapped value out by using `.get()`. But then, to check the value of that atom, again `.get()`. + expect(mapping$.get().get()).toBe('1'); + + myDerivable.unset(); + expect(mapping$.get().get()).toBe('unresolved'); + + myDerivable.setError('Just a random error.'); + expect(mapping$.get().get()).toBe('error'); + }); + + it('TEMP', () => { + // FINAL + // libs/sherlock/src/lib/utils/final-wrapper.ts + + // TODO: EXPLAIN WHY YOU WOULD WANT THIS + let myAtom$ = atom(1); + + // every atom has a `final` property. + expect(myAtom$.final).toBeFalse(); + + // you can make an atom final using the `makeFinal()` function. + myAtom$.makeFinal(); + expect(myAtom$.final).toBeTrue(); + + // final atoms cannot be set anymore, but can be get. + expect(() => myAtom$.set(2)).toThrow('cannot set a final derivable'); + expect(() => myAtom$.get()).not.toThrow(); + + // alternatively, you can set a last value before setting it to `final`. + // Obviously, if the state is already `final`, this function will also throw an error. + expect(() => myAtom$.setFinal(2)).toThrow('cannot set a final derivable'); + myAtom$ = atom(1); // reset + myAtom$.setFinal(2); // try again + expect(myAtom$.final).toBeTrue(); + + // Every Derivable has a state. We have seen that states (`State`) can be `undefined`, `ErrorWrapper`, + // or any regular type `V`. Other states exist, such as the `MaybeFinalState`. This state can be either + // a normal state `State` or a special `FinalWrapper>` state. Let's see that in action. + myAtom$ = atom(1); + expect(myAtom$.getMaybeFinalState()).toBe(1); // `getMaybeFinalState` can return a normal state, which in turn can be a normal type + myAtom$.makeFinal(); + expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState` can also return a `FinalWrapper` type! + + myAtom$ = atom(1); + // But what is the point of this? What can we do with these "states"? + // You can pattern match on the state to find out what the situation is. + // + // + // FIXME: But no seriously, what is the point of this STATE? You already have the boolean to check for final. + // FIXME: and what is the difference between Constants and Finals? Just that you can SET a final whenever you want? + // then isn't a Final just more powerful than a constant? + // FIXME: and when would you use this, in a real scenario? + // + // + // Let's first define a small checking function as we don't know exactly what type we deal with. + function verifyState(state: MaybeFinalState, value: T, final: boolean): void { + if (state instanceof FinalWrapper) { + expect(final).toBeTrue(); + expect(state.value).toBe(value); + } else { + expect(final).toBeFalse(); + expect(state).toBe(value); + } + } + + let myAtomState$ = myAtom$.getMaybeFinalState(); + verifyState(myAtomState$, 1, false); // the state is the same as my value. + + myAtom$.setFinal(2); + myAtomState$ = myAtom$.getMaybeFinalState(); + verifyState(myAtomState$, 2, true); // the final state still contains my value! + + // + // + // + + const final$ = final(1); + // finals cannot be `set`. See for yourself by uncommenting the next line. + // const final$.value = 2; + // finals can be `get` + expect(final$.value).toBe(1); + + finalGetter; + setFinalMethod; + makeFinalMethod; + // markFinal; + + // const myAtomMaybeFinal$ = myAtom$.getMaybeFinalState(); + // myAtomMaybeFinal$ + + // A normal state is called `State; a final state is `FinalWrapper>`, so a + // `MaybeFinalState` can be either! :: + // export type MaybeFinalState = State | FinalWrapper>; + // export type State = V | unresolved | ErrorWrapper; + + let a: MaybeFinalState = 1; + // a FinalWrapper can be made using the `final` function. + a = final(1); // similar to `atom`, but makes a FinalWrapper instead of an Atom. + // This is just syntactic sugar for: + a = FinalWrapper.wrap(1); + a = FinalWrapper.wrap(a); // this does nothing. + expect(a).toBeInstanceOf(FinalWrapper); + + // You can also use other functions. + a = FinalWrapper.unwrap(a); // does the opposite: get the V out of the FinalWrapper. + expect(a).not.toBeInstanceOf(FinalWrapper); // now it is not a FinalWrapper, but a State! + + // also has its own Map function + a = FinalWrapper.map(1, v => v + 1); + expect(a).toBe(2); + a = FinalWrapper.map(final(1), v => v + 1); + expect(a).toMatchObject(final(2)); }); }); + +/** + * Final States; (finalGetter, finalMethod, getMaybeFinalState, FinalWrapper, MaybeFinalState, _isFinal, makeFinal, markFinal, .final, .finalized, setFinal...) + * Lens; (libs/sherlock/src/lib/derivable/lens.ts) - ??? + * x Lift; (libs/sherlock-utils/src/lib/lift.ts) + * Peek; (libs/sherlock-utils/src/lib/peek.ts) - ??? + * x Template; (libs/sherlock-utils/src/lib/template.ts) - to make a string using a template literal. (Uses unwrap!!) + * / Factory; (libs/sherlock/src/lib/derivable/factories.ts) - simply contains functions to create objects, namely + * lens; atom; constant; derive. + * Flat-map; (libs/sherlock/src/lib/derivable/mixins/flat-map.ts) - ??? + * Fallback-to; + */ diff --git a/tutorial/7 - utils.test.ts b/tutorial/7 - utils.test.ts index 3e49e8e..695aa53 100644 --- a/tutorial/7 - utils.test.ts +++ b/tutorial/7 - utils.test.ts @@ -1,5 +1,7 @@ -import { atom } from '@skunkteam/sherlock'; -import { pairwise, scan, struct } from '@skunkteam/sherlock-utils'; +import { atom, Derivable } from '@skunkteam/sherlock'; +import { lift, pairwise, scan, struct } from '@skunkteam/sherlock-utils'; + +// FIXME: // interne review document, mocht ik iets hebben om te laten zien! In Google Drive, zet het erin! /** * ** Your Turn ** @@ -16,14 +18,14 @@ expect(struct).toBe(struct); /** * In the `sherlock-utils` lib, there are a couple of functions that can combine * multiple values of a single `Derivable` or combine multiple `Derivable`s into - * one. We will show a couple of those here. + * one. We will show a couple of those here. TODO: Hmm, I want to see some others too! */ -describe.skip('utils', () => { +describe('utils', () => { /** * As the name suggests, `pairwise()` will call the given function with both * the current and the previous state. * - * *Note functions like `pairwise` and `scan` can be used with any callback, + * *Note: functions like `pairwise` and `scan` can be used with any callback, * so it can be used both in a `.derive()` step and in a `.react()`* */ it('pairwise', () => { @@ -33,25 +35,26 @@ describe.skip('utils', () => { /** * ** Your Turn ** * - * Now, use `pairwise()`, to subtract the previous value from the + * Now, use `pairwise()` to subtract the previous value from the * current. * * *Hint: check the overloads of pairwise if you're struggling with * `oldVal`.* */ - myCounter$.derive(__YOUR_TURN__).react(reactSpy); + myCounter$.derive(pairwise((newVal, oldVal) => (oldVal ? newVal - oldVal : newVal))).react(reactSpy); + // myCounter$.derive(pairwise((newVal, oldVal) => newVal - oldVal, 0)).react(reactSpy); // OR: alternatively. expect(reactSpy).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); - myCounter$.set(3); + myCounter$.set(5); expect(reactSpy).toHaveBeenCalledTimes(2); - expect(reactSpy).toHaveBeenCalledWith(2, expect.toBeFunction()); + expect(reactSpy).toHaveBeenCalledWith(4, expect.toBeFunction()); myCounter$.set(45); expect(reactSpy).toHaveBeenCalledTimes(3); - expect(reactSpy).toHaveBeenCalledWith(42, expect.toBeFunction()); + expect(reactSpy).toHaveBeenCalledWith(40, expect.toBeFunction()); }); /** @@ -70,21 +73,22 @@ describe.skip('utils', () => { /** * ** Your Turn ** * - * Now, use `scan()`, to add all the emitted values together + * Now, use `scan()` to subtract the previous value from the + * current. TODO: */ - myCounter$.derive(__YOUR_TURN__).react(reactSpy); + myCounter$.derive(scan((acc, val) => val + acc, 0)).react(reactSpy); expect(reactSpy).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); - myCounter$.set(3); + myCounter$.set(5); expect(reactSpy).toHaveBeenCalledTimes(2); - expect(reactSpy).toHaveBeenCalledWith(4, expect.toBeFunction()); + expect(reactSpy).toHaveBeenCalledWith(6, expect.toBeFunction()); myCounter$.set(45); expect(reactSpy).toHaveBeenCalledTimes(3); - expect(reactSpy).toHaveBeenCalledWith(49, expect.toBeFunction()); + expect(reactSpy).toHaveBeenCalledWith(51, expect.toBeFunction()); /** * *BONUS: Try using `scan()` (or `pairwise()`) directly in the @@ -92,9 +96,195 @@ describe.skip('utils', () => { */ }); - it.skip('pairwise - BONUS', () => { + // TODO: dit laat niet mooi het verschil zien. Hier lijkt het net alsof ze hetzelfde doen! + // En `scan` naar `val - acc` veranderen werkt niet. Geeft weird gedrag. + + it('scan2', () => { + // const myList$ = atom([]); + const myInt$ = atom(1); + const reactSpy = jest.fn(); + const f: (n1: number, n2: number) => number[] = (newVal, oldVal) => [newVal + oldVal]; + const d: number = 0; + let stopper: () => void; + + myInt$.derive(pairwise(f, d)); + // this is actually the same as: + myInt$.derive(v => pairwise(f, d)(v)); + // it just uses partial application. Pairwise itself is a function after all, which you apply to some value. + // This value then internally has a `previous state` property somewhere. + + // 1) this is one way or writing a derivable + react. + const myList$: Derivable = myInt$.derive(pairwise(f, d)); + stopper = myList$.react(reactSpy); + stopper(); + + // 2) the value of `myList$` is now directly passed to `reactSpy` without it being an extra variable. + // since we can get the value out of the `reactSpy`, this might be all we need. + stopper = myInt$.derive(pairwise(f, d)).react(reactSpy); + stopper(); + + // Let's try it out for real. + // The previous exercise made it seem like `pairwise` and `scan` do similar things. This is not true. + stopper = myInt$.derive(pairwise(f, d)).react(reactSpy); + + myInt$.set(2); + myInt$.set(3); + myInt$.set(4); + + expect(reactSpy).toHaveBeenCalledWith([3 + 4], expect.toBeFunction()); + stopper(); + + // Now let's to the same with scan. Already, the types don't match. + // The return type must be number (uncomment to see error). + // stopper = myInt$.derive(scan(f, d)).react(reactSpy); + // TODO: why? + + const f2: (n1: number, n2: number) => number = (newVal, oldVal) => newVal + oldVal; + stopper = myInt$.derive(scan(f2, d)).react(reactSpy); // starts at 4 + + myInt$.set(2); // then becomes 6 + myInt$.set(3); // then becomes 9 + myInt$.set(4); // lastly, becomes 13 + + expect(reactSpy).toHaveBeenCalledWith(13, expect.toBeFunction()); + stopper(); + + // ------- + // ------- + // ------- + // ------- + + // expect(reactSpy).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); + + // myCounter$.set(5); + + // expect(reactSpy).toHaveBeenCalledTimes(2); + // expect(reactSpy).toHaveBeenCalledWith(6, expect.toBeFunction()); + + // myCounter$.set(45); + + // expect(reactSpy).toHaveBeenCalledTimes(3); + // expect(reactSpy).toHaveBeenCalledWith(51, expect.toBeFunction()); + + /** + * *BONUS: Try using `scan()` (or `pairwise()`) directly in the + * `.react()` method.* + */ + }); + + it('`pairwise()` on normal arrays', () => { + // Functions like `pairwise()` and `scan()` work on normal lists too. They are often + // used in combination with `map()` and `filter()`. + const myList = [1, 2, 3, 5, 10]; + let myList2: number[]; + + /** + * ** Your Turn ** + * + * Use a `pairwise()` combined with a `map` on `myList` + * to subtract the previous value from the current. + * + * Hint: do not use a lambda function! + */ + + // let oldValue = init; - libs/sherlock-utils/src/lib/pairwise.ts:24 + // Closures?? TODO: + + // myList2 = myList.map(pairwise((newV, oldV) => newV - oldV, 0)); + myList2 = myList.map(__YOUR_TURN__); + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // However, we should be careful with this, as this does not always behave as intended. + myList2 = myList.map(v => __YOUR_TURN__(v)); // copy the same implementation here + expect(myList2).toMatchObject([1, 2, 3, 5, 10]); + + // Even if we are more clear about what we pass, this unintended behavior does not go away. + myList2 = myList.map((v, _, _2) => __YOUR_TURN__(v)); // copy the same implementation here + expect(myList2).toMatchObject([1, 2, 3, 5, 10]); + + // `pairwise()` keeps track of the previous value under the hood. Using a lambda of + // the form `v => pairwise(...)(v)` would create a new `pairwise` function every call, + // essentially resetting the previous value every call. And resetting the previous value + // to 0 causes the input to stay the same (after all: x - 0 = x). + // We can solve this by saving the `pairwise` in a variable and reusing it for every call. + + // let f = pairwise((newV, oldV) => newV - oldV, 0); + let f: (v: number) => number = __YOUR_TURN__; // copy the same implementation here + myList2 = myList.map(v => f(v)); + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // To get more insight in the `pairwise()` function, you can also just call it + // manually. Here, we show what happens under the hood. + + // f = pairwise((newV, oldV) => newV - oldV, 0); + f = pairwise(__YOUR_TURN__); // copy the same implementation here + + myList2 = []; + myList2[0] = f(myList[0]); // f is newly created with `init = 0`, so applies `1 - 0`. + myList2[1] = f(myList[1]); // f has saved `1` internally, so applies `2 - 1`. + myList2[2] = f(myList[2]); // f has saved `2` internally, so applies `3 - 2`. + myList2[3] = f(myList[3]); // f has saved `3` internally, so applies `5 - 3`. + myList2[4] = f(myList[4]); // f has saved `5` internally, so applies `10 - 5`. + + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // This also works for functions other than `map()`, such as `filter()`. + // Use `pairwise()` to filter out all values which produce `1` when subtracted + // with their previous value. + + // myList2 = myList.filter(pairwise((newV, oldV) => newV - oldV === 1, 0)); + myList2 = myList.filter(__YOUR_TURN__); + expect(myList2).toMatchObject([1, 2, 3]); + }); + + it('`scan()` on normal arrays', () => { + // As with `pairwise()` in the last test, `scan()` can be used with arrays too. + const myList = [1, 2, 3, 5, 10]; + let myList2: number[]; + + /** + * ** Your Turn ** + * + * Use a `scan()` combined with a `map` on `myList` + * to subtract the previous value from the current. + * + * Hint: do not use a lambda function! + * TODO: instead, make them write expectancies rather than the implementation. Is way nicer? + */ + + myList2 = myList.map(scan((acc, val) => val - acc, 0)); + // myList2 = myList.map(__YOUR_TURN__); + expect(myList2).toMatchObject([1, 1, 2, 3, 7]); + + // again, it is useful to consider what happens internally. + let f: (v: number) => number = scan((acc, val) => val - acc, 0); + // let f: (v: number) => number = pairwise(__YOUR_TURN__); // copy the same implementation here + + myList2 = []; + myList2[0] = f(myList[0]); // 1 :: f is newly created with `init = 0`, so applies `1 - 0 = 1`. + myList2[1] = f(myList[1]); // 1 :: f has saved the result `1` internally, so applies `2 - 1 = 1`. + myList2[2] = f(myList[2]); // 2 :: f has saved the result `1` internally, so applies `3 - 1 = 2`. + myList2[3] = f(myList[3]); // 3 :: f has saved the result `2` internally, so applies `5 - 2 = 3`. + myList2[4] = f(myList[4]); // 7 :: f has saved the result `3` internally, so applies `10 - 3 = 7`. + + expect(myList2).toMatchObject([1, 1, 2, 3, 7]); + + // This also works for functions other than `map()`, such as `filter()`. + // Use `scan()` to filter out all values which produce `1` when subtracted + // with the previous result. + // TODO: note (earlier) that `scan()` must return the same type as it gets as input. This is required + // as this returned value is also used for the accumulator value for the next call! + + // f = scan((acc, val) => val - acc, 0); + // myList2 = myList.filter(v => f(v) == 1); + f = scan(__YOUR_TURN__); + myList2 = myList.filter(__YOUR_TURN__); + expect(myList2).toMatchObject([1, 2]); // Only the numbers `1` and `2` from `myList` return `1`. + }); + + it('pairwise - BONUS', () => { const myCounter$ = atom(1); - let lastPairwiseResult = 0; + let reactSpy = jest.fn(); /** * ** Your Turn ** @@ -102,48 +292,50 @@ describe.skip('utils', () => { * * Now, use `pairwise()` directly in `.react()`. Implement the same * derivation as before: subtract the previous value from the current. - * - * Instead of returning the computed value, assign it - * `lastPairwiseResult` instead. This is so the implementation can be - * validated. */ - myCounter$.react(__YOUR_TURN__); - expect(lastPairwiseResult).toEqual(1); + reactSpy = jest.fn(pairwise((newV, oldV) => newV - oldV, 0)); + // reactSpy = jest.fn(__YOUR_TURN__); + myCounter$.react(reactSpy); + + expect(reactSpy).toHaveLastReturnedWith(1); myCounter$.set(3); - expect(lastPairwiseResult).toEqual(2); + expect(reactSpy).toHaveLastReturnedWith(2); myCounter$.set(45); - expect(lastPairwiseResult).toEqual(42); + expect(reactSpy).toHaveLastReturnedWith(42); // 45 - 3 (last value of `myCounter$`) }); - it.skip('scan - BONUS', () => { + it('scan - BONUS', () => { const myCounter$ = atom(1); - let lastScanResult = 0; + let reactSpy = jest.fn(); /** * ** Your Turn ** * ** BONUS ** * * Now, use `scan()` directly in `.react()`. Implement the same - * derivation as before: add all the emitted values together. - * - * In addition to returning the computed value, assign it - * `lastScanResult` instead. This is so the implementation can be - * validated. + * derivation as before: subtract all the emitted values. */ - myCounter$.react(__YOUR_TURN__); - expect(lastScanResult).toEqual(1); + reactSpy = jest.fn(scan((acc, val) => val - acc, 0)); + // reactSpy = jest.fn(__YOUR_TURN__); + // NOTE: acc is the last returned value, not the last value of `myCounter$`!! They are not the same! + myCounter$.react(reactSpy); + // TODO: can I also get all reactors within `myCounter$`? + + expect(reactSpy).toHaveLastReturnedWith(1); myCounter$.set(3); - expect(lastScanResult).toEqual(4); + + expect(reactSpy).toHaveLastReturnedWith(2); myCounter$.set(45); - expect(lastScanResult).toEqual(49); + + expect(reactSpy).toHaveLastReturnedWith(43); // 45 - 2 (last returned value) = 43 TODO: show this difference better! }); /** @@ -186,12 +378,37 @@ describe.skip('utils', () => { * expect? */ expect(myOneAtom$.get()).toEqual({ - regularProp: __YOUR_TURN__, - string: __YOUR_TURN__, - number: __YOUR_TURN__, + regularProp: 'new value', // it turns everything in a atom, sure + string: 'my string', // but why does changing the original normal string work?? TODO: does it listen to that actual struct (string) now?? + number: 1, sub: { - string: __YOUR_TURN__, + string: 'my new substring', }, }); }); + + it('lift', () => { + // Derivables can feel like a language build on top of Typescript. Sometimes + // you might want to use normal objects and functions and not have to rewrite + // your code. + // In other words, just like keywords like `atom(V)` lift the type `V` to the higher + // level of Derivables, the `lift(F)` keyword lifts a function `F` to the higher + // level of Derivables. + + // Example: after years of effort, I finally finished my super-long function: + const isEvenNumber = (v: number) => v % 2 == 0; + + // TODO: + // So rewriting this function to work with derivables would be a waste of time. + // YOUR TURN, use lift to reuse `isEvenNumber` on derivable level. + const isEvenDerivable = lift(isEvenNumber); + + expect(isEvenNumber(2)).toBe(true); + expect(isEvenNumber(13)).toBe(true); + expect(isEvenDerivable(atom(2))).toMatchObject(atom(true)); + expect(isEvenDerivable(atom(13))).toMatchObject(atom(false)); + }); + + // TODO: + it('peek', () => {}); }); diff --git a/tutorial/8 - advanced.test.ts b/tutorial/8 - advanced.test.ts index 48e9944..87a026a 100644 --- a/tutorial/8 - advanced.test.ts +++ b/tutorial/8 - advanced.test.ts @@ -1,4 +1,5 @@ import { atom, constant, Derivable, derive, SettableDerivable } from '@skunkteam/sherlock'; +import { template } from '@skunkteam/sherlock-utils'; import { Map as ImmutableMap } from 'immutable'; /** @@ -8,7 +9,7 @@ import { Map as ImmutableMap } from 'immutable'; */ export const __YOUR_TURN__ = {} as any; -describe.skip('advanced', () => { +describe('advanced', () => { /** * In the case a `Derivable` is required, but the value is immutable. * You can use a `constant()`. @@ -21,7 +22,7 @@ describe.skip('advanced', () => { * It can be valueable to know what a `constant()` is, though. * So try and remove the `cast`, see what happens! */ - const c = constant('value') as unknown as SettableDerivable; + const c = constant('value') as SettableDerivable; /** * ** Your Turn ** @@ -32,8 +33,17 @@ describe.skip('advanced', () => { // Remove this after taking your turn below. expect(false).toBe(true); // .toThrow() or .not.toThrow()? ↴ (2x) - expect(() => c.get()) /* __YOUR_TURN__ */; - expect(() => c.set('new value')) /* __YOUR_TURN__ */; + expect(() => c.get()).not.toThrow(); /* __YOUR_TURN__ */ + expect(() => c.set('new value')).toThrow() /* __YOUR_TURN__ */; + }); + + it('`templates`', () => { + // Staying in the theme of redefining normal Typescript code in our Derivable language + // we also have a special syntax to copy template literals to a Derivable. + const one = 1; + const myDerivable = template`I want to go to ${one} party`; + // expect(myDerivable.value).toBe(`I want to go to 1 party`); + expect(myDerivable.value).toBe(__YOUR_TURN__); /* __YOUR_TURN__ */ }); /** @@ -57,12 +67,13 @@ describe.skip('advanced', () => { * * Rewrite the `.get()`/`.set()` combos below using `.swap()`. */ - expect(false).toBe(true); // Remove this after taking your turn below. - myCounter$.set(plusOne(myCounter$.get())); + // expect(false).toBe(true); + + myCounter$.swap(plusOne); expect(myCounter$.get()).toEqual(1); - myCounter$.set(plusOne(myCounter$.get())); + myCounter$.swap(plusOne); expect(myCounter$.get()).toEqual(2); }); @@ -83,19 +94,19 @@ describe.skip('advanced', () => { * * Use the `.value` accessor to get the current value. */ - expect(__YOUR_TURN__).toEqual('foo'); + expect(myAtom$.value).toEqual('foo'); /** * ** Your Turn ** * * Now use the `.value` accessor to set a 'new value'. */ - myAtom$.value = __YOUR_TURN__; + myAtom$.value = 'new value'; expect(myAtom$.get()).toEqual('new value'); }); - /** + /** FIXME: SAME FOR ERRORS!! * If a `Derivable` is `unresolved`, `.get()` will normally throw. * `.value` will return `undefined` instead. */ @@ -105,7 +116,7 @@ describe.skip('advanced', () => { /** * ** Your Turn ** */ - expect(myAtom$.value).toEqual(__YOUR_TURN__); + expect(myAtom$.value).toEqual(undefined); }); /** @@ -128,11 +139,11 @@ describe.skip('advanced', () => { * We just created two `Derivable`s that are almost exactly the same. * But what happens when their source becomes `unresolved`? */ - expect(usingGet$.resolved).toEqual(__YOUR_TURN__); - expect(usingVal$.resolved).toEqual(__YOUR_TURN__); + expect(usingGet$.resolved).toEqual(true); + expect(usingVal$.resolved).toEqual(true); myAtom$.unset(); - expect(usingGet$.resolved).toEqual(__YOUR_TURN__); - expect(usingVal$.resolved).toEqual(__YOUR_TURN__); + expect(usingGet$.resolved).toEqual(false); + expect(usingVal$.resolved).toEqual(true); }); }); @@ -155,7 +166,7 @@ describe.skip('advanced', () => { * * Use the `.map()` method to create the expected output below */ - const mappedAtom$: Derivable = __YOUR_TURN__; + const mappedAtom$: Derivable = myAtom$.map(base => base.toString().repeat(base)); mappedAtom$.react(mapReactSpy); @@ -186,11 +197,11 @@ describe.skip('advanced', () => { * We changed`myRepeat$` to equal 3. * Do you expect both reactors to have fired? And with what? */ - expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(deriveReactSpy).toHaveBeenCalledTimes(2); + expect(deriveReactSpy).toHaveBeenLastCalledWith('hohoho', expect.toBeFunction()); - expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(mapReactSpy).toHaveBeenCalledTimes(2); + expect(mapReactSpy).toHaveBeenLastCalledWith('hohoho', expect.toBeFunction()); myString$.value = 'ha'; /** @@ -198,18 +209,18 @@ describe.skip('advanced', () => { * * And now that we have changed `myString$`? And when `myRepeat$` changed again? */ - expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(deriveReactSpy).toHaveBeenCalledTimes(3); + expect(deriveReactSpy).toHaveBeenLastCalledWith('hahaha', expect.toBeFunction()); - expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(mapReactSpy).toHaveBeenCalledTimes(2); + expect(mapReactSpy).toHaveBeenLastCalledWith('hohoho', expect.toBeFunction()); myRepeat$.value = 2; - expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(deriveReactSpy).toHaveBeenCalledTimes(4); + expect(deriveReactSpy).toHaveBeenLastCalledWith('haha', expect.toBeFunction()); - expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(mapReactSpy).toHaveBeenCalledTimes(3); + expect(mapReactSpy).toHaveBeenLastCalledWith('haha', expect.toBeFunction()); /** * As you can see, a change in `myString$` will not trigger an @@ -236,7 +247,7 @@ describe.skip('advanced', () => { // This first function is called when getting... n => -n, // ...and this second function is called when setting. - __YOUR_TURN__, + (newV, _) => -newV, ); // The original `atom` was set to 1, so we want the inverse to @@ -249,15 +260,37 @@ describe.skip('advanced', () => { expect(myAtom$.get()).toEqual(2); expect(myInverse$.get()).toEqual(-2); }); + + it('similar to `map()` on arrays', () => { + // if the similarity is not clear yet, here is a comparison between + // the normal `map()` on arrays and our `Derivable` `map()`. + // both get values out of a container (`Array` or `Derivable`), apply + // some function, and put it back in the container. + + const addOne: (v: number) => number = v => v + 1; + + const myList = [1, 2, 3]; + const myList2 = myList.map(addOne); + expect(myList2).toMatchObject([2, 3, 4]); + + const myDerivable = atom(1); + const myDerivable2 = myDerivable.map(addOne); + expect(myDerivable2.value).toBe(2); + + // you can combine them too + const myDerivable3 = atom([1, 2, 3]); + const myDerivable4 = myDerivable3.map(v => v.map(addOne)); + expect(myDerivable4.value).toMatchObject([2, 3, 4]); + }); }); /** * `.pluck()` is a special case of the `.map()` method. * If a collection of values, like an Object, Map, Array is the result of a - * `Derivable` one of those values can be plucked into a new `Derivable`. + * `Derivable`, one of those values can be plucked into a new `Derivable`. * This plucked `Derivable` can be settable, if the source supports it. * - * The way properties are plucked is pluggable, but by default both + * The way properties are plucked is pluggable, but by default both // TODO: no-one here knows what "pluggable" is. Or ImmutableJS. * `.get()` and `[]` are supported to support * basic Objects, Maps and Arrays. * @@ -290,7 +323,7 @@ describe.skip('advanced', () => { * * * Hint: you'll have to cast the result from `.pluck()`. */ - firstProp$ = __YOUR_TURN__; + firstProp$ = myMap$.pluck('firstProp') as SettableDerivable; }); /** @@ -306,18 +339,18 @@ describe.skip('advanced', () => { * What do you expect the plucked `Derivable` to look like? And what * happens when we `.set()` it? */ - expect(firstProp$.get()).toEqual(__YOUR_TURN__); + expect(firstProp$.get()).toEqual('firstValue'); // the plucked `Derivable` should be settable firstProp$.set('other value'); // is the `Derivable` value the same as was set? - expect(firstProp$.get()).toEqual(__YOUR_TURN__); + expect(firstProp$.get()).toEqual('other value'); // How many times was the spy called? Note the `skipFirst`.. - expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactPropSpy).toHaveBeenCalledTimes(1); // ...and what was the value? - expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(reactPropSpy).toHaveBeenLastCalledWith('other value', expect.toBeFunction()); }); /** @@ -339,7 +372,7 @@ describe.skip('advanced', () => { myMap$.swap(map => map.set('secondProp', 'new value')); // How many times was the spy called? Note the `skipFirst`. - expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactPropSpy).toHaveBeenCalledTimes(0); /** * ** Your Turn ** @@ -349,10 +382,10 @@ describe.skip('advanced', () => { myMap$.swap(map => map.set('firstProp', 'new value')); // How many times was the spy called? Note the `skipFirst`.. - expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactPropSpy).toHaveBeenCalledTimes(1); // ...and what was the value? - expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(reactPropSpy).toHaveBeenLastCalledWith('new value', expect.toBeFunction()); }); /** @@ -372,10 +405,10 @@ describe.skip('advanced', () => { * So what if we set `firstProp$`? Does this propagate to the source * `Derivable`? */ - firstProp$.set(__YOUR_TURN__); - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(myMap$.get().get('firstProp')).toEqual(__YOUR_TURN__); - expect(myMap$.get().get('secondProp')).toEqual(__YOUR_TURN__); + firstProp$.set('new value'); + expect(reactSpy).toHaveBeenCalledTimes(1); + expect(myMap$.get().get('firstProp')).toEqual('new value'); + expect(myMap$.get().get('secondProp')).toEqual('secondValue'); }); }); }); diff --git a/tutorial/9 - expert.test.ts b/tutorial/9 - expert.test.ts index 4f6b6fb..5b3e1c1 100644 --- a/tutorial/9 - expert.test.ts +++ b/tutorial/9 - expert.test.ts @@ -8,7 +8,7 @@ import { derivableCache } from '@skunkteam/sherlock-utils'; */ export const __YOUR_TURN__ = {} as any; -describe.skip('expert', () => { +describe('expert', () => { describe('`.autoCache()`', () => { /** * If a `.get()` is called on a `Derivable` all derivations will be @@ -32,7 +32,7 @@ describe.skip('expert', () => { */ // `.toHaveBeenCalled()` or `.not.toHaveBeenCalled()`? ↴ - expect(hasDerived) /* Your Turn */; + expect(hasDerived).not.toHaveBeenCalled() /* Your Turn */; mySecondDerivation$.get(); @@ -44,7 +44,7 @@ describe.skip('expert', () => { * first `Derivable` actually executed its derivation? */ // how many times? - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(3); }); /** @@ -68,7 +68,7 @@ describe.skip('expert', () => { * expectations pass. */ const myAtom$ = atom(true); - const myFirstDerivation$ = myAtom$.derive(firstHasDerived); + const myFirstDerivation$ = myAtom$.derive(firstHasDerived).autoCache(); const mySecondDerivation$ = myFirstDerivation$.derive(() => secondHasDerived(myFirstDerivation$.get() + myFirstDerivation$.get()), ); @@ -110,9 +110,9 @@ describe.skip('expert', () => { mySecondDerivation$.get(); // first after last .get() - expect(firstHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(firstHasDerived).toHaveBeenCalledTimes(2); // Ohhhh, it does not reset the value of last time. It just adds to it. But caches again apparently, because 2, not 4. Weetnie, is met `clear` niet beter? // second after last .get() - expect(secondHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(secondHasDerived).toHaveBeenCalledTimes(3); }); }); @@ -182,9 +182,10 @@ describe.skip('expert', () => { * But does that apply here? * How many times has the setup run, for the price `Derivable`. */ - expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(2); /** Can you explain this behavior? */ + // Yes: it creates a different Derivable every time, so it cannot use any caching! A similar issue to the `pairwise()` issue from tutorial 7. }); /** @@ -229,19 +230,19 @@ describe.skip('expert', () => { */ // How often was the reactor on price$ called? - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledTimes(0); // And how many times did the setup run? - expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(2); // What's the value of price$ now? - expect(price$.value).toEqual(__YOUR_TURN__); + expect(price$.value).toEqual(undefined); // And the value of googlPrice$? - expect(googlPrice$.value).toEqual(__YOUR_TURN__); + expect(googlPrice$.value).toEqual(1079.11); // Is googlPrice$ still even driving any reactors? - expect(googlPrice$.connected).toEqual(__YOUR_TURN__); + expect(googlPrice$.connected).toEqual(false); /** * Can you explain this behavior? @@ -273,7 +274,7 @@ describe.skip('expert', () => { * the created `Derivable` will not run the setup again and * everything should work as expected. * - * ** Your Turn ** + * ** Your Turn ** TODO: not in the SOLUTIONS!! * * *Hint: there is even an `unwrap` helper function for just * such an occasion, try it!* @@ -300,12 +301,14 @@ describe.skip('expert', () => { */ .map(companies => companies.map(company => stockPrice$(company))) // Then we get the prices from the created `Derivable`s in a separate step - .derive(price$s => price$s.map(price$ => price$.value)); + .derive(price$s => price$s.map(price$ => price$.value)); // TODO: yeah, you lost me. I need to dive into `.map()` again... + // So, in practice: the `.map()` maps the `companies` to a new Derivable, one where the list is changed to a new list of prices. + // We save this new Derivable in `prices$`, and then this new list is derived on such that, when it changes, we do something with it. prices$.react(reactor); // Because we use `.value` instead of `.get()` the reactor - // should emit immediately this time. + // should emit immediately this time. TODO: is there truly a difference? // But it should emit `undefined`. expect(reactSpy).toHaveBeenCalledExactlyOnceWith([undefined]); @@ -324,8 +327,8 @@ describe.skip('expert', () => { * * So the value was increased. What do you think happened now? */ - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reactSpy).toHaveBeenLastCalledWith([__YOUR_TURN__]); + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenLastCalledWith([1079.11]); /** * So that worked, now let's try and add another company to the @@ -343,8 +346,10 @@ describe.skip('expert', () => { * * We had a price for 'GOOGL', but not for 'APPL'... */ - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reactSpy).toHaveBeenCalledWith([__YOUR_TURN__, __YOUR_TURN__]); + expect(reactSpy).toHaveBeenCalledTimes(3); + expect(reactSpy).toHaveBeenCalledWith([undefined, undefined]); + // Because companies was the central component on which a derive lied... + // ...changing it made the whole thing reset?? Idk man. }); }); @@ -404,7 +409,7 @@ describe.skip('expert', () => { * * Has anything changed, by using the `derivableCache`? */ - expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(1); // Now let's resolve the price stockPrice$.mock.results[0].value.set(1079.11); @@ -417,10 +422,10 @@ describe.skip('expert', () => { * * What happens this time? Has the setup run again? */ - expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(1); // Ok, but did it update the HTML? - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(lastEmittedHTMLs()[0]).toContain(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(lastEmittedHTMLs()[0]).toContain('$ 1079.11'); // Last chance, what if we add a company companies$.swap(current => [...current, 'APPL']); @@ -433,12 +438,12 @@ describe.skip('expert', () => { * * But did it calculate 'GOOGL' again too? */ - expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenCalledTimes(3); // The first should be the generated HTML for 'GOOGL'. - expect(lastEmittedHTMLs()[0]).toContain(__YOUR_TURN__); + expect(lastEmittedHTMLs()[0]).toContain('$ 1079.11'); // The second should be the generated HTML for 'APPL'. - expect(lastEmittedHTMLs()[1]).toContain(__YOUR_TURN__); + expect(lastEmittedHTMLs()[1]).toContain('$ unknown'); }); }); });