Shots in this scene
Build: The Tokens File and the Test Suite
In one line
Finish tokens.css with purpose and spring tokens and a JavaScript mirror, then add a Vitest suite (fake timers) and a Playwright suite (seeking, reduced motion, a token change, a snapshot) that check the toast, the drawer and reduced motion. The real output of both runs is below.
Before you start
You will learn
- Add purpose and spring tokens to
tokens.cssand the mirrortokens.js - Add
tests/motion.test.js(Vitest) andtests/launch.spec.js(Playwright) and the two configs - Run both suites, accept the first snapshot, and break each check on purpose to see it fail
You write code in
In your lumen-motion project (npm run dev, npm test, npm run test:e2e) and in the editor on this page: add five files, change two
Files you touch
- src/styles/tokens.css
- src/js/motion/tokens.js
- vitest.config.js
- playwright.config.js
- package.json
- tests/motion.test.js
- tests/launch.spec.js
Think of it like this
The shopkeeper who writes the opening checklist on the shop door: lights on, till counted, sign turned. Anyone can run it, on any morning, and a missed step shows. Tests are the opening checklist for the site's motion.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- test suite
- A set of automatic checks you run with one command.
- baseline
- The saved picture a snapshot test compares against. The first run writes it.
- webServer
- A Playwright setting that starts your dev server before the tests and stops it after.
- pinned version
- An exact version written in
package.json(5.0.5, not^5), so every machine installs the same code.
Step by step
01Install the test tools
Run the install of Topic 18.4 in your project. Then add the two scripts to package.json (the file below has them): test runs vitest run, test:e2e runs playwright test.
02Finish the tokens
Replace tokens.css with the file below (every earlier token stays, the new block is marked Phase 18) and add src/js/motion/tokens.js. Your index.html already links tokens.css, so no other file changes.
03Add the configs and the tests
Create vitest.config.js (happy-dom, tests under tests/), playwright.config.js (the dev server and baseURL), tests/motion.test.js and tests/launch.spec.js.
04Run the logic tests
npm test runs the eight Vitest tests.
05Run the browser tests, twice
The first run has no baseline for the snapshot, so that one test fails and writes the picture. Open tests/launch.spec.js-snapshots/toast-win32.png: it is a strip of the #toasts element (1280 px wide and 41 px tall in our run) with the dark "Lock added to cart" toast in the middle and a little of the page behind it. Commit it and run again.
Try it yourself
- 1
Slow the toast with a token
Press --dur-base: 440ms and then Show a toast. The toast's
WAAPItrack is 440 ms long. Press Reset: the next toast is 220 ms again. The Playwright test "a token change reaches the toast" does the same with 900 ms. - 2
Run one test
In your project run
npx vitest run -t "fourth toast"to run a single test by name, andnpx playwright test -g "reduced motion"to run the two reduced-motion tests only.
Code & diagrams
`tokens.css` and `tokens.js` inlined (the preview cannot import project files), four of the unit checks, and a toast whose duration and curve come from the tokens. **--dur-base: 440ms** changes the next toast. Vitest and Playwright cannot run here; their real output is above.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
ok the mirror reads the CSS: --dur-base is 220 ms
ok a purpose token resolves through var()
ok a missing token throws, it does not return NaN
ok a spring token becomes spring() options: damping 33.92
toast lasts 220 ms
after --dur-base: 440ms, the next toast lasts 440 msYour project files
Your lumen-motion project after this topic. Click a file to see it in full.
The whole file after this topic. If yours differs, you can copy this one and carry on.
/* tokens.css: Lumen's motion tokens. One place for durations and easings, so every component speaks the same language.
Phase 2 started this file and Phase 18 finishes it. Never rename a token: components depend on the names. */
:root {
/* durations (the Nielsen Norman Group ladder, Topic 2.4) */
--dur-fast: 120ms; /* feedback: a press, a hover colour */
--dur-base: 220ms; /* most things: toasts, panels, dialogs entering */
--dur-slow: 400ms; /* large movement: a drawer across the screen */
/* easings (Topic 5.3 explains the choices) */
--ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* things arriving */
--ease-in: cubic-bezier(0.55, 0, 1, 0.45); /* things leaving */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* things moving between two places on screen */
/* ── Phase 5: choreography ── */
--stagger-step: 60ms; /* gap between neighbours in a reveal; main.js caps how many steps count */
/* ── end Phase 5 ── */
/* ── Phase 18: purpose names (components ask for the purpose, not for a number) ── */
--motion-enter-duration: var(--dur-base); /* something appears */
--motion-enter-ease: var(--ease-out);
--motion-exit-duration: var(--dur-fast); /* something leaves: quicker than it came */
--motion-exit-ease: var(--ease-in);
--motion-emphasis-duration: var(--dur-slow); /* a big, rare move: the drawer, a page change */
--motion-emphasis-ease: var(--ease-in-out);
/* springs: "stiffness damping-ratio" (mass is always 1). CSS cannot run a spring: tokens.js reads these for spring.js.
The shape follows Material 3's spring tokens (stiffness plus a damping ratio, spatial and effects); the numbers are Lumen's own. */
--spring-spatial-fast: 700 0.9; /* small things that move: a switch, a chip */
--spring-spatial: 380 0.87; /* the drawer (Phase 9): a calm, tiny overshoot */
--spring-spatial-slow: 200 0.9; /* large things that move */
--spring-effects: 1600 1; /* opacity and colour: never overshoot (a ratio of 1 does not) */
/* ── end Phase 18 ── */
}
Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Write a token without its unit
In tokens.css change --dur-base: 220ms; to --dur-base: 220; and run both suites.
Break #2
Lose the reduced-motion rule
In components.css change the toast's @media (prefers-reduced-motion: reduce) to no-preference (the rule under "reduced motion: a fade instead of a slide").
Myth vs fact
Myth
Visual snapshots make animation tests unnecessary.
Fact
A picture of the final state says nothing about the animation, reduced motion or cleanup. Use snapshots for how it looks, and seeks and counts for how it behaves.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The unit suite stubs nothing about animation: it relies on
toast.jsremoving the toast at once whengetAnimations()is empty, as it is in happy-dom. If that fallback were removed, the test would need a stub forgetAnimations. - ▸
playwright.config.jshasreuseExistingServer: true, so a dev server you already started is used. In CI, set it tofalseso a stale server never answers.
Remember this
- 1
Why the site needs this now: this closes Kofi's chapter. The contractors will edit the toast and the drawer, Lena wants a failing check instead of a complaint, and Tomás wants to know before customers do. Nothing yet tells the team when motion changes by accident.
- 2
What it adds.
tokens.csskeeps every earlier token and gains the purpose and spring tokens;src/js/motion/tokens.jsexportstoken(),ms()andspringOf().index.htmlalready linkstokens.css(Phase 2), so nothing else loads it. The two configs are new andpackage.jsongains the scripts and pinned dev dependencies (it also lists the packages of Phases 14 to 16). One honest gap: the purpose tokens are in place but Lumen's components still ask for primitives, such asvar(--dur-base, 220ms). Moving each rule over to--motion-enter-*is the first pull request after this phase; until then a change to--dur-basestill reaches the toast, but a change to--motion-enter-durationdoes not. - 3
The Vitest suite (
tests/motion.test.js, 8 tests, happy-dom, fake timers) checks the tokens (the mirror reads the CSS, a wrong token throws, everyvar()names a token that exists), the toast (there at 2,599 ms, gone at 2,600 ms; a fourth pushes out the oldest), the drawer (starts closed, opens with the spring, ends attranslateX(0px), Escape closes it) and reduced motion (the device setting is followed, the visitor's choice beats it). It checks our logic; it cannot see an animation (Topic 18.4). - 4
The Playwright suite (
tests/launch.spec.js, 6 tests, Chromium, with its own dev server) seeks the toast's real animation to 0, 110 and 220 ms; opens the drawer and polls until the spring settles (a spring is JavaScript, so there is nothing to seek); underreducedMotion: "reduce"checks the toast uses the fade and the drawer needs no spring; changes--dur-baseto 900 ms and checks the toast's real duration; and takes a snapshot of the toast. - 5
What we measured.
npm test: 8 passed.npm run test:e2e: the first run passed 5 and failed the snapshot (A snapshot doesn't exist ... writing actual.); the second passed all 6. Then we broke things on purpose (see Break it) and the right check failed. - 6
What the launch site can do now that it could not before: be edited by someone who was not in the room, with a safety net for the toast, the drawer and reduced motion; read its motion numbers from one place in CSS and JavaScript; and fail a check, not a customer. What is still missing: the interview rounds, the jank hunt and the launch audit (Phase 19). The suites skip the gallery, the page transition and the scroll story; we ran them in Chromium on Windows only (snapshot
toast-win32.png), and on no real phone.
Explain it without notes
Why does the browser suite poll for the drawer instead of seeking it?
Why does the toast's timing test advance 2,599 ms and then 1 ms?
Practice
Add a test to tests/motion.test.js that checks every --motion-*-duration token resolves to a number with ms().
Say why the Playwright test that changes --dur-base still works, although the toast does not use the purpose tokens.
Done when you can
tokens.css has every earlier token plus the purpose and spring tokens, and tokens.js reads them
npm testpasses 8 tests andnpm run test:e2epasses 6, after the first snapshot is committedYou broke a token, a media query and a timer on purpose and saw the right check fail
You can say which problems the unit suite can catch and which only the browser suite can