Files
familysync/.planning/milestones/v1.1-phases/07-mobile-test-harness/07-03-PLAN.md
T
2026-06-18 22:21:38 -04:00

15 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup must_haves
07-mobile-test-harness 03 execute 3
07-01
07-02
apps/pwa/e2e/layout.spec.ts
true
TEST-01
truths artifacts key_links
On both iphone and pixel profiles (D-03/D-04), BottomTabBar Calendar/Lists tabs each measure >=44x44 CSS px
The New Event FAB measures >=56x56 and the PhoneNav settings button >=44x44 on both profiles
Neither /calendar nor /lists has horizontal overflow (documentElement.scrollWidth <= clientWidth) on either profile
BottomTabBar is visible and fully in-viewport (bottom edge <= viewport height) on both mobile profiles
Every asserted interactive element is locatable by ARIA role + accessible name (no CSS-selector fallback)
The harness PROVABLY fails on injected defects: a forced 20px tap target fails Rule 1; a forced 2000px body width fails Rule 2; both pass after the injection is removed
path provides contains
apps/pwa/e2e/layout.spec.ts UI-SPEC Rules 1-4 assertions (tap targets, overflow, in-viewport, accessible names) + harness self-validation injected-defect proofs boundingBox
from to via pattern
apps/pwa/e2e/layout.spec.ts BottomTabBar aria-label='Main navigation' + 'Calendar'/'Lists' links getByRole('navigation'/'link', { name }) getByRole
from to via pattern
apps/pwa/e2e/layout.spec.ts page.addStyleTag injected-defect proof self-validation must-fail assertions addStyleTag
Author `apps/pwa/e2e/layout.spec.ts`: the cross-route structural quality-bar assertions (UI-SPEC Rules 1-4) running on both the iPhone/WebKit and Pixel/Chromium profiles (per D-03 two-profile matrix + D-04 faithful engines) with `serviceWorkers: 'block'` on each context (D-02) — tap targets ≥44px, no horizontal overflow, critical elements visible and in-viewport, accessible names present. Bake in the harness self-validation: prove each core assertion FAILS on a deliberately injected defect, then PASSES once removed. This is the proof that the harness measures rendered geometry, not CSS source (TEST-01).

Purpose: These are the mobile-only defect classes the harness exists to catch (sub-44px touch targets, Schedule-X horizontal overflow). A green suite alone does not prove the assertions are live — the injected-defect proofs are the acceptance bar (07-VALIDATION.md § Harness Self-Validation).

Output: apps/pwa/e2e/layout.spec.ts.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/07-mobile-test-harness/07-CONTEXT.md @.planning/phases/07-mobile-test-harness/07-RESEARCH.md @.planning/phases/07-mobile-test-harness/07-PATTERNS.md @.planning/phases/07-mobile-test-harness/07-UI-SPEC.md @.planning/phases/07-mobile-test-harness/07-VALIDATION.md @apps/pwa/playwright.config.ts @apps/pwa/src/components/BottomTabBar.tsx @apps/pwa/src/components/CalendarShell.tsx Task 1: layout.spec.ts — Rules 1-4 (tap targets, overflow, in-viewport, accessible names) on both profiles apps/pwa/e2e/layout.spec.ts - .planning/phases/07-mobile-test-harness/07-UI-SPEC.md § "Rule 1" (explicit element table + 44/56px thresholds + locator strategies), § "Rule 2" (overflow), § "Rule 3" (in-viewport + safe-area-inset), § "Rule 4" (accessible names table), § "Copywriting Contract" (exact aria-labels) — the authoritative assertion contract - .planning/phases/07-mobile-test-harness/07-PATTERNS.md § "apps/pwa/e2e/layout.spec.ts" — Playwright getByRole/boundingBox/page.evaluate pattern, analog from CalendarShell.test.tsx - .planning/phases/07-mobile-test-harness/07-RESEARCH.md § "Pattern 3" + § "Pattern 4" + § "layout.spec.ts skeleton" — boundingBox + overflow eval idioms - apps/pwa/src/components/BottomTabBar.tsx — confirms `aria-label="Main navigation"` (nav), `aria-label="Calendar"` / `aria-label="Lists"` links, `minHeight: '44px'`, and that it renders null on desktop via matchMedia('(max-width: 767px)') - apps/pwa/src/components/AppNav.tsx — settings button `aria-label` contains `— open settings`; AppNav ALSO exposes a nav with aria-label="Main navigation" AND Calendar/Lists links (see action: scope to avoid strict-mode double match) - apps/pwa/src/components/CalendarShell.tsx — FAB `aria-label="New Event"`; Retry `Retry` (text name); error heading `

Couldn't load events

` Create `apps/pwa/e2e/layout.spec.ts` importing `test, expect` from `@playwright/test`. Tests run against BOTH projects automatically (config matrix per D-03/D-04) — write profile-agnostic specs; do not hardcode viewport widths (read `page.viewportSize()` when needed). STRICT-MODE CAVEAT: both `BottomTabBar` (phone) and `AppNav`/`PhoneNav` expose a `navigation` landmark named "Main navigation" and `link`s named "Calendar"/"Lists" — a bare `getByRole('link', { name: 'Calendar' })` may match 2 elements and throw a strict-mode violation. Scope tap-target assertions to the BottomTabBar specifically: locate the bar via its nav landmark, then query links WITHIN it (e.g. `const bar = page.getByRole('navigation', { name: 'Main navigation' }).last()` or scope by the bottom-bar container, then `bar.getByRole('link', { name: 'Calendar' })`). Confirm the correct scoping by reading BottomTabBar.tsx vs AppNav.tsx before writing the locator; if both share the exact landmark name, disambiguate by position (bottom bar is the fixed-bottom one) or add a `.last()`/filter — document the chosen disambiguation in a comment. Implement, on `/calendar` (and `/lists` where the route applies): Rule 1 (tap targets, UI-SPEC table): BottomTabBar Calendar tab ≥44×44, Lists tab ≥44×44, PhoneNav settings button (`getByRole('button', { name: /open settings/i })`) ≥44×44, New Event FAB (`getByRole('button', { name: 'New Event' })`) ≥56×56 — measure via `await locator.boundingBox()`, assert non-null and width/height thresholds. (Retry button tap target is covered in the calendar error-state spec, Plan 04 — do not duplicate here.) Rule 2 (overflow): on `/calendar` and `/lists`, `page.evaluate(() => ({ scrollWidth: document.documentElement.scrollWidth, clientWidth: document.documentElement.clientWidth }))`, assert `scrollWidth <= clientWidth`. No allowed exceptions (Schedule-X overflow is the defect to catch). Rule 3 (in-viewport): assert the BottomTabBar nav `isVisible()` is true and its `boundingBox().y + height <= page.viewportSize()!.height` (safe-area-inset is 0 in emulation); assert the PhoneNav header is visible. Rule 4 (accessible names): the fact that the Rule 1 locators resolve by role+name already proves accessible names exist; additionally assert the navigation landmark `getByRole('navigation', { name: 'Main navigation' })` is present (scoped per the caveat above). Use relative `page.goto('/calendar')` / `page.goto('/lists')` — NEVER an absolute URL (resolves against config baseURL, Rule 8). Add the standard file header comment (PATTERNS.md § "Test file header comment convention") naming TEST-01 and the run command. - `pnpm --filter @familysync/pwa exec playwright test layout.spec.ts` passes on BOTH `iphone` and `pixel` projects (dev stack + seed up) - the spec asserts boundingBox width AND height ≥44 for BottomTabBar Calendar and Lists tabs and the settings button, and ≥56 for the New Event FAB - the spec asserts `scrollWidth <= clientWidth` on both `/calendar` and `/lists` - every interactive locator uses `getByRole(...)` with a `name` (no `page.locator('css=...')` / testid fallback for the asserted elements) - no absolute URL appears in the file (`grep -E "https?://" layout.spec.ts` returns nothing) - no strict-mode "resolved to N elements" error appears in the run output cd apps/pwa && pnpm exec playwright test layout.spec.ts 2>&1 | tail -15; grep -cE "https?://localhost" e2e/layout.spec.ts layout.spec.ts asserts UI-SPEC Rules 1-4 (tap targets, overflow, in-viewport, accessible names) on both profiles with role+name locators and relative URLs, no strict-mode collisions. Task 2: Harness self-validation — injected-defect must-fail proofs (Rules 1 and 2) apps/pwa/e2e/layout.spec.ts - .planning/phases/07-mobile-test-harness/07-VALIDATION.md § "Harness Self-Validation" — the four self-validation proofs; items 1 (tap-target injection) and 2 (overflow injection) are automatable here - .planning/phases/07-mobile-test-harness/07-RESEARCH.md § "Validation Architecture — Harness Self-Validation" — addStyleTag injection approach - Context7 /microsoft/playwright.dev — `page.addStyleTag`, `expect(...).rejects` / asserting a failing expectation, `test.step` (use Query Documentation if the addStyleTag + must-fail-then-pass pattern needs confirmation) Add a `test.describe('harness self-validation — injected defects', ...)` block to `layout.spec.ts` proving the Rule 1 and Rule 2 assertions are LIVE (measuring rendered geometry, not passing vacuously). Approach: do NOT structure these as tests that intentionally fail the suite — structure each as a single passing test that internally proves the assertion would have failed under a defect and passes after removal. For the tap-target proof: navigate to `/calendar`, inject `page.addStyleTag({ content: 'nav[aria-label="Main navigation"] a { min-height: 20px !important; height: 20px !important; }' })` (or the equivalently-scoped BottomTabBar selector), measure the Calendar tab boundingBox, assert its height is now < 44 (proving the measurement tracks the rendered box, not the source CSS). Then remove the injected style — use `page.addStyleTag` returning a handle and `handle.evaluate(el => el.remove())`, OR reload the page to drop the injected tag — re-measure and assert height ≥ 44 again. For the overflow proof: on `/calendar`, inject `page.addStyleTag({ content: 'body { width: 2000px !important; }' })`, evaluate scrollWidth/clientWidth, assert `scrollWidth > clientWidth` (defect detected), then remove/reload and assert `scrollWidth <= clientWidth` (clean). Each proof is one test that PASSES by demonstrating the fail→clean transition; the suite stays green while proving the assertions detect real defects. Keep these in the same file so they share the config matrix (run on both profiles). Confirm the addStyleTag-remove / reload approach against Context7 before finalizing if uncertain about handle lifecycle. - `layout.spec.ts` contains a self-validation describe block using `page.addStyleTag` - the tap-target proof asserts boundingBox height < 44 WHILE the 20px style is injected, and ≥ 44 after removal/reload - the overflow proof asserts scrollWidth > clientWidth WHILE the 2000px-width style is injected, and ≤ clientWidth after removal/reload - the full `layout.spec.ts` suite (Rules 1-4 + self-validation) is green on both profiles cd apps/pwa && pnpm exec playwright test layout.spec.ts 2>&1 | tail -15; grep -c "addStyleTag" e2e/layout.spec.ts Self-validation proofs in layout.spec.ts demonstrate the tap-target and overflow assertions fail under injected defects and pass once removed — confirming the harness measures rendered geometry. Suite green on both profiles.

<threat_model>

Trust Boundaries

Boundary Description
spec → dev PWA/API Playwright drives the authed PWA (DEV_AUTH_BYPASS); read-only assertions, no form submission

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-07-07 Tampering injected addStyleTag defect styles leaking between tests mitigate Each self-validation proof removes its injected style (handle.remove or page.reload) within the same test before completing; styles are page-scoped and do not persist across navigations/contexts. No global state is mutated.
T-07-08 Information Disclosure spec hardcoding a host/credential mitigate All navigation uses relative paths against the env-driven baseURL (Rule 8); no absolute URL or credential appears in the spec (grep-gated in acceptance).
T-07-12 Tampering serviceWorkers: 'block' (D-02) not applied → SW intercepts and masks a real layout defect mitigate The block is set per-context in playwright.config.ts (Plan 01); these specs assume it and Plan 04 asserts no SW controller. A stale Workbox response cannot satisfy a boundingBox/overflow measurement, so the geometry assertions remain authoritative.

</threat_model>

- `playwright test layout.spec.ts` green on both `iphone` and `pixel`. - Tap-target (≥44/≥56) + overflow + in-viewport + accessible-name assertions present, role+name locators only, relative URLs only. - Self-validation proves Rule 1 and Rule 2 assertions fail under injected defects and recover.

<success_criteria>

  • layout.spec.ts enforces UI-SPEC Rules 1-4 on both device profiles (D-03/D-04).
  • Harness self-validation proves the assertions are live (injected-defect must-fail-then-pass).
  • No strict-mode collisions; no absolute URLs. </success_criteria>
Create `.planning/phases/07-mobile-test-harness/07-03-SUMMARY.md` when done.