Testing¶
Last reviewed: 2026-09-27
This page explains what each test layer proves and where the remaining test-documentation risk sits.
Test Layers¶
| Layer | Command | Proves |
|---|---|---|
| Laravel PHPUnit | vendor/bin/phpunit --testsuite=Laravel,LaravelMigrated --colors=always |
Backend routes, controllers, services, tenant boundaries, auth, money paths, and migrations. |
| PHPStan / Larastan | vendor/bin/phpstan analyse --no-progress --memory-limit=512M --error-format=github |
Static-analysis regressions beyond the configured baseline. |
| React type check | cd react-frontend && npx tsc --noEmit |
TypeScript correctness for the primary frontend. |
| React build | cd react-frontend && npm run build |
Production build viability. |
| Vitest (local) | cd react-frontend && npm test |
Component, hook, and frontend behavior tests. |
| Vitest (the CI gate) | cd react-frontend && node scripts/run-vitest-shard.mjs --shard 1/8 |
The whole suite minus the quarantine list, split across eight shards. This — not npm test — is what gates a release. See "Frontend quarantine" below. |
| Playwright E2E | npm run test:e2e |
Browser behavior against the React frontend and Laravel API. |
| Events enterprise E2E | npm run test:events:e2e:enterprise |
The destructive five-step create, publication, registration, waitlist, check-in, cancellation, notification, and cleanup lifecycle against an isolated fixture environment. |
Accessible frontend (web-uk) |
npm --prefix web-uk run brand:check, npm --prefix web-uk run lint, npm --prefix web-uk test, npm --prefix web-uk run build:css |
GOV.UK branding prohibitions, lint, 80 Jest suites, and Sass compilation for the HTML-first accessible frontend. The three *:accessible-frontend* commands this row named until 2026-08-14 were removed with the Blade accessible frontend and no longer exist. |
| Android native release | cd mobile && npm run verify:release && npm run type-check && npm test -- --runInBand |
OTA/release policy, native configuration contracts, TypeScript, and mobile behavior before Expo prebuild. |
| Documentation | npm run check:docs, npm run check:version, npx markdownlint-cli2, Redocly, strict MkDocs build |
Public-doc hygiene, version/changelog integrity, Markdown structure, OpenAPI validity, and publishable site navigation. |
Frontend quarantine — what a green pipeline proves¶
The eight-shard React Full Suite job has been blocking since 2026-07-28. It
skips the suites listed in react-frontend/src/test/failing-suites.baseline.json,
so a green pipeline currently proves 1,228 of 1,283 suites. Before that job
existed the blocking Vitest steps covered roughly 150 files — about 88% of the
suite could break with a green build, which is why frontend breakage was only ever
discovered in large batches.
The list is a fix-and-remove queue, not a set of exemptions:
- It may only shrink.
react-frontend/scripts/check-quarantine-budget.mjscarries aBASELINEconstant that must be lowered in the same commit as any removal. It runs in theReact Build & Testsjob rather than in the shard job, so the list cannot be grown to turn a red shard green. - A listed path that no longer exists fails the runner, rather than rotting there because a rename quietly excluded it forever.
- A non-gating visibility step runs the quarantined suites on shard 1, so a suite that gets fixed elsewhere is noticed instead of sitting there unrun.
- Verify a fix with
--retry=0. The shard runner passes--retry=1, so a suite can pass by retry rescue; removing one on that evidence puts a flaky suite into the gate. - Record why each entry fails. Entries sharing a root cause get fixed as a group; lumping unrelated failures together is how the queue becomes an exemption list.
Local concurrency differs from CI on purpose¶
react-frontend/vitest.config.ts derives its fork count from
os.availableParallelism(). A developer machine runs test files concurrently
(half its logical cores); CI stays on the original serial settings
(maxForks: 2, fileParallelism: false) because every ci.yml job runs on a
4-vCPU ubuntu-latest runner and the eight-shard gate was stabilised there.
Two consequences worth knowing:
- A suite that depends on file execution order or on shared module state can
pass in one mode and fail in the other. Reproduce serially with
NEXUS_VITEST_MAX_FORKS=1before concluding a test is flaky. - Do not pin
--maxWorkersor--no-file-parallelismin scripts; those flags override the config and reimpose serial execution everywhere.
See LOCAL-PERFORMANCE.md for the measured figures and for the container file-I/O limit that dominates PHP-side timings.
Two ways a test passes locally and fails in CI¶
Both have cost real debugging time, and both are properties of the environment rather than of the test:
$_SERVERis not populated by Laravel's test HTTP kernel. PHP-FPM always setsREQUEST_METHOD; the test kernel dispatches aRequestobject without writing the superglobal, so code reading it directly returns 500 under test only. Read from the request object instead.- Config sourced from a developer
.envis absent in CI. A test that needs a signing key, an API credential, or a feature flag must set it insetUp()rather than inherit it. Reproduce a suspected case by clearing the variable on the command line (FOO= vendor/bin/phpunit <path>) before concluding the test is sound.
E2E Status¶
The Playwright suite combines broad smoke coverage with real journey assertions. CI does not treat a configured zero-test run as green, but some lower-priority specs still contain defensive presence checks; those checks are not substitutes for outcome assertions on release-critical flows.
Before treating E2E as release evidence, prefer tests that assert real outcomes:
- account state changed;
- balances or ledgers changed correctly;
- a message, notification, listing, event, or review persists after reload;
- route protection works for signed-out and cross-tenant users;
- validation errors are visible and keyboard reachable.
The Events enterprise journey is deliberately excluded from the broad Chromium,
Firefox, and mobile projects. Run it only through
npm run test:events:e2e:enterprise; it refuses Project NEXUS production hosts
and requires an explicit opt-in for any other non-loopback fixture target. CI
runs it against a disposable database with CI-local actors, not repository or
environment secrets.
Help Centre guides¶
The Help Centre (/help, and /broker/help inside the Broker Panel) is a set of built-in
guides for three audiences: members, brokers and coordinators, and community admins. It is
shown only for the features a community has switched on. The phone app shows the members'
guide too (mobile/lib/help/), fetching the same text from the website.
Where things live:
| What | Where |
|---|---|
| Structure: sections, articles, order, icons, feature switches, the page each article is about | react-frontend/src/pages/help/guides/data/{members,brokers,admins}.registry.json |
| Text, in every language | react-frontend/public/locales/<lang>/help_{members,brokers,admins}.json; page chrome in help_centre.json |
Body format (paragraphs, ##, -, 1., >, **bold**, internal links — never HTML) |
react-frontend/src/pages/help/guides/HelpBody.tsx |
| The app's copy of the members' structure | mobile/lib/help/membersRegistry.json, kept identical by mobile/lib/help/guides.test.ts |
| Which code each article describes, and when it was last checked | react-frontend/src/pages/help/guides/data/sources.json |
Three checks guard the guides:
- Integrity —
src/pages/help/guides/guides.integrity.test.ts. Every registered article has text in every language and nothing is unregistered; feature switches exist; links stay inside the app and are hidden whenever the page they point at is switched off; translations keep the English links, lists and steps. Runs in the React suite. - Labels —
src/pages/help/guides/guides.labels.test.ts, rules inlabelCheck.ts. Every**bold**span must be words the app really shows in that language: it is looked up in the locale's other UI translation files, ignoring spacing and a trailing colon or ellipsis, otherwise exactly (capitals count). Placeholder texts ({{count}} left today) match filled-in labels; a bolded sentence ending in a full stop is emphasis and is skipped; a bold guide title is a cross-reference and passes. Genuine exceptions go indata/label-allowlist.jsonwith a reason. Known failures are indata/label-baseline.json, which may only shrink: a new unmatched label fails, and so does a baseline line that is fixed. A pass proves the words exist in the interface, not that they are on the page the article describes. Labels that only the server or the phone app produces are not in these files and show as failures until allowlisted. Regenerate the baseline, after checking the change is an improvement, fromreact-frontend/withnode scripts/write-help-label-baseline.mjs(--report <file>also writes every failure for translators). - Staleness —
npm run check:help-staleness(scripts/check-help-staleness.mjs, tests innpm run test:help-staleness). Eachsources.jsonentry maps an article (<audience>/<section>/<article>) to the files it depends on and the full commit sha its text was last checked at. The check lists articles whose sources changed since that commit, articles with no entry, sources that no longer exist, and entries that are malformed or point at an article that no longer exists..github/help-staleness-baseline.jsonholds the drift accepted today; only new drift, or a baseline line that no longer applies, fails. A verified commit that is not in the clone is reported as UNAVAILABLE (exit 2), never as a pass — CI needs a full history checkout (fetch-depth: 0).
The staleness workflow: when an article has been read against its sources and is right, run
node scripts/check-help-staleness.mjs --mark-verified <audience>/<section>/<article> (sets
verified to HEAD) and commit sources.json with the text change. When a change to a page makes
an article stale, update the article, then mark it verified. Accept existing drift only with
--write-baseline, and only after looking at what it accepts.
Generated Reports¶
Playwright reports under e2e/reports/, coverage reports, raw PHPStan output, and temporary static-analysis dumps are generated artifacts. Do not commit them as maintained docs. If a one-off report must be retained locally, put it under .local-docs-archive/.
Test Documentation Rules¶
- Keep test instructions near the test harness they describe (
tests/README.md,e2e/README.md,mobile/README.md). - Put platform-wide testing policy here.
- Update this page when a test layer changes meaningfully, especially if a green check no longer proves what this page says it proves.