Reference

Testing

Récit is tested on four levels. Each one answers a different question, and a change is tested at the lowest level that can see it.

LevelWhat it checksWhereCommand
Unit testsLogic without a DOM: parsers, schemas, config, SSR*.test.ts, *.spec.ts, __tests__/ next to the codepnpm test:related <files…>
Stories and playA component rendered in Chromium: states, interactions, axeX.stories.tsx next to X.tsxpnpm test:stories [files…]
Behavioural end-to-endA built site or deck, navigated as a reader doese2e/*-behaviour.spec.ts, e2e/site-*.spec.tspnpm test:e2e:host <spec>
ScreenshotsWhat every page and every story looks like, light and darke2e/__screenshots__/…-linux.png, in the Docker containerpnpm test:e2e:linux [specs]

Everything at once, as CI runs it: pnpm verify (lint, build, typecheck, unit tests and stories), then pnpm test:e2e:linux.

Unit tests

Vitest runs one project per package (vitest.config.ts at the root lists them) plus scripts and the storybook project. A unit test stays for what a story cannot show: a function without a DOM, a schema, a server render through renderToString. A test that needs a mounted component reuses its story with composeStories instead of building another harness.

TerminalBash
pnpm test:related packages/core/src/schema/assemble.ts # the tests that import these files pnpm exec vitest run packages/core/src/__tests__/source-conversion.spec.ts pnpm test # every project, stories included pnpm test:coverage # with the coverage thresholds

The coverage thresholds sit in the root vitest.config.ts, one entry per package, at the level last measured (rounded down) with autoUpdate: coverage can only go up.

Stories

Every component of the framework (packages/site), of the slides (packages/components), of the deck, of the motion blocks, of the site kit and of the sites has a story next to it, and every interactive component a play function. node scripts/check-stories.js counts what is missing and fails pnpm check on the first finding. The workshop lives in sites/storybook; the title of a story picks its decorators:

TitleSourceWhat it gets
Site/UI/<Component>packages/site/src/lib/uiMantine, the color mode
Site/Components/…packages/site/src/lib/components, pluginsplus a site: router, query client, site context, slots, head
Slides/<Component>packages/components/srca slide frame
Deck/<Component>packages/formats/deck/srca compiled deck
Motion/Blocks/<Block>packages/formats/motion-blocks/srca still film frame and a motion theme
Kit/<Component>packages/tools/site-kit/src/componentsthe site, as for Site/

Writing a story

Reactpackages/site/src/lib/components/Banner.stories.tsx
import type { Meta, StoryObj } from "@storybook/react-vite"; import { expect, fn, userEvent, within } from "storybook/test"; import { Banner } from "./Banner.js"; /** One sentence: what the component is, what it does. */ const meta = { title: "Site/Components/Banner", component: Banner, args: { message: "Lorem ipsum dolor sit amet.", onDismiss: fn() }, parameters: { site: { path: "/dolor" } }, } satisfies Meta<typeof Banner>; export default meta; type Story = StoryObj<typeof meta>; export const Default: Story = { play: async ({ args, canvasElement }) => { const canvas = within(canvasElement); await expect(canvas.getByRole("status")).toHaveTextContent("Lorem ipsum"); await userEvent.click(canvas.getByRole("button", { name: "Dismiss" })); await expect(args.onDismiss).toHaveBeenCalledOnce(); }, }; export const Hidden: Story = { args: { message: undefined } };
  • One file per component, one export per state, args complete on meta.
  • Text in Lorem Ipsum, titles and comments in English.
  • No class and no style in a story: lay variants out with Mantine's Stack or Group, or with parameters.grid ("stack" or "row").
  • A site story sets its route with parameters.site.path and overrides the fixture config with parameters.site.options.

Writing a play function

A play function asserts what a reader sees and does, by role and name:

  • getByRole("button", { name: /copy/i }), never a class, a CSS selector or a data-testid (allowed only on an element without role and text, listed in scripts/check-stories.js).
  • After an interaction, findBy* or waitFor: the built Storybook renders without act().
  • A portal (dialog, menu, popover) is outside the canvas: within(canvasElement.ownerDocument.body).
  • A callback is a fn() of args, asserted with toHaveBeenCalledWith.
  • No snapshot of HTML: the screenshot is the picture, the play is the behaviour.
  • Name a step by the behaviour: opens on click and closes on Escape.

Axe runs on every story in error. A violation is fixed, or marked parameters: { a11y: { test: "todo" } } with the reason on the line above.

Running the stories

TerminalBash
pnpm test:stories # every story, in Chromium pnpm test:stories packages/site/src/lib/ui/Button.stories.tsx # one file BROWSER=none pnpm -F recit-storybook dev # the workshop, to browse pnpm -F recit-storybook build # the build the screenshots read

A missing Chromium stops the run with the command that installs it: pnpm exec playwright install chromium.

End-to-end tests

The e2e suite serves the built sites, decks and Storybook on the ports of e2e/ports.ts: build before running it. Pass the specs you need, and only their projects are built and served.

TerminalBash
pnpm test:e2e:host e2e/site-behaviour.spec.ts # behaviour on this machine, no screenshot pnpm test:e2e:linux e2e/site-docs.spec.ts # in Docker, screenshots compared, like CI pnpm test:e2e:update:linux e2e/site-docs.spec.ts # write the -linux.png references

The behavioural specs (site-behaviour, deck-behaviour, deck-features, accessibility) navigate, type and click; they run anywhere. The screenshots are compared only in the Linux container: its -linux.png files are the reference, and only pnpm test:e2e:update:linux writes them.

The stories are screenshotted in light and dark by one spec per surface, so a change to one surface replays only its own:

SpecStories
e2e/storybook-site-ui.spec.tsSite/UI/
e2e/storybook-site.spec.tsSite/Components/, Site/Navigation/, Site/Plugins/
e2e/storybook-slides.spec.tsSlides/
e2e/storybook-deck.spec.tsDeck/
e2e/storybook-motion.spec.tsMotion/
e2e/storybook-kit.spec.tsKit/, Sites/

A new story gets its two screenshots without touching a spec; its references are written in the container at the next gate.

After a container run, node scripts/screenshot-gallery.js builds test-results/gallery/index.html: one row per screenshot that changed, with the reference, the new image and their difference, a filter by spec, and an "accepted" button. Add --serve <port> to open it from another machine. A change is accepted only when the page changed on purpose; the references are then regenerated with pnpm test:e2e:update:linux <specs>.