Closing a card-opened document asks the browser to go back, so the entry the selection pushed is consumed and the reader's next Back reaches the page before it. A traversal is a request, not a guarantee: one asked for while another is still settling can be dropped, and nothing raises when it is. The sheet closes, React stays consistent, and the address bar goes on naming a document nobody has open, so a reload or a shared link re-opens the sheet that was just closed. Pressing Escape straight after the browser's Forward button is exactly that shape, and it reproduces: ten repeats across both viewports failed two of twenty with the URL still carrying the id, and none of twenty once the close verifies itself. The check is guarded by the document id, so a traversal that landed on a different selection is left alone, and it is cancelled the moment the sheet re-opens. The browser test that found it could not say so. It asserted the sheet was closed through a locator matched on the document TITLE, and the sheet falls back to an untitled label while its detail read is pending, so the assertion also passed on an open sheet showing a skeleton, resolved immediately instead of waiting, and pushed the failure into a URL poll ten seconds later. It now asserts the dialog role, which is what the neighbouring deep-link test already does.
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.