--- phase: 7 slug: mobile-test-harness status: draft shadcn_initialized: false preset: none created: 2026-06-11 framing: quality-bar-contract --- # Phase 7 — Mobile UI Quality-Bar Contract > This phase builds **no new UI**. The harness asserts against the existing > FamilySync PWA. This document is a **quality-bar contract**, not a design > system spec. Its job is to pin every measurable threshold the harness must > enforce so the planner can turn each rule into a concrete Playwright > assertion. Template sections that have no assertable content for a test > harness are marked N/A with a one-line reason. --- ## Design System N/A — test harness, no new UI. The existing design system is declared in `apps/pwa/src/styles/tokens.css` and consumed by the assertions below. | Property | Value | |----------|-------| | Tool | none (no shadcn; inline CSS custom properties) | | Preset | not applicable | | Component library | none (lucide-react icons; Schedule-X calendar widget) | | Icon library | lucide-react (via npm dep, no CDN) | | Font | `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` | --- ## Spacing Scale N/A — test harness, no new UI. Spacing tokens are declared in `tokens.css` and are not re-specified here. Assertions reference computed pixel values derived from those tokens where needed (e.g. BottomTabBar height = 56px + safe-area-inset). --- ## Typography N/A — test harness, no new UI. Typography tokens exist in `tokens.css`; the harness does not assert on font metrics unless a visible-text / accessible-name check requires it (captured in Assertion Contract below). --- ## Color N/A — test harness, no new UI. The 60/30/10 color split is declared in `tokens.css`. The harness does not assert computed colors — color drift is out of scope and prone to rendering-pipeline variance. --- ## Copywriting Contract Copywriting that the harness **must** be able to locate by text in assertions. These are the exact strings emitted by the existing components; the harness uses them as stable locator anchors. | Element | Exact String | Source Component | |---------|-------------|------------------| | Calendar empty-state heading | `Nothing here` | `EmptyState.tsx` | | Calendar empty-state body | `No events in this period. Try a different date or switch views.` | `EmptyState.tsx` | | Lists empty-state heading | `No lists yet` | `ListsEmptyState.tsx` | | Lists empty-state body | `Tap + to create your first shared list` (contains) | `ListsEmptyState.tsx` | | Calendar error heading | `Couldn't load events` | `CalendarShell.tsx` | | Calendar error CTA | `Retry` (button text) | `CalendarShell.tsx` | | New Event FAB | `aria-label="New Event"` | `CalendarShell.tsx` | | Bottom nav — Calendar tab | `aria-label="Calendar"` | `BottomTabBar.tsx` | | Bottom nav — Lists tab | `aria-label="Lists"` | `BottomTabBar.tsx` | | Top nav (phone) | `FamilySync` (visible text) | `AppNav.tsx` — `PhoneNav` | | Settings button | `aria-label` contains `open settings` | `AppNav.tsx` — `PhoneNav` | > Stable copywriting anchor rule: **always locate interactive elements by > `aria-label` or `role` + accessible name first.** Text-content locators > (`getByText`) are second resort — acceptable for static headings/bodies > that have no ARIA role. --- ## Registry Safety N/A — test harness, no new UI components. `@playwright/test` is a new dev dependency in `apps/pwa`; it is the official Playwright package from the Playwright team and requires no safety vetting under this gate. --- ## Assertion Contract This section is the primary deliverable for Phase 7. It replaces the design-system sections of the standard template with the measurable quality-bar rules that the harness enforces. ### Device / Viewport Matrix | Profile ID | Playwright Descriptor | Engine | Viewport | UA Type | |---|---|---|---|---| | `iphone` | `'iPhone 14'` | WebKit | 390×844 logical px | Mobile Safari | | `pixel` | `'Pixel 7'` | Chromium | 412×915 logical px | Chrome Android | **Source:** D-03 (iPhone + Pixel matrix), D-04 (WebKit for iPhone, Chromium for Pixel). These are the exact Playwright device descriptor strings to pass to `devices['iPhone 14']` and `devices['Pixel 7']` in `playwright.config.ts`. Both profiles run with `serviceWorkers: 'block'` (D-02 / Pitfall 15) and `DEV_AUTH_BYPASS=true` (D-01 / Pitfall 14). No `storageState` file. **CI note:** Both engines must be installed in the Phase 8 CI image. The harness adds WebKit beyond the existing global `playwright-cli` (Chromium only). Accept the larger CI image cost — this was a deliberate call (D-04, 07-CONTEXT.md §Specifics). --- ### Rule 1 — Touch-Target Minimum **Threshold:** Every interactive element (button, link, `role="button"`) must have a computed bounding box of **≥ 44 × 44 logical pixels**. **Basis:** - Apple Human Interface Guidelines: minimum touch target 44×44 pt. - WCAG 2.5.5 (Level AAA): minimum 44×44 CSS px. - The existing codebase declares this as a hard constraint: `BottomTabBar` uses `minHeight: '44px'`; `AppNav` `PhoneNav` settings button uses `minWidth: '44px', minHeight: '44px'`; calendar FAB is `56×56px`; Retry button uses `minHeight: '44px'`; nav links use `minHeight: '44px'`. **Measurement approach:** ```typescript // Use boundingBox() on the element handle, not CSS-declared values. const box = await element.boundingBox() expect(box!.width).toBeGreaterThanOrEqual(44) expect(box!.height).toBeGreaterThanOrEqual(44) ``` **What counts as an interactive target:** - `