Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
132 lines
7.6 KiB
Markdown
132 lines
7.6 KiB
Markdown
---
|
||
phase: 07-mobile-test-harness
|
||
plan: '03'
|
||
subsystem: test-harness
|
||
tags: [playwright, e2e, layout, tap-targets, overflow, accessibility, self-validation]
|
||
dependency_graph:
|
||
requires:
|
||
- "apps/pwa/playwright.config.ts (07-01) — iphone/pixel project matrix, serviceWorkers: 'block'"
|
||
- 'apps/pwa/e2e/global-setup.ts (07-02) — /health readiness gate + DB seed (calendar_id=10, E2E Grocery List)'
|
||
provides:
|
||
- 'apps/pwa/e2e/layout.spec.ts — UI-SPEC Rules 1-4 assertions (tap targets, overflow, in-viewport, accessible names)'
|
||
- 'Harness self-validation: addStyleTag injected-defect proofs for Rule 1 + Rule 2'
|
||
- '30 tests (15 per profile) — all passing on iphone (WebKit) and pixel (Chromium)'
|
||
affects:
|
||
- 'Phase 07 plan 04 (calendar.spec.ts / lists.spec.ts share the same harness foundation)'
|
||
- 'Phase 08 CI (layout.spec.ts is a PR regression step)'
|
||
tech_stack:
|
||
added: []
|
||
patterns:
|
||
- 'boundingBox() — rendered geometry measurement, not CSS-declared values'
|
||
- 'page.evaluate(() => scrollWidth/clientWidth) — DOM overflow measurement'
|
||
- 'page.addStyleTag + handle.evaluate(el => el.remove()) — injected-defect proof pattern'
|
||
- "getByRole('navigation', { name }) scoping — avoids strict-mode collision between BottomTabBar and DesktopNav"
|
||
- "getByText('FamilySync', { exact: true }) — avoids matching 'Install FamilySync' install-prompt"
|
||
key_files:
|
||
created:
|
||
- apps/pwa/e2e/layout.spec.ts
|
||
modified:
|
||
- apps/pwa/e2e/global-setup.ts
|
||
decisions:
|
||
- "D-03-SCOPE-NAV: On mobile profiles (390px/412px), AppNav renders PhoneNav as <header> (not a nav landmark) — only BottomTabBar exposes <nav aria-label='Main navigation'>. No strict-mode collision in practice, but tap-target locators are scoped inside the nav landmark for robustness."
|
||
- "D-03-PHONENAV-TEXT: getByText('FamilySync', { exact: true }) required — the InstallPrompt renders 'Install FamilySync', which getByText('FamilySync') without exact:true matches as a substring, causing a strict-mode violation on WebKit."
|
||
- 'D-03-SELF-VALIDATION: Self-validation proofs use addStyleTag + handle.evaluate(el => el.remove()) to inject and remove the defect style within the same test. No page.reload() needed — handle removal is synchronous and immediately clears the injected CSS.'
|
||
metrics:
|
||
duration_seconds: 480
|
||
completed_date: '2026-06-11'
|
||
tasks_completed: 2
|
||
files_changed: 2
|
||
---
|
||
|
||
# Phase 07 Plan 03: layout.spec.ts Layout Assertions Summary
|
||
|
||
**One-liner:** layout.spec.ts enforcing UI-SPEC Rules 1-4 (tap targets ≥44/56px, no horizontal overflow, in-viewport, accessible names) on iPhone/WebKit + Pixel/Chromium with addStyleTag injected-defect proofs — 30 tests, 0 failures.
|
||
|
||
## What Was Built
|
||
|
||
`apps/pwa/e2e/layout.spec.ts` with four describe blocks covering:
|
||
|
||
**Rule 1/3/4 — BottomTabBar on /calendar (8 tests per profile)**
|
||
|
||
- Navigation landmark visible (Rule 4 — accessible name proof)
|
||
- Calendar tab boundingBox ≥ 44×44px (Rule 1)
|
||
- Lists tab boundingBox ≥ 44×44px (Rule 1)
|
||
- BottomTabBar bottom edge ≤ viewport height (Rule 3 — in-viewport, safe-area-inset)
|
||
- PhoneNav header "FamilySync" visible (Rule 3)
|
||
- Settings button boundingBox ≥ 44×44px (`getByRole('button', { name: /open settings/i })`)
|
||
- New Event FAB boundingBox ≥ 56×56px (Rule 1 — larger threshold)
|
||
|
||
**Rule 1/3/4 — BottomTabBar on /lists (4 tests per profile)**
|
||
|
||
- Navigation landmark visible on /lists
|
||
- Calendar tab ≥ 44×44px on /lists
|
||
- Lists tab ≥ 44×44px on /lists
|
||
- BottomTabBar in-viewport on /lists
|
||
|
||
**Rule 2 — No horizontal overflow (2 tests per profile)**
|
||
|
||
- `scrollWidth ≤ clientWidth` on /calendar
|
||
- `scrollWidth ≤ clientWidth` on /lists
|
||
|
||
**Harness self-validation — injected defects (2 tests per profile)**
|
||
|
||
- Rule 1 proof: injects `nav[aria-label="Main navigation"] a { height: 20px !important }`, asserts height < 44, removes, asserts height ≥ 44 — proves boundingBox tracks rendered geometry
|
||
- Rule 2 proof: injects `body { width: 2000px !important }`, asserts scrollWidth > clientWidth, removes, asserts scrollWidth ≤ clientWidth — proves overflow detection is live
|
||
|
||
**Total: 30 tests (15 iphone, 15 pixel), 0 failures.**
|
||
|
||
## Verification Evidence
|
||
|
||
- `playwright test e2e/layout.spec.ts` (both profiles): `30 passed`
|
||
- `grep -E "https?://" apps/pwa/e2e/layout.spec.ts` → empty (no absolute URLs)
|
||
- `grep -c addStyleTag apps/pwa/e2e/layout.spec.ts` → 2 (both self-validation proofs present)
|
||
- All locators use `getByRole(..., { name })` or scoped-within-nav — no CSS selector fallback
|
||
- No strict-mode "resolved to N elements" errors in either profile run
|
||
|
||
## Deviations from Plan
|
||
|
||
### Auto-fixed Issues
|
||
|
||
**1. [Rule 1 - Bug] WebKit strict-mode violation: `getByText('FamilySync')` matched 2 elements**
|
||
|
||
- **Found during:** Task 1 first iphone run
|
||
- **Issue:** `getByText('FamilySync')` without `exact:true` also matched the `<div>Install FamilySync</div>` text in the InstallPrompt component, causing a strict-mode violation on WebKit (where the install prompt was visible).
|
||
- **Fix:** Changed to `getByText('FamilySync', { exact: true })` — matches only the `<span>FamilySync</span>` in PhoneNav.
|
||
- **Files modified:** `apps/pwa/e2e/layout.spec.ts`
|
||
- **Commit:** 52e14a8
|
||
|
||
**2. [Rule 3 - Blocking] global-setup.ts: MariaDB TIMESTAMP rejected ISO 8601 format**
|
||
|
||
- **Found during:** Task 1 execution — global-setup failed before any spec could run
|
||
- **Issue:** `futureStart.toISOString().replace(/\.\d+Z$/, 'Z')` produces `'2026-06-12T05:58:35Z'` (with `T` separator), which MariaDB TIMESTAMP rejects with `Incorrect datetime value`. MariaDB requires `'YYYY-MM-DD HH:MM:SS'` format.
|
||
- **Fix:** Added `.replace('T', ' ')` and removed the trailing `Z` — produces `'2026-06-12 05:58:35'` which MariaDB TIMESTAMP accepts.
|
||
- **Files modified:** `apps/pwa/e2e/global-setup.ts`
|
||
- **Commit:** d3c6726 (fix(07-02))
|
||
|
||
**3. [Observation] DesktopNav nav landmark absent on mobile profiles — no strict-mode risk**
|
||
|
||
- **Found during:** Component analysis before writing locators
|
||
- **Issue:** The plan warned about strict-mode collision between BottomTabBar nav and DesktopNav nav, both named "Main navigation". In practice, on mobile profiles (390px/412px), `AppNav` renders `PhoneNav` (a `<header>`, not a nav), so DesktopNav's nav is absent. No collision occurs.
|
||
- **Fix:** Still scoped tap-target locators inside `getByRole('navigation', { name: 'Main navigation' })` for defensive robustness against any future layout change.
|
||
- **Files modified:** None (design decision, no code change)
|
||
|
||
## Known Stubs
|
||
|
||
None.
|
||
|
||
## Threat Surface Scan
|
||
|
||
No new network endpoints, auth paths, or schema changes. `layout.spec.ts` is a test-only file. Threat mitigations from plan:
|
||
|
||
- **T-07-07 (injected style leakage):** Each self-validation test removes the injected style via `handle.evaluate(el => el.remove())` within the same test before completing. Styles are page-scoped and do not persist across navigations or test contexts.
|
||
- **T-07-08 (hardcoded host):** `grep -E "https?://" apps/pwa/e2e/layout.spec.ts` returns empty — all navigation uses relative paths (`/calendar`, `/lists`) that resolve against `playwright.config.ts` `baseURL`.
|
||
- **T-07-12 (SW intercept):** `serviceWorkers: 'block'` is set per-context in `playwright.config.ts` (Plan 01). Geometry assertions (boundingBox, scrollWidth) cannot be satisfied by a cached SW response, so the assertions remain authoritative even if the block were bypassed.
|
||
|
||
## Self-Check: PASSED
|
||
|
||
- `apps/pwa/e2e/layout.spec.ts` — exists (`git show --stat 52e14a8`)
|
||
- `apps/pwa/e2e/global-setup.ts` — modified (fix commit d3c6726)
|
||
- Fix commit `d3c6726` — exists
|
||
- Task commit `52e14a8` — exists
|
||
- 30 tests passing on both profiles — verified by final run output
|