The invitation form gains the choice, and the option labels say what actually ships rather than the word "write": a delegate can add readings, results, observations, a medication and a marked dose, and can edit or delete nothing, including what they entered a moment ago. That gap between what "write access" sounds like and what it does is where a support case lives, so it is on the card rather than in a reply. The delegate reads the same thing before they accept. A pending write invitation states its level and one sentence about what accepting means, on the screen where they accept it, because acceptance is where the two consents meet and it happens once. Ending an access says what stays: the entries remain and the owner can remove any of them, and the activity view keeps saying who entered what for as long as the instance's audit window reaches. That number comes from the server, so an instance running ninety days says ninety, and a browser that has not been told says nothing rather than a made-up year. The browser journey now invites at the wider level and reads the posted body for it. That is the only check in the tree that can: the component convention is server-rendered only, so no test under src/ runs the form's submit path, and every one of them stays green with the control wired to a constant.
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.