Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port van @politie/sherlock tutorial #55

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"update": "nx migrate latest",
"workspace-generator": "nx workspace-generator",
"dep-graph": "nx dep-graph",
"help": "nx help"
"help": "nx help",
"tutorial": "jest tutorial/* -c tutorial/jest.config.ts"
},
"standard-version": {
"bumpFiles": [
Expand Down
140 changes: 140 additions & 0 deletions tutorial/1 - intro.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { atom } from '@skunkteam/sherlock';

/**
* ** Your Turn **
* If you see this variable, you should do something about it. :-)
*/
export const __YOUR_TURN__ = {} as any;

/**
* Welcome to the `@skunkteam/sherlock` tutorial.
*
* It is set up as a collection of specs, with the goal of getting all the specs
* to pass. The `expect()`s and basic setup are there, you just need to get it
* to work.
*
* All specs except the first one are set to `.skip`. Remove this to start on
* that part of the tutorial.
*
* Start the tutorial by running:
* `npm run tutorial`.
*
* To not manually re-enter the command, use:
* `npm run tutorial -- --watch`
* This will automatically rerun the tests when a file change has been detected.
*
* *Hint: most methods and functions are fairly well documented in jsDoc,
* which is easily accessed through TypeScript*
*/
describe('intro', () => {
it(`

--- Welcome to the tutorial! ---

Please look in \`./tutorial/1 - intro.ts\` to see what to do next.`, () => {
// At the start of the spec, there will be some setup.
let bool = false;

// Sometimes including an expectation, to show the current state.
expect(bool).toBeFalse();

/**
* If ** Your Turn ** is shown in a comment, there is work for you to do.
* This can also be indicated with the `__YOUR_TURN__` variable.
*
* It should be clear what to do here... */
bool = __YOUR_TURN__;
expect(bool).toBeTrue();
// We use expectations like this to verify the result.
});
});

/**
* Let's start with the `Derivable` basics.
*
* ** Your Turn **
* Remove the `.skip` so this part of the tutorial will run.
*/
describe.skip('the basics', () => {
/**
* The `Atom` is the basic building block of `@skunkteam/sherlock`.
* It holds a value which you can `get()` and `set()`.
*/
it('the `Atom`', () => {
// An `Atom` can be created with the `atom()` function. The parameter
// of this function is used as the initial value of the `Atom`.
const myValue$ = atom(1);
// Variables containing `Atom`s or any other `Derivable` are usually
// postfixed with a `$` to indicate this. Hence `myValue$`.

// The `.get()` method can be used to get the current value of
// the `Atom`.
expect(myValue$.get()).toEqual(1);

// ** Your Turn **
// Use the `.set(<newValue>)` method to change the value of the `Atom`.
expect(myValue$.get()).toEqual(2);
});

/**
* The `Atom` is a `Derivable`. This means it can be used to create a
* derived value. This derived value stays up to date with the original
* `Atom`.
*
* The easiest way to do this, is to call `.derive()` on another
* `Derivable`.
*
* Let's try this.
*/
it('the `Derivable`', () => {
const myValue$ = atom(1);
expect(myValue$.get()).toEqual(1);

/**
* ** Your Turn **
*
* We want to create a new `Derivable` that outputs the inverse (from a
* 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__);
expect(myInverse$.get()).toEqual(-1);
// So if we set `myValue$` to -2:
myValue$.set(-2);
// `myInverse$` will change accordingly.
expect(myInverse$.get()).toEqual(2);
});

/**
* Of course, `Derivable`s are not only meant to get, set and derive state.
* You can also listen to the changes.
*
* This is done with the `.react()` method.
* This method is given a function that is executed every time the value of
* the `Derivable` changes.
*/
it('reacting to `Derivable`s', () => {
const myCounter$ = atom(0);
let reacted = 0;

/**
* ** Your Turn **
*
* Now react to `myCounter$`. In every `react()`.
* Increase the `reacted` variable by one. */
myCounter$.react(() => __YOUR_TURN__);
expect(reacted).toEqual(1);
// `react()` will react immediately, more on that later.

/**
* And then we set the `Atom` a couple of times
* to make the `Derivable` react.
* */
for (let i = 0; i <= 100; i++) {
// Set the value of the `Atom`.
myCounter$.set(i);
}

expect(reacted).toEqual(101);
});
});
223 changes: 223 additions & 0 deletions tutorial/2 - deriving.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { atom, Derivable, derive } from '@skunkteam/sherlock';

/**
* ** Your Turn **
*
* If you see this variable, you should do something about it. :-)
*/
export const __YOUR_TURN__ = {} as any;

/**
* Any `Derivable` (including `Atom`s) can be used (and/or combined) to create
* a derived state. This derived state is in turn a `Derivable`.
*
* There are a couple of ways to do this.
*/
describe.skip('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
* new `Derivable`.
*
* In the derivation, other `Derivable`s can be used as well.
* If a `Derivable.get()` is called inside a derivation, the changes to that
* `Derivable` are also tracked and kept up to date.
*/
it('combining `Derivable`s', () => {
const repeat$ = atom(1);
const text$ = atom(`It won't be long`);

/**
* ** Your Turn **
*
* Let's create some lyrics by combining `text$` and `repeat$`.
* As you might have guessed, we want to repeat the text a couple of times.
*
* (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat should do fine)
*/

// We can combine txt with `repeat$.get()` here.
const lyric$ = text$.derive(txt => txt /* __YOUR_TURN__ */);

expect(lyric$.get()).toEqual(`It won't be long`);

text$.set(' yeah');
repeat$.set(3);
expect(lyric$.get()).toEqual(` yeah yeah yeah`);
});

/**
* Now that we have used `.get()` in a `.derive()`. You may wonder, can
* we skip the original `Derivable` and just call the function `derive()`?
*
* Of course you can!
*
* And you can use any `Derivable` you want, even if they all have the same
* `Atom` as a parent.
*/
it('the `derive()` function', () => {
const myCounter$ = atom(1);

/**
* ** Your Turn **
*
* Let's try creating a `Derivable` [FizzBuzz](https://en.wikipedia.org/wiki/Fizz_buzz).
* `fizzBuzz$` should combine `fizz$`, `buzz$` and `myCounter$` to
* produce the correct output.
*
* Multiple `Derivable`s can be combined to create a new one. To do
* this, just use `.get()` on (other) `Derivable`s in the `.derive()`
* step.
*
* This can be done both when `derive()` is used standalone or as a
* method on another `Derivable`.
*/

// Should return 'Fizz' when `myCounter$` is a multiple of 3 and '' otherwise.
const fizz$: Derivable<string> = myCounter$.derive(__YOUR_TURN__);

// Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise.
const buzz$: Derivable<string> = myCounter$.derive(__YOUR_TURN__);

const fizzBuzz$: Derivable<string | number> = derive(__YOUR_TURN__);

expect(fizz$.get()).toEqual('');
expect(buzz$.get()).toEqual('');
expect(fizzBuzz$.get()).toEqual(1);
for (let count = 1; count <= 100; count++) {
// Set the value of the `Atom`,
myCounter$.set(count);

// and check if the output changed accordingly.
checkFizzBuzz(count, fizzBuzz$.get());
}
});

function checkFizzBuzz(count: number, out: string | number) {
if ((count % 3) + (count % 5) === 0) {
// If `count` is a multiple of 3 AND 5, output 'FizzBuzz'.
expect(out).toEqual('FizzBuzz');
} else if (count % 3 === 0) {
// If `count` is a multiple of 3, output 'Fizz'.
expect(out).toEqual('Fizz');
} else if (count % 5 === 0) {
// If `count` is a multiple of 5, output 'Buzz'.
expect(out).toEqual('Buzz');
} else {
// Otherwise just output the `count` itself.
expect(out).toEqual(count);
}
}

/**
* The automatic tracking of `.get()` calls will also happen inside called
* `function`s.
*
* This can be really powerful, but also dangerous. One of the dangers is
* shown here.
*/
it('indirect derivations', () => {
const pastTweets = [] as string[];
const currentUser$ = atom('Barack');
const tweet$ = atom('First tweet');

function log(tweet: string) {
pastTweets.push(`${currentUser$.get()} - ${tweet}`);
}

tweet$.derive(log).react(txt => {
// Normally we would do something with the tweet here.
return txt;
});

// The first tweet should have automatically been added to the `pastTweets` array.
expect(pastTweets).toHaveLength(1);
expect(pastTweets[0]).toContain('Barack');
expect(pastTweets[0]).toContain('First tweet');

// Let's add a famous quote by Mr Barack:
tweet$.set('We need to reject any politics that targets people because of race or religion.');
// As expected this is automatically added to the log.
expect(pastTweets).toHaveLength(2);
expect(pastTweets[1]).toContain('Barack');
expect(pastTweets[1]).toContain('reject');

// But what if the user changes?
currentUser$.set('Donald');

/**
* ** Your Turn **
*
* Time to set your own expectations.
*/
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?

/**
* As you can see, this is something to look out for.
* Luckily there are ways to circumvent this. But more on that later.
*
* * Note that this behavior can also be really helpful if you know what
* you are doing *
*/
});

/**
* Every `Derivable` has a couple of convenience methods.
* These are methods that make common derivations a bit easier.
*
* These methods are: `.and()`, `.or()`, `.is()` and `.not()`.
*
* Their function is as you would expect from `boolean` operators in a
* JavaScript environment.
*
* The first three will take a `Derivable` or regular value as parameter.
* `.not()` does not need any input.
*
* `.is()` will resolve equality in the same way as `@skunkteam/sherlock`
* would do internally.
*
* More on the equality check in the 'inner workings' part. But know that
* the first check is [Object.is()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)
*/
it('convenience methods', () => {
const myCounter$ = atom(1);

/**
* ** Your Turn **
*
* The FizzBuzz example above can be rewritten using the convenience
* methods. This is not how you would normally write it, but it looks
* like a fun excercise.
*
* `fizz$` and `buzz$` can be completed with only `.is(...)`,
* `.and(...)` and `.or(...)`. Make sure the output of those `Derivable`s
* is either 'Fizz'/'Buzz' or ''.
*/
const fizz$ = myCounter$
.derive(count => count % 3)
.is(__YOUR_TURN__)
.and(__YOUR_TURN__)
.or(__YOUR_TURN__) as Derivable<string>;

const buzz$ = myCounter$
.derive(count => count % 5)
.is(__YOUR_TURN__)
.and(__YOUR_TURN__)
.or(__YOUR_TURN__) as Derivable<string>;

const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(__YOUR_TURN__);

for (let count = 1; count <= 100; count++) {
// Set the value of the `Atom`,
myCounter$.set(count);

// and check if the output changed accordingly.
checkFizzBuzz(count, fizzBuzz$.get());
}
});
});
Loading
Loading