Six suites pinned the old rule as the rule. Each inversion, and why it is one: `breadth.test.ts` — the case table asserted a refusal reason per set; it asserts a tier and a `physiological` flag now. Every row in it except the empty set used to be a refusal, and each is a number somebody gets to see. Adds the tier boundaries (one pillar, two areas, physiological-absent), a case proving the same pillars produce the same number at `partial` and at `full`, and a block whose only job is the counter-case: the empty verdict still carries a reason and no tier, and a composite whose selected pillars all came back short still refuses with `no_usable_data`. A rule that has stopped refusing anything is a deleted rule, not a graded one. `composite.test.ts` — "requires three domains and one measured physiological domain" asserted the refusal that no longer happens; it now asserts the score and the basis block on the same fixture. The coverage case keeps its fixture and its arithmetic and flips its outcome: two of three used to be a refusal and is a scored account with a real coverage fraction. Adds the one-area case and the zero-pillar counter-case, and the `SCORE_VERSION` floor moves 2 -> 3 because widening who gets a score is exactly the kind of change that must not ship without a version bump. `record.test.ts` — "records nothing when the composite did not resolve" used a two-pillar fixture to prove a below-the-floor day stored nothing. That day has a number now and the table has to keep it, or an account that starts narrow and widens later has a hole where its early days were. The assertion is now that a one-pillar day builds a draft AND reaches the database; the absence case survives on the fixture that genuinely produces no score. `composition-source.test.ts` — three recipes asserted as refused at read time. Two of them score. What the block proves is unchanged — the reader publishes the shared rule's verdict on the reader's own resolved set, so a second copy of the rule would still surface here — but the agreement is over a tier rather than over a refusal. `route.test.ts` (score config) — the two too-narrow refusals become acceptances, because a write that refuses what the read is willing to serve is the two layers disagreeing. The empty selection keeps its 422 and gains the assertions the old cases carried: errorCode, reason, and the sentence. The errorCode driver loses the two selections that no longer 422. `score-refusal-consumer-guard.test.ts` — its non-zero control anchored on `three_domains_required`; it anchors on `no_pillars_selected`. New: `health-score-basis-additive-contract.test.ts` pins that `scoreBasis` is published, is optional on all three carriers, and points at the one shared component — a required field on a cached surface would invalidate every cached payload with nothing failing anywhere. It also pins that the walk does not confuse the composite's basis with the older per-pillar `scoreBasis` that blood pressure uses for its axis, since they share a key. Fixtures: the notice-key assertions derive the method half from `SCORE_VERSION` rather than restating 3. Three fixtures that named `three_domains_required` now name `no_usable_data`, a reason the server can still emit. The settings e2e drove its refusal through a two-pillar selection that now succeeds; it deselects every pillar instead.
e2e/
End-to-end browser tests. Playwright 1.60 drives a production build of the app against a real PostgreSQL, with @axe-core/playwright for accessibility assertions. This is the heaviest test tier; CI runs it on every PR, and locally it is worth running when a change touches a UI flow.
Running
pnpm e2e
The Playwright config (../playwright.config.ts) builds the app (next build) and brings up Postgres before the specs run; setup/global-setup.ts seeds the auth + fixture state. The suite is not part of the fast local gate (typecheck / lint / test) — it surfaces at PR CI, so expect responsive/wizard/nav reworks to flag here first.
Layout
setup/—test.ts(the suite'stest+expectentry point),global-setup.ts(auth + DB seeding),test-helpers.ts(shared login + navigation helpers).utils/— fixtures such asmock-dashboard-snapshot.ts.*.spec.ts— one spec per flow: auth/login/redirect, dashboard, charts, the medication-wizard cadences (medications-wizard-*), insights generation + scroll restoration, doctor report, settings, mobile-viewport consistency, locale switch, accessibility (a11y.spec.ts), and thev1427-*regression set.
Conventions
Import test and expect from ./setup/test, never from @playwright/test — types (Page, Route, …) still come from the package. The entry point extends Playwright's test with an automatic fixture that drops route handlers and page listeners after every test, so a handler left in flight cannot fail the test it belonged to once the context closes. src/__tests__/e2e-route-cleanup-guard.test.ts enforces the import boundary.
Assert against stable data-* attributes (data-slot, data-display-step, root display attrs), not viewport-dependent visible text. Responsive sm:hidden UI changes break getByText on desktop while the underlying element is still present — anchoring on data attributes keeps the specs stable across breakpoints. See ../CLAUDE.md.