One journey across the whole path: an account writes readings in three domains and files a PDF, downloads its own record as the passphrase-sealed archive, an admin snapshots it from the backups console, and the snapshot is restored over an account that has since lost a reading and a dose and gained one the snapshot never saw. The assertions address what the documentation promises rather than what the implementation happens to do. The archive is checked byte by byte against the HLX1 header in src/lib/export/passphrase-archive.ts — magic, version, KDF id and the Argon2 cost the file carries — because "a file arrived" is equally true of plaintext JSON under a new extension. The restored reading and dose are checked by their ORIGINAL ids, because a restore that re-created them under fresh ones would have written a different record that reads the same. The reading created after the snapshot is asserted GONE: the runbook says the restore replaces the account's data tables, and replacing is not merging. Two refusing controls. A stored copy with one flipped ciphertext byte is refused with the route's decrypt failure and leaves the record untouched — the envelope is AES-256-GCM and its tag covers every byte, so the flip is caught before the transaction opens. A delegate holding whole-record READ inside the account gets the not-covered panel instead of the export card, never reaches the console, and is refused 403 by the list, the manual run and the restore, the last of them naming the real snapshot so the refusal cannot be mistaken for a missing row. Waiting is on the job's own evidence: the console's endpoint is polled until this account's stored copy is newer than the click, so nothing in the file guesses at a queue. The journey has its own account, its own delegate and its own cookie jars; only the reset between repetitions and the deliberate corruption reach Postgres directly, because a browser cannot express either. The backups console gains data-testid hooks and a data-backup-id on each row so the spec addresses controls rather than translated labels.
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.fixtures/— synthetic payloads a journey uploads, built in code rather than committed as blobs so the content stays reviewable (apple-health-export.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.