From 179766d44742ba1ada406f0dcab4741f238c0240 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Wed, 10 Jun 2026 22:11:18 -0400 Subject: [PATCH] docs(07): mobile UI quality-bar contract (UI-SPEC) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 07-UI-SPEC.md for Phase 7 (Mobile Test Harness). Documents the assertable mobile-UI quality bar the harness enforces: touch-target thresholds (≥44px), no-horizontal-overflow rule, two-profile viewport matrix (iPhone 14/WebKit + Pixel 7/Chromium), role-based accessible name requirements, empty/error-state copy anchors, screenshot omission rationale, and CI portability rules. Locks D-01 through D-10 from CONTEXT.md as the assertion contract. --- .../07-mobile-test-harness/07-UI-SPEC.md | 388 ++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 .planning/phases/07-mobile-test-harness/07-UI-SPEC.md diff --git a/.planning/phases/07-mobile-test-harness/07-UI-SPEC.md b/.planning/phases/07-mobile-test-harness/07-UI-SPEC.md new file mode 100644 index 0000000..fad46c8 --- /dev/null +++ b/.planning/phases/07-mobile-test-harness/07-UI-SPEC.md @@ -0,0 +1,388 @@ +--- +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:** +- `