`<main id="main-content">` scrolls the authenticated shell, but it was `position: static`, so an absolutely-positioned descendant with no nearer positioned ancestor resolved against the initial containing block. That block sits outside the scroll container: `overflow-y-auto` never clipped those boxes, and their static position extended the DOCUMENT's scrollable overflow instead. The result is a second vertical scroll surface beside `<main>`, which UI-STANDARDS §9 forbids. The escapees are library markup, so no per-component wrapper would have caught them. A Radix `<Switch>` outside a `<form>` renders a hidden `position:absolute` bubble input on its first pass and drops it only once the button ref resolves, and the root `loading.tsx` chart skeleton carries an `sr-only` announcement that lives until the real content streams in. Both are gone within a few hundred milliseconds of hydration, so the page carried a full-height document scrollbar for the whole pre-hydration window and looked clean immediately after. On /settings/modules at a 390x844 viewport the document measured 1952 px against an 844 px viewport while the switches were still bubble-inputs. Anchoring the scroll container closes the escape route for every such box at once. The over-scroll guard grows the matching structural assertion: `<main>` must not compute to `position: static`. The height read it already had can only see an escapee that happens to be mounted at measure time, which is why it went red on a loaded CI runner and green on the same tree twenty minutes earlier; the computed style is timing-free and fails on every route and both viewports the moment the anchor is removed.
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.