Files
familysync/.planning/phases/07-mobile-test-harness/07-03-PLAN.md
T

153 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 07-mobile-test-harness
plan: 03
type: execute
wave: 3
depends_on: ["07-01", "07-02"]
files_modified:
- apps/pwa/e2e/layout.spec.ts
autonomous: true
requirements: [TEST-01]
user_setup: []
must_haves:
truths:
- "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"
artifacts:
- path: "apps/pwa/e2e/layout.spec.ts"
provides: "UI-SPEC Rules 1-4 assertions (tap targets, overflow, in-viewport, accessible names) + harness self-validation injected-defect proofs"
contains: "boundingBox"
key_links:
- from: "apps/pwa/e2e/layout.spec.ts"
to: "BottomTabBar aria-label='Main navigation' + 'Calendar'/'Lists' links"
via: "getByRole('navigation'/'link', { name })"
pattern: "getByRole"
- from: "apps/pwa/e2e/layout.spec.ts"
to: "page.addStyleTag injected-defect proof"
via: "self-validation must-fail assertions"
pattern: "addStyleTag"
---
<objective>
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`.
</objective>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<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
</context>
<tasks>
<task type="auto">
<name>Task 1: layout.spec.ts — Rules 1-4 (tap targets, overflow, in-viewport, accessible names) on both profiles</name>
<files>apps/pwa/e2e/layout.spec.ts</files>
<read_first>
- .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 `<button>Retry</button>` (text name); error heading `<h2>Couldn't load events</h2>`
</read_first>
<action>
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.
</action>
<acceptance_criteria>
- `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
</acceptance_criteria>
<verify>
<automated>cd apps/pwa && pnpm exec playwright test layout.spec.ts 2>&1 | tail -15; grep -cE "https?://localhost" e2e/layout.spec.ts</automated>
</verify>
<done>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.</done>
</task>
<task type="auto">
<name>Task 2: Harness self-validation — injected-defect must-fail proofs (Rules 1 and 2)</name>
<files>apps/pwa/e2e/layout.spec.ts</files>
<read_first>
- .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)
</read_first>
<action>
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.
</action>
<acceptance_criteria>
- `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
</acceptance_criteria>
<verify>
<automated>cd apps/pwa && pnpm exec playwright test layout.spec.ts 2>&1 | tail -15; grep -c "addStyleTag" e2e/layout.spec.ts</automated>
</verify>
<done>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.</done>
</task>
</tasks>
<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>
<verification>
- `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.
</verification>
<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>
<output>
Create `.planning/phases/07-mobile-test-harness/07-03-SUMMARY.md` when done.
</output>