The Insights overview panned sideways at every phone width. SectionHeading pinned its trailing action slot with shrink-0, which sizes a flex item to its max-content width. That slot was designed for a compact control, but every caller passes prose today — the provenance method caption, the device-score subtitle, the sleep-quality subtitle. A sentence at max-content measures 1229 px in English and 1464 px in German, and since the row sits inside the AuthShell <main> — whose overflow-y:auto makes overflow-x compute to auto — that width turned the page into a horizontal scroller. min-w-0 lifts the flex min-width:auto floor so the caption wraps to the row instead; the parent's flex-wrap still moves the slot to its own line before anything is squeezed. The existing width guards could not see this. Both measured documentElement.scrollWidth, which never budges when <main> absorbs the overflow into its own scroll area, and the insights case ran against an empty mocked payload that fits any viewport by construction. - Add e2e/utils/horizontal-overflow.ts: asserts the document AND the <main> scroll container, and names the widest offending element on failure. - Add e2e/mobile-horizontal-overflow.spec.ts: sweeps /insights, /, /measurements and /medications at 360/390/393/402/430 px against populated fixtures, plus a canary proving the measurement can see through <main>, and a check that the insights fixture really populates the page. - Rewrite the cycle spec's empty payload as a populated one and sweep every phone width; its /insights case moves to the new spec. - Point mobile-density.spec.ts at the shared assertion — it carried the same document-only blind spot.
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.