Entering or leaving a record is a server write followed by window.location.assign, so the click that starts it returns long before the document it produces exists. The switcher helper returned there, and everything a test did next raced the app's own navigation. Both ways of losing that race are silent. A goto issued into the gap is cancelled when the assign commits, which Chromium reports as net::ERR_ABORTED and which is what CI saw twice. Or the goto wins, and the page it lands on belongs to the caller's own record, where a route that is ungranted in somebody else's record is simply granted: /coach opened in that gap issued all four of the protected reads the test asserts never happen. The second is the worse outcome, because the assertion was reading the wrong record and could as easily have passed there. Neither is visible in the DOM the click leaves behind, which is why waiting on the banner cannot settle it: the document on its way out already shows whatever the next assertion looks for, so the wait returns at once and proves nothing. A marker on window can settle it, because a document replacement is the one thing that destroys it. If the navigation never happens the wait fails, which is what a switch that did not land should do. Both helpers use it now, so the retry that was added downstream of leaving a record has nothing left to absorb; it stays, because the claim there is that the record survives the round trip rather than that one navigation attempt wins. Reproduced before fixing, under CPU contention at CI's worker count: three of six runs flaked, showing both faces. Six of six clean at the same contention afterwards, then twenty consecutive runs of the file and the whole sharing set, none flaky.
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.