Two browser contexts, two seeded accounts, one database: the owner invites, the delegate accepts and opens the record, the owner takes it back. Every step through the shipped UI, because the half of this feature that can go wrong is the half a person touches. The stale-paint check is a negative one and deliberately not "the new record's data arrives" — a check that waits for the new data says nothing about what was on screen while it arrived. An init script arms a MutationObserver before any page script on every navigation, including the reload the switch performs, and latches the delegate's own marker weight. The observer's positive control runs first on a page that definitely shows it, so its later silence is not the silence of a broken observer. Then it was broken on purpose, and the result is written into the spec rather than assumed. Removing the switch-time cache wipe, the persister's restore guard and the record-scoped hash — all three — leaves the spec GREEN: the restore already refuses to clobber a query the live cache holds, and the dashboard's query is created during render, so the snapshot never reaches that route's screen at all. Removing the reload turns it red on the banner rather than on the marker. So this suite is a regression net over the journey, not a proof of any one control; the controls are proven where they can be broken and observed, in the unit suites, and the spec's header says so. The nav assertion turned up a real gap while it was failing: the avatar block still linked into the account settings from inside a shared record. Those routes refuse under a switch, so the link led only to an explanation of why it did not work. It renders as a plain identity block now, and the mobile user menu drops the same utilities. Setup gains a second account, a marker weight in each record, a grant reset, and a clear of the login bucket — this setup signs in twice now, and the ceiling is five attempts a quarter-hour, so three local runs used to end with a 429 from the fixture rather than a failure from the product.
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.