Lint (eslint --max-warnings 0): - index.ts: disable no-unsafe-argument on the type-only Context mismatch when delegating to the OIDC handler inside the local-session skip wrapper - localAuth.ts: handleLogout is sync (no await) — drop async (require-await) - devBypass.ts: disable detect-possible-timing-attacks on the public well-known dev-placeholder string compare (not a secret comparison) - remove dead code / unused bindings flagged by no-unused-vars: makeTestApp (localSession.test), makeUnauthContext + BrowserContext import (login.spec), unused memberId (admin.test), unused txSelectCount counter (me.test) - localAuthMiddleware.test / me.test: fix unused + reflow-detached eslint-disable directives Format: prettier --write across the 20 Phase-19 files that were never formatted. Secret scan (gitleaks): allowlist two false positives — the synthetic >=32-char TEST_SECRET in localSession.test.ts, and .planning/ design prose (a generic-api-key regex hit on "credential atomically, 409-equivalent"). Neither is a real secret. Verified locally: format:check, lint, typecheck, md:lint, gitleaks (no leaks), PWA 266/266, API 452/452. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
E2E Test Harness
Playwright test harness for the FamilySync PWA — mobile-emulated (iPhone 14/WebKit + Pixel 7/Chromium) and Desktop Chrome (1280×720), authenticated via DEV_AUTH_BYPASS, deterministically seeded, runs headlessly in CI.
Prerequisites
You bring up the dev stack first (D-09). The harness waits for it — it does not start it.
See docs/deployment.md under "Running locally (host-side, no Docker)" for the canonical bring-up command.
The stack must include:
- API on
:3000started withDEV_AUTH_BYPASS=true(see Security Guardrail below) - PWA dev server on
:5173(pnpm --filter @familysync/pwa dev) - Dev MariaDB on
:3306(exposed viadocker-compose.dev.yml) - Redis on
:6379
DEV_AUTH_BYPASS=true MUST be set in the API's environment BEFORE the API process starts. The harness cannot inject it at runtime — the API reads the env var once at startup. If the API is running without it, all /api/* requests return an auth redirect and every spec fails.
Run Commands
global-setup.ts is fail-closed: it refuses to run (throws before any DB write) unless
DEV_AUTH_BYPASS=true and NODE_ENV !== 'production' — the same handshake the API uses
(see Security Guardrail). So DEV_AUTH_BYPASS=true must be exported in the test process
environment (not only the API's). Source the DB credentials from the repo-root .env and point
DB_HOST at the host-side MariaDB:
# Load DB creds, then run. DEV_AUTH_BYPASS=true is required by the global-setup guard.
set -a; source .env; set +a
export DEV_AUTH_BYPASS=true DB_HOST=127.0.0.1 DB_PORT=3306
# Full suite — iPhone (WebKit), Pixel (Chromium), Desktop Chrome profiles
pnpm --filter @familysync/pwa test:e2e
# Single profile (faster local iteration)
pnpm --filter @familysync/pwa exec playwright test --project=pixel
pnpm --filter @familysync/pwa exec playwright test --project=desktop
# Headed (local debug — shows the browser)
pnpm --filter @familysync/pwa exec playwright test --headed
# UI mode (interactive test explorer)
pnpm --filter @familysync/pwa test:e2e:ui
Env Vars
The harness reads these from the environment. DB credentials are env-only — never hardcoded in seed scripts or specs.
| Var | Default | Purpose |
|---|---|---|
PLAYWRIGHT_BASE_URL |
http://localhost:5173 |
Base URL for all spec navigation and the /health readiness poll |
DB_HOST |
127.0.0.1 |
MariaDB host for the global-setup seed script |
DB_PORT |
3306 |
MariaDB port |
DB_USER |
familysync |
MariaDB user |
DB_PASSWORD |
(empty) | MariaDB password — set in environment or .env |
DB_NAME |
familysync |
MariaDB database name |
Set DB_PASSWORD (and other non-default values) via the shell or the repo root .env file before running. The .env file is gitignored — never commit credentials.
Security Guardrail — DEV_AUTH_BYPASS
DEV_AUTH_BYPASS=true is a development-only bypass that resolves all API requests to Dev User id 1 without OIDC authentication.
The API enforces this via apps/api/src/auth/devBypass.ts:
if (process.env.NODE_ENV === 'production') → bypass is a no-op (always)
if (process.env.DEV_AUTH_BYPASS !== 'true') → bypass is a no-op
The production Docker Compose (docker-compose.yml) MUST NOT set DEV_AUTH_BYPASS. Setting it in production is an Elevation of Privilege vulnerability — any request would be resolved as the dev user with no authentication.
The docker-compose.dev.yml override sets it for local dev and CI. Review it before any production deployment to confirm DEV_AUTH_BYPASS is absent from the production compose file.
No Session State File
This harness uses no storageState file (D-01). There is no checked-in session cookie, no expiring auth artifact, and no per-run login flow. DEV_AUTH_BYPASS=true makes the API respond as user 1 unconditionally — the tests run repeatably day-over-day without re-authentication. See PITFALLS.md §Pitfall 14 for why storageState is excluded.
What globalSetup Does
Before any spec runs, global-setup.ts:
- Fail-closed guard: throws immediately if
NODE_ENV === 'production'orDEV_AUTH_BYPASS !== 'true', before opening any DB connection — so the TRUNCATE/seed can never run against a production (or unconfirmed) database. - Polls
PLAYWRIGHT_BASE_URL/healthuntil 200 OK (60s timeout, then fails fast with a clear error). - Truncates
list_items,list_shares,lists,calendar_events(FK checks disabled around TRUNCATE). - Seeds deterministic fixtures for Dev User 1:
- One timed calendar event (
'Seeded Test Event') on calendar_id=10 - One shared list (
'E2E Grocery List') owned by user 1, with alist_sharesrow and two items ('Milk','Eggs')
- One timed calendar event (
This seeding is idempotent — two consecutive runs leave the same row counts, no stale rows, no duplicate-key errors.
CI (Phase 8)
Phase 8 (Gitea CI) runs these specs unchanged as a PR UI-regression step. The CI workflow owns:
- Bringing up the dev stack (compose) with
DEV_AUTH_BYPASS=true - Waiting for the MariaDB health check before starting the API
- Setting
PLAYWRIGHT_BASE_URL,DB_*, andDEV_AUTH_BYPASS=trueenv vars in the runner environment (the global-setup guard requiresDEV_AUTH_BYPASS=truein the Playwright process, not only the API's)
The harness handles its own readiness gate (/health poll) once the runner sets things up. No changes to spec files are needed for CI — the harness is stack-agnostic via env vars.
CI Dockerfile must use playwright install --with-deps webkit chromium to install WebKit system deps (see RESEARCH.md §Pitfall 6).