diff --git a/.planning/phases/07-mobile-test-harness/07-VERIFICATION.md b/.planning/phases/07-mobile-test-harness/07-VERIFICATION.md new file mode 100644 index 0000000..94c7460 --- /dev/null +++ b/.planning/phases/07-mobile-test-harness/07-VERIFICATION.md @@ -0,0 +1,113 @@ +--- +phase: 07-mobile-test-harness +verified: 2026-06-11T02:30:00Z +status: passed +score: 4/4 must-haves verified +overrides_applied: 0 +--- + +# Phase 7: Mobile Test Harness Verification Report + +**Phase Goal:** The assistant can drive the PWA in a mobile-emulated, authenticated browser context against the host-side dev stack, so mobile-only layout and flow defects can be caught automatically instead of only by the operator on real devices. This harness is also the artifact Phase 8 (CI) runs for UI regression. +**Verified:** 2026-06-11T02:30:00Z +**Status:** passed +**Re-verification:** No — initial verification + +## Goal Achievement + +### Observable Truths + +| # | Truth | Status | Evidence | +|---|-------|--------|----------| +| 1 | An automated run can load the PWA in a mobile-emulated viewport (device profile + mobile UA + touch) and assert on responsive layout / tap targets. | VERIFIED | `playwright.config.ts` defines `devices['iPhone 14']` (WebKit, 390×844, Mobile Safari UA, hasTouch) and `devices['Pixel 7']` (Chromium, 412×915, Chrome Android UA, hasTouch). `layout.spec.ts` asserts boundingBox geometry (≥44px tap targets, ≥56px FAB, no overflow). All 58 tests pass on both profiles. Harness self-validation proves assertions track rendered geometry, not CSS source. | +| 2 | The automated run reaches the authenticated PWA via the existing DEV_AUTH_BYPASS on the host-side dev stack — no manual login and no Authelia/OIDC mocking. | VERIFIED | `playwright.config.ts` has no `storageState` key. `global-setup.ts` has no OIDC mock. `calendar.spec.ts` includes an explicit runtime assertion: `getByRole('navigation', { name: 'Main navigation' })` is visible and `page.url()` hostname matches `/^(localhost|127\.0\.0\.1)$/`. Live run confirms both profiles reach authenticated content via DEV_AUTH_BYPASS with no redirect to Authelia. | +| 3 | The harness runs repeatably day-over-day without re-capturing any session state (no stale storage-state failures). | VERIFIED | No `storageState` key exists anywhere in `playwright.config.ts`, `global-setup.ts`, or any spec. `global-setup.ts` issues TRUNCATE+seed on every run. Two consecutive full runs both produced 58/58 passing in 27–28s with identical results. No auth artifact on disk; the bypass is stateless per-request. | +| 4 | The harness specs are structured so they can run headlessly in CI (Phase 8) against a dev stack the runner brings up — no dependence on a developer's already-running host stack. | VERIFIED | `baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:5173'` (env-driven). `reuseExistingServer: !process.env.CI` (CI starts fresh). `retries: process.env.CI ? 2 : 0`, `workers: process.env.CI ? 1 : undefined`, `reporter: process.env.CI ? 'github' : 'list'`. No hardcoded hosts in any spec file (grep confirms 0 absolute URLs). DB credentials all env-driven via `DB_*` vars. `e2e/README.md` documents exact CI env-var contract. | + +**Score:** 4/4 truths verified + +### Required Artifacts + +| Artifact | Expected | Status | Details | +|----------|----------|--------|---------| +| `apps/pwa/playwright.config.ts` | Two-project device matrix, serviceWorkers block, env baseURL, globalSetup ref, vite-only webServer | VERIFIED | Contains `devices['iPhone 14']`, `devices['Pixel 7']`, `serviceWorkers: 'block'` on both projects, `globalSetup: './e2e/global-setup.ts'`, `PLAYWRIGHT_BASE_URL` env pattern, `reuseExistingServer: !process.env.CI` | +| `apps/pwa/e2e/global-setup.ts` | /health readiness poll, TRUNCATE+seed for user 1/calendar 10, fail-closed guard | VERIFIED | Fail-closed guard (NODE_ENV=production throws, DEV_AUTH_BYPASS!='true' throws) runs before any DB connection. Polls `${baseURL}/health` 60s. TRUNCATE list_items/list_shares/lists/calendar_events with FK checks disabled. INSERT IGNORE calendars id=10. Seeds one calendar_event, one shared list, list_shares row, two list_items. No `@playwright/test` import. | +| `apps/pwa/e2e/layout.spec.ts` | UI-SPEC Rules 1-4 + injected-defect self-validation | VERIFIED | Contains `boundingBox` assertions (≥44px Calendar/Lists/Settings tabs, ≥56px FAB), overflow eval on /calendar and /lists, in-viewport check, role+name locators throughout, two `addStyleTag` self-validation proofs, no absolute URLs. | +| `apps/pwa/e2e/calendar.spec.ts` | Populated + error states, auth-bypass + SW-block precondition assertions | VERIFIED | Contains "Couldn't load events" heading assertion, Retry button ≥44px, `page.route('/api/events*', ...500)` before goto, auth-bypass URL hostname assertion, `navigator.serviceWorker.controller` null assertion. | +| `apps/pwa/e2e/lists.spec.ts` | Populated + empty states, no DB mutation | VERIFIED | Contains "E2E Grocery List" card assertion, "No lists yet" presence/absence checks, network-simulated empty state via `page.route('/api/lists', ...)`, overflow checks in both states. | +| `apps/pwa/e2e/README.md` | Run instructions, DEV_AUTH_BYPASS guardrail, no-storageState documentation | VERIFIED | Documents the full run command, security guardrail (production compose MUST NOT set DEV_AUTH_BYPASS), all DB_* and PLAYWRIGHT_BASE_URL env vars, no storageState file policy, CI usage notes. | +| `apps/pwa/vitest.config.ts` | `exclude: ['e2e/**']` to prevent Vitest/Playwright spec collision | VERIFIED | Line 17: `exclude: ['e2e/**', 'node_modules/**']` inside the `test:` block. | +| `apps/pwa/tsconfig.e2e.json` | Separate tsconfig bringing playwright.config.ts and e2e/** into the typecheck gate | VERIFIED | Extends `./tsconfig.json`, `include: ["playwright.config.ts", "e2e/**/*"]`. The `typecheck` script runs both: `tsc --noEmit && tsc --project tsconfig.e2e.json --noEmit`. | +| `apps/pwa/package.json` | `@playwright/test` devDependency + `test:e2e` scripts | VERIFIED | `@playwright/test: 1.60.0` in devDependencies. `test:e2e`, `test:e2e:ui`, `test:e2e:headed` scripts present. | +| Root `package.json` | `test:e2e` workspace delegation script | VERIFIED | `"test:e2e": "pnpm --filter @familysync/pwa test:e2e"` | + +### Key Link Verification + +| From | To | Via | Status | Details | +|------|----|-----|--------|---------| +| `playwright.config.ts` | `e2e/global-setup.ts` | `globalSetup: './e2e/global-setup.ts'` | VERIFIED | File exists, is a valid default export async function, runs before any spec | +| `playwright.config.ts` | `PLAYWRIGHT_BASE_URL` env | `baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:5173'` | VERIFIED | Same pattern used in `webServer.url` and inside `global-setup.ts` | +| `global-setup.ts` | dev MariaDB :3306 | `mysql2.createConnection` with `DB_*` env vars | VERIFIED | Uses `DB_HOST ?? '127.0.0.1'`, `DB_PORT ?? 3306`, `DB_USER ?? 'familysync'`, `DB_PASSWORD ?? ''`, `DB_NAME ?? 'familysync'` — mirrors `apps/api/src/db/client.ts` | +| `global-setup.ts` | `calendar_events.calendar_id=10` | `INSERT IGNORE INTO calendars` guard before event insert | VERIFIED | Line 90: `INSERT IGNORE INTO calendars (id, user_id, url, display_name, color, is_shared) VALUES (10, 1, ...)` — works on fresh CI DB and populated dev DB | +| `layout.spec.ts` | `BottomTabBar` nav | `getByRole('navigation', { name: 'Main navigation' })` | VERIFIED | Tests resolve on both profiles; no strict-mode collision (DesktopNav at ≥768px is not rendered on 390/412px viewports) | +| `calendar.spec.ts` | `page.route('/api/events*', ...)` | Error-state simulation registered before goto | VERIFIED | Line 98: route registered before `page.goto('/calendar')`, unrouted on line 116 | +| `lists.spec.ts` | seeded 'E2E Grocery List' card | `getByRole('button', { name: 'Open list: E2E Grocery List' })` | VERIFIED | Card resolves on both profiles after global-setup seed | + +### Data-Flow Trace (Level 4) + +Not applicable — this phase produces a test harness (spec files and config), not a UI component that renders dynamic data from an API. The harness itself is the data producer for downstream assertions. + +### Behavioral Spot-Checks (Step 7b) + +| Behavior | Command | Result | Status | +|----------|---------|--------|--------| +| Exactly two projects (iphone, pixel) reported | `playwright test --list` | Lists 29 iphone + 29 pixel = 58 tests; both project names confirmed | PASS | +| Full 58-test suite passes (run 1) | `pnpm --filter @familysync/pwa test:e2e` | 58 passed (28.0s) | PASS | +| Full 58-test suite passes (run 2 — idempotency) | `pnpm --filter @familysync/pwa test:e2e` | 58 passed (27.4s) | PASS | +| Typecheck gate covers e2e files | `pnpm --filter @familysync/pwa typecheck` | Exit 0 (both `tsc --noEmit` and `tsc --project tsconfig.e2e.json --noEmit`) | PASS | +| No storageState or toHaveScreenshot in harness | grep across config + all specs | 0 matches (1 comment-only hit in config) | PASS | +| No absolute URLs in spec files | grep for `https?://localhost` in e2e/*.spec.ts | 0 matches | PASS | + +### Probe Execution + +No conventional `scripts/*/tests/probe-*.sh` probes declared for this phase. + +### Requirements Coverage + +| Requirement | Source Plan | Description | Status | Evidence | +|-------------|-------------|-------------|--------|----------| +| TEST-01 | Plans 01, 03, 04 | Drive PWA in mobile-emulated viewport (device profile + mobile UA + touch) for automated UI/layout verification | SATISFIED | `devices['iPhone 14']` + `devices['Pixel 7']` in config with hasTouch; layout.spec.ts measures boundingBox; 29 tests per profile pass | +| TEST-02 | Plans 01, 02, 04 | Automated runs reach authenticated PWA via DEV_AUTH_BYPASS (no manual login, no OIDC mock) | SATISFIED | No storageState; fail-closed guard requires DEV_AUTH_BYPASS=true; calendar.spec.ts asserts auth-bypass at runtime; live runs confirm | + +### Anti-Patterns Found + +| File | Line | Pattern | Severity | Impact | +|------|------|---------|----------|--------| +| None found | — | No TBD/FIXME/XXX/TODO/PLACEHOLDER markers in any harness file | — | — | + +Code review WR-04 identified that `lists.spec.ts` uses the exact string `/api/lists` instead of the glob `/api/lists*` for the empty-state route mock (inconsistent with the calendar spec's `/api/events*` glob). This is an advisory warning from the review — the harness currently passes because the endpoint has no query params. This is catalogued but does not block goal achievement; the phase goal is achieved and this is a robustness concern for future maintenance. + +### Human Verification Required + +None. All success criteria are verifiable programmatically and were verified via live execution. + +The following items from the code review are advisory warnings (not blockers for phase goal): + +- WR-01: Readiness gate accepts any 2xx — could mis-fire if Authelia redirects to a 200 login page. Mitigated in practice by the fail-closed DEV_AUTH_BYPASS guard which ensures the API is running in bypass mode before the test process starts. +- WR-02: Deadline-expiry uses `Date.now() >= deadline` inference instead of an explicit boolean. Low practical risk given the 60s window and 1s poll interval. +- WR-03: Health gate polls Vite's proxied /health rather than the API directly. Works correctly via the Vite proxy; a downed API would produce a 5xx not a 200. +- WR-04: `/api/lists` exact match vs glob — brittle to future query param additions. +- WR-05: `unroute` not in `finally` — an assertion failure could leave a mock active for subsequent tests in the same worker. +- WR-06: Self-validation comment says "page.reload drops inline style tags" but code uses `el.remove()` — stale comment; code is correct. +- WR-07: SW-controller assertion passes vacuously on WebKit where `navigator.serviceWorker` may be undefined. + +These are carried from the code review as advisory only; none block the phase goal. + +### Gaps Summary + +No gaps. All four success criteria are verified against the actual codebase and confirmed by live execution. + +--- + +_Verified: 2026-06-11T02:30:00Z_ +_Verifier: Claude (gsd-verifier)_