Both failed on CI and neither failure was about the product. The loading-state scan held /api/auth/me open with a fixed sleep in the route handler. Three things follow from that and all three are wrong: the hold ends 1500 ms after the first request whatever the axe scan is doing, so the state being scanned is not the state the test names; every refetch queues another sleeping handler, so how many are live is a property of the machine; and a handler still sleeping when its request is cancelled answers route.continue with "Route is already handled", which is what the runner reported. It now holds on an explicit gate released at a point the test chooses, counts what it held so a page that was merely slow cannot pass for a page it held, and asserts the gate goes away on release, which is the exit control the entry assertion never had. The labs doorway proved that no OCR probe fires by waiting for that request with a 500 ms timeout and expecting the rejection. An instrument whose pass condition is its own failure has to be armed early and awaited late, and in between it is an unhandled rejection that lands on whichever test is finishing. Its window was wrong too: the timer started before the switch was clicked, so on a slow runner it could expire before the labs page existed. A request listener replaces it, covering everything from before the click to after the record read landed, with the domain's own read asserted in the same set so an absence cannot be a statement about a matcher that stopped matching.
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.