diff --git a/docs/contributing/testing.md b/docs/contributing/testing.md index fd0a7d434..21f3fc211 100644 --- a/docs/contributing/testing.md +++ b/docs/contributing/testing.md @@ -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 `. 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 diff --git a/src/__tests__/unit-suite-browser-install-guard.test.ts b/src/__tests__/unit-suite-browser-install-guard.test.ts index 8e80cc492..3771c14ea 100644 --- a/src/__tests__/unit-suite-browser-install-guard.test.ts +++ b/src/__tests__/unit-suite-browser-install-guard.test.ts @@ -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([]); }); });