Appearance
Testing Strategy
STALE — there is no "4-layer Vitest model" (2026-08-08)
Vitest-for-everything, src/test/setup.ts and .env.test supplying VITE_SUPABASE_* all belong to the retired Vite SPA (legacy/). The current model is FOUR runners split by workspace, and which one you get is decided by where the code lives, not by what layer it is:
| Runner | Where | What |
|---|---|---|
node --test | packages/* | Pure computation. @qrsetu/domain needs no DB and no bundler — that is deliberate. |
| jest-expo | apps/mobile | Hooks/services against the packages/data stub (not a mock of it) + component smoke: import the real component, never mock it or its icons. |
| vitest | apps/web | No jsdom; renderToStaticMarkup smoke tests. |
| Playwright | apps/mobile/e2e, apps/web/e2e | Web surface only. Plus pgTAP (test:db) and Deno (test:ef) for the backend. |
⚠ apps/mobile/e2e/visual.spec.ts does not exist (QRS-364) — the folder holds layout-invariants, parity-probe and theme-consistency only. apps/web/e2e/visual.spec.ts (M14) is the first real implementation of the Linux-only-screenshot pattern.
⚠ No automated layer covers Android or iOS native. For anything touching packages/tokens/**, apps/*/src/ui/**, theme plumbing or press/gesture handling, the native builds are the gate. Authoritative list: CLAUDE.md § Testing model.
Co-located tests/ per feature. Coverage thresholds: 70% lines / 80% functions.
The 4-layer Vitest model
| Layer | What | Key rule |
|---|---|---|
| L1 | Pure functions | No mocks needed; fast. |
| L2 | Hooks/services | Mock supabase + TanStack Query. Assert correct rpc/invoke calls + query keys. |
| L3 | Component smoke | Import the real component. First test = "mounts without error." |
| L4 | Page integration | Optional. Plus a compliance test asserting the feature never calls from(). |
The RiEdit3Line lesson: never mock the component under test or its icon imports — a smoke test that mocks the thing it's testing proves nothing. Import the real component and real icons.
Config essentials
vitest.config.tsis standalone — deliberately does NOT mergevite.config.js(keeps the dev-only Horizons/visual-editor plugins out of the runner). It duplicates onlyresolve.alias/extensions+@vitejs/plugin-react.- Setup:
src/test/setup.ts(@testing-library/jest-dom/vitest). globals: false→ the setup file must registerafterEach(cleanup)itself (otherwiserender()calls silently accumulate across tests in the same file)..env.test(committed, dummy values) suppliesVITE_SUPABASE_*so the client doesn't throw.
Commands
bash
npm test # full suite
npm run test:watch # watch mode
npm run test:coverage # coverage (70% lines / 80% functions)
npx vitest run path/to/file.test.tsx # single file
npm run test:ef # Deno edge-function testsOther layers
| Layer | Tool | Status |
|---|---|---|
| Edge Functions | Deno test:ef | ✅ live — every EF ships co-located tests/. |
| SQL / RPC | pgTAP test:db | 🎯 planned — RPC projections don't leak columns; RLS isolation; grants. |
| E2E | Playwright e2e:* | 🎯 planned — ephemeral local Supabase stack (schema snapshot, not migration replay); functions serve as a separate process; mutation guard; deploy-smoke on the real URL. |
What EF tests must cover
Success · validation failure · auth/authz failure · safe 500 (no leakage) · edge cases. Green before deploy. See Adding an Edge Function.
Testing model (operating-manual text)
Provenance — moved from CLAUDE.md on 2026-09-23 (QRS-1288)
This is the verbatim text of CLAUDE.md § "Testing model" as of commit 00c1eca, relocated here under the context-architecture programme. Sentences of the form "this said X until [date]" are corrections recorded at the time they were made; the live rule is the corrected one. Retired vocabulary inside those corrections names what was retired and is not a live claim.
Testing model
Co-located __tests__/ next to the code. Layers, by runner:
node --test(packages/*) — L1 pure computation.@qrsetu/domainis the home for anything derivable (recurrence expansion, DST, notification budgets) precisely so it needs no database and no bundler.- jest-expo (
apps/mobile) — L2 hooks/services (against thepackages/datastub, not a mock of it) · L3 component smoke: import the real component; first test = "mounts without error" — never mock the component or its icon imports (the RiEdit3Line lesson). Also used for build-config assertions (app/__tests__/native-splash|web-manifest|sentry-build-config). - Playwright (
apps/mobile/e2e, web surface only) — two layers, order deliberate:layout-invariants.spec.tsis deterministic DOM measurement (overflow, truncation, off-viewport controls, touch-target size; no baselines, no flake) and is the primary gate;visual.spec.tsDOES NOT EXIST (QRS-364 — see the note above; the intended second layer, screenshot baselines Linux-only by design, was never actually written for this app). Four viewport projects —phone-small(360×740, the tightest real case) ·phone-large·tablet·desktop. - Playwright (
apps/web/e2e, M14) — the SAME two-layer shape, actually built this time:layout-invariants.spec.ts+a11y.spec.ts(@axe-core/playwright, zero violations) as the primary gate,visual.spec.ts(Linux-only screenshot baselines) as the second layer. Runs against a static fixture rendered from the realSetuCardRenderer+ real compiled CSS — no live Supabase route is reachable in this environment. - pgTAP (
test:db) — RPC projections don't leak columns; RLS isolation; least-privilege grants ⚠ neither of those filenames exists —supabase/tests/database/holds ten files covering chat, feature grants, orders/payments, the collected-payment predicate and reconciliation. This named twoprofiles-era files until 2026-08-28. - Deno (
test:ef) — every EF ships co-locatedtests/(success · validation · auth/authz · safe 500s · edges). - Static parity (
check:parity) — the gate prints its own rule count; R1-R7 each encode a real incident (QRS-201/203/206/207), and R8 (ADR-0019's "one renderer" seam) and R9 (expo-camerahas exactly one importer, so the per-surface fallback is single-sourced and the module stays swappable) were added pre-emptively, before an incident forced either. ⚠ "Every rule is mutation-tested" WAS FALSE and this line asserted it until 2026-08-24. There is nocheck-parity.test.mjsanywhere in the repo — measured by enumerating every*.test.mjsand*.test.jsoutsidenode_modules. The parity gate, which encodes four real incidents (QRS-201/203/206/207), is itself unproven in both directions (QRS-872). Add a rule for every new parity incident — this layer only ever encodes yesterday's defects, which is an acceptable trade at 200 ms provided it keeps learning. - Static analysis (
npm run lint, type-aware, + CI'ssonarjob) — ~217 sonarjs rules pluseslint-plugin-security, and it is a defect layer, not a style layer: it has already found a catastrophically backtrackable regex in thecheck:sqlgate itself, a quadratic PII scrubber on the crash path, and an always-true comparison whose "obvious" fix would have caused data loss (QRS-247/253). The type-aware program is what makes it work at all — without it ~15 rules that are alreadyerrorsilently never run, which is how the gate stayed green over 92 findings. Nothing is sequenced off any more (QRS-247 closed 2026-07-30): every sonarjs rule is ON, at zero. If a rule ever has to be turned off it needs a measured COUNT and a REASON in that layer's comment — that format is what let a 158-finding backlog finish instead of drift.
Coverage target 70% lines / 80% functions (not yet wired as a hard gate). No automated layer covers Android or iOS native — the native builds are the gate for anything systemic; see "Cross-platform feature parity".