docs(testing): describe the browser project and its helper

Say which claims belong in *.browser.test.tsx, how to run the project
alone, and what tests/browser/ provides. The browser install guard now
names the browser project beside the hero-geometry check, since both
launch the headless shell the CI slices install.
This commit is contained in:
Marc-André Bombeck committed 2026-10-23 13:16:56 +02:00
1 parent 392b2c47a9
commit 232c3db6ef
2 files changed
+48 -3

No files matched your search

+40 -1
View File
@@ -11,6 +11,7 @@ is about choosing and writing tests.
| Pure logic: statistics, time zones, cadence, parsers, unit conversion, formatting | `src/**/__tests__/*.test.ts` (unit) | `pnpm test` |
| Laws of that logic over random inputs | `*.property.test.ts` next to the unit tests | `pnpm test` |
| What a component renders for a given state | `src/**/__tests__/*.test.tsx` (unit) | `pnpm test` |
| How a component behaves when someone types, clicks or presses keys, and what it sends | `src/**/__tests__/*.browser.test.tsx` | `pnpm test` |
| A structural rule about the tree (a guard) | `src/__tests__/` with a stated floor | `pnpm test` |
| A route: auth, scope, validation, what it stores and what it answers | `tests/integration/routes/` with the kit | `pnpm test:integration` |
| Anything whose truth lives in Postgres: constraints, `where` semantics, transactions, locks, cascades, time zones in SQL, backups, jobs | `tests/integration/` | `pnpm test:integration` |
@@ -114,6 +115,43 @@ into a fresh account and reads it back there. A new setting column without a
table entry fails the file. A column with no write route says why in its
entry.
## Browser tests
Interaction belongs in the `browser` project: `*.browser.test.tsx` files run
in headless Chromium through Vitest's Playwright provider, as part of
`pnpm test`. Run them alone with `pnpm test --project browser`, or one file
with `pnpm exec vitest run --project browser <path>`. The Node projects never
collect these files and this project collects nothing else. CI installs the
browser it needs (`playwright install --only-shell chromium`); locally, run
that once.
Reach for it when the claim is about behaviour a markup test cannot see:
focus, keys, an open menu, a pending state, the body a form posts, the URL a
filter writes. A `renderToStaticMarkup` grep stays the tool for what a given
state renders, and bottom-sheet gestures stay in `e2e/`.
The helper is `tests/browser/`:
- `renderWithApp(ui, { locale, user })` mounts the app's own providers: a real
TanStack `QueryClient` (no retries) and `I18nProvider` with the real
catalog. The signed-in account is seeded where `useAuth` reads it, with
`authUser()` defaults under `user`. The app stylesheet and the default
theme class are loaded, so layout, visibility and colour tokens resolve.
- `stubFetch()` replaces `fetch` and nothing above it. Answer a route with
`.on(method, path, { status, data, error, meta })`, hold one with
`.deferred(...)` to assert a pending state, and read the parsed bodies from
`.calls(method, path)`. An unmatched request gets a 404 envelope and is
recorded. Do not mock `@tanstack/react-query` or `@/lib/api/api-fetch`.
- `next/navigation` is aliased to a shim over the page's real `location` and
`history`; `url.ts` sets the starting query, records pushes and replaces,
and simulates Back or an edited address.
- `pinDate(iso)` fakes `Date` only, so timers and animation frames still run.
Every test gets the real `fetch`, clock, address and an empty `localStorage`
back afterwards. Locate by role and accessible name, the way a person finds a
control; a `Label` adds a colon to its name, so match a labelled combobox with
`/^Type/` rather than `"Type"`.
## The integration database
Global setup boots one Postgres container and migrates one template database.
@@ -152,7 +190,8 @@ node scripts/test-durations.mjs unit unit.json
The nightly workflow (`.github/workflows/test-nightly.yml`) uploads a file
regenerated from CI timings, which is the better source.
The unit suite is three projects, decided per file from its source
The unit suite is three Node projects and the `browser` project above. The
Node ones are decided per file from its source
(`tests/unit-projects.ts`): `guards` and `unit` share a module graph within a
worker, `isolated` gives each file a fresh one. A file lands in `isolated` when
it mocks, spies, stubs env or globals, fakes timers or resets modules. If a
@@ -12,6 +12,12 @@
* same suite: every dependency bump failed on a launch error that said nothing
* about the bump. One workflow was fixed, one class was not.
*
* Since v1.46 the suite also carries the `browser` project
* (`*.browser.test.tsx`, see `tests/browser/`), which launches the same
* headless shell through Vitest's Playwright provider for every interaction
* test. A slice without the browser now fails on many files rather than one;
* the install it needs is the same, so this guard covers both.
*
* The sweep is textual and file-scoped, which is its limit: a workflow with two
* jobs, only one of which installs the browser, satisfies it. It catches the
* thing that actually went wrong — a whole workflow running the suite with no
@@ -58,8 +64,8 @@ describe("the browser the unit suite measures with", () => {
expect(
missing,
"a workflow runs the unit suite without installing the headless " +
"Chromium the hero-geometry check measures with — that check refuses " +
"to skip under CI, so the job fails on a launch error instead",
"Chromium the hero-geometry check and the browser project launch — " +
"neither skips under CI, so the job fails on a launch error instead",
).toEqual([]);
});
});