diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 57d48e2..40da09d 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -253,8 +253,8 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem - Desktop WebKit is optional — the Apple member is already covered on mobile Safari via `iphone`; Desktop Chrome is likely sufficient for a shared/wall browser. **Plans**: 1 plan - Plans: + - [ ] 14-01-PLAN.md — Add the `desktop` Playwright project, desktop-skip the two mobile-only layout assertions (+ D-04 parity), update spec/README docs, and prove `pnpm test:e2e` is green on iphone + pixel + desktop with a blocking CI gate. **UI hint**: no @@ -474,6 +474,7 @@ Plans: **The footgun:** branch protection currently requires three contexts — `CI / fast-checks`, `CI / api`, `CI / harness`. The naive fix (`paths-ignore` on the workflow, or path-filtering `harness`) **deadlocks**: on a docs-only PR the required `harness`/`api` contexts never report, so the PR can never merge. A required check that never reports is worse than a slow one. **Solution (Option A — aggregate gate):** + - Add a cheap `changes` detector job: `git diff --name-only base...HEAD`, set `code=true` unless every changed path matches `docs/` or `*.md`. - Gate the heavy jobs: `api` and `harness` get `needs: changes` + `if: needs.changes.outputs.code == 'true'`. - Add an always-running `gate` job: `needs: [fast-checks, api, harness]`, `if: always()`, passes when each dependency `result` is `success` OR `skipped`. diff --git a/.planning/STATE.md b/.planning/STATE.md index 9d12dc4..fc7d750 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,10 +2,10 @@ gsd_state_version: 1.0 milestone: v1.1 milestone_name: Operability & Polish -status: verifying +status: executing stopped_at: Phase 14 context gathered -last_updated: "2026-06-12T11:57:10.401Z" -last_activity: 2026-06-12 +last_updated: "2026-06-12T12:09:50.254Z" +last_activity: 2026-06-12 -- Phase 14 planning complete progress: total_phases: 18 completed_phases: 3 @@ -27,8 +27,8 @@ See: .planning/PROJECT.md (updated 2026-06-10) Phase: 999.1 Plan: Not started -Status: Phase complete — ready for verification -Last activity: 2026-06-12 +Status: Ready to execute +Last activity: 2026-06-12 -- Phase 14 planning complete ## Performance Metrics diff --git a/.planning/intel/API-SURFACE.md b/.planning/intel/API-SURFACE.md index 9f361d8..67ea90c 100644 --- a/.planning/intel/API-SURFACE.md +++ b/.planning/intel/API-SURFACE.md @@ -2,6 +2,8 @@ > Generated from `.planning/intel/api-map.json`. Do not edit by hand. +> **Warning:** api-map.json is stale (>24 hours old). Data below may be out of date. + ## `GET /health` - **method:** GET diff --git a/.planning/phases/14-desktop-e2e-coverage/14-PATTERNS.md b/.planning/phases/14-desktop-e2e-coverage/14-PATTERNS.md new file mode 100644 index 0000000..ee2696c --- /dev/null +++ b/.planning/phases/14-desktop-e2e-coverage/14-PATTERNS.md @@ -0,0 +1,252 @@ +# Phase 14: Desktop E2E Coverage - Pattern Map + +**Mapped:** 2026-06-12 +**Files analyzed:** 5 modified files +**Analogs found:** 5 / 5 (all files are self-analogs — modifications to existing code) + +--- + +## File Classification + +| Modified File | Role | Data Flow | Closest Analog | Match Quality | +|---|---|---|---|---| +| `apps/pwa/playwright.config.ts` | config | N/A | itself (iphone/pixel project entries) | exact | +| `apps/pwa/e2e/layout.spec.ts` | test | request-response | itself (existing describe/skip patterns) | exact | +| `apps/pwa/e2e/calendar.spec.ts` | test | request-response | itself (test.skip at line 59 is live reference) | exact | +| `apps/pwa/e2e/lists.spec.ts` | test | request-response | itself (all tests pass unchanged on desktop) | exact | +| `apps/pwa/e2e/README.md` | docs | N/A | itself | exact | + +--- + +## Pattern Assignments + +### `apps/pwa/playwright.config.ts` — add `desktop` project entry + +**Analog:** existing `iphone` and `pixel` project entries (lines 35–54) + +**Current `projects` array** (lines 35–55 — mirror this shape exactly for the third entry): +```typescript +projects: [ + { + // iPhone 14: 390×844 viewport, WebKit engine, Mobile Safari UA, hasTouch: true + name: 'iphone', + use: { + ...devices['iPhone 14'], + serviceWorkers: 'block', + }, + }, + { + // Pixel 7: 412×915 viewport, Chromium engine, Chrome Android UA, hasTouch: true + name: 'pixel', + use: { + ...devices['Pixel 7'], + serviceWorkers: 'block', + }, + }, +], +``` + +**New `desktop` entry to add** (append after the `pixel` entry, before the closing `]`): +```typescript +{ + // Desktop Chrome: 1280×720 viewport, Chromium engine, no hasTouch (D-06) + name: 'desktop', + use: { + ...devices['Desktop Chrome'], + serviceWorkers: 'block', + }, +}, +``` + +Key invariants to preserve (from file header comments): +- `serviceWorkers: 'block'` — same as both mobile profiles; mandatory on every project (D-02/Pitfall 15) +- `devices['Desktop Chrome']` — provides 1280×720, no `hasTouch`, desktop UA, Chromium engine +- No `baseURL` override in the project entry — it is inherited from the top-level `use` block (line 29) +- No `webServer` change — shared block at lines 57–64 applies to all projects automatically + +--- + +### `apps/pwa/e2e/layout.spec.ts` — spec-compat pass (primary target) + +**Self-analog** — read carefully; two `test.describe` blocks need desktop skip guards, one test needs a desktop parity assertion, one test passes unchanged. + +#### Skip mechanism in the existing codebase + +The one existing `test.skip` in the suite (from `calendar.spec.ts` lines 59–62) uses the inline conditional form: +```typescript +test.skip( + !swAvailable, + 'navigator.serviceWorker is unavailable in this context (e.g. WebKit over http://localhost) — block is unobservable here', +); +``` + +This form — `test.skip(condition, reason)` called at the top of the test body — is the established pattern. Do **not** use `test.skip(testInfo.project.name === 'desktop', ...)` with a `testInfo` parameter; the simpler form without `testInfo` is consistent with what already exists. + +For project-name gating the correct signature requires the `testInfo` fixture: +```typescript +test('...', async ({ page }, testInfo) => { + test.skip(testInfo.project.name === 'desktop', 'reason string'); + // ...rest of test +}); +``` + +Use this signature for every mobile-only test that needs a desktop skip. Keep the reason string explicit and factual (consistent with the SW-block skip's style). + +#### Tests in `layout.spec.ts` that need a desktop skip + +**Test at line 62 — "BottomTabBar is fully in-viewport (Rule 3 — safe-area-inset)":** +```typescript +test('BottomTabBar is fully in-viewport (Rule 3 — safe-area-inset)', async ({ page }) => { + // The bar uses env(safe-area-inset-bottom, 0px). In emulation there is no + // safe-area-inset, so the bar's bottom edge must be ≤ viewport height. + const nav = page.getByRole('navigation', { name: 'Main navigation' }); + await expect(nav).toBeVisible(); + const box = await nav.boundingBox(); + // ... +``` +On desktop `BottomTabBar.tsx:53-57` returns `null` at ≥768px, so this `nav` would resolve to DesktopNav sidebar and the safe-area-inset assertion is semantically wrong for a sidebar. **Skip on desktop.** + +**Test at line 130 — "BottomTabBar is fully in-viewport on /lists (Rule 3)"** — same reason. **Skip on desktop.** + +**Test at line 91 — "New Event FAB meets 56×56px touch-target minimum (Rule 1)":** +```typescript +test('New Event FAB meets 56×56px touch-target minimum (Rule 1)', async ({ page }) => { + // Phone-only FAB — aria-label="New Event", fixed 56×56px (CalendarShell.tsx) + const fab = page.getByRole('button', { name: 'New Event' }); + const box = await fab.boundingBox(); + expect(box, 'New Event FAB bounding box must not be null').not.toBeNull(); + expect(box!.width, 'New Event FAB width ≥ 56px').toBeGreaterThanOrEqual(56); + expect(box!.height, 'New Event FAB height ≥ 56px').toBeGreaterThanOrEqual(56); +}); +``` +On desktop `getByRole('button', { name: 'New Event' })` resolves to the **desktop toolbar button** (CalendarShell.tsx:436–457), not the 56×56px FAB. The toolbar button has `minHeight: 44px` but no 56px constraint. **Skip the FAB-geometry (56×56) assertion on desktop.** Per D-04, add a desktop parity block asserting ≥44px instead. + +Desktop parity assertion to add (new test or a conditional branch in the same test): +```typescript +// Desktop parity: toolbar "New Event" button meets ≥44px minimum (D-04) +// CalendarShell.tsx:443 sets minHeight:'44px' on the desktop toolbar button. +const toolbarBtn = page.getByRole('button', { name: 'New Event' }); +const box = await toolbarBtn.boundingBox(); +expect(box, 'New Event toolbar button bounding box must not be null').not.toBeNull(); +expect(box!.height, 'New Event toolbar button height ≥ 44px (Rule 1 desktop parity)').toBeGreaterThanOrEqual(44); +``` + +#### Tests that pass unchanged on desktop (no modification needed) + +- **"BottomTabBar navigation landmark is visible" (lines 35–41 and 108–110):** `getByRole('navigation', { name: 'Main navigation' })` resolves to DesktopNav sidebar on desktop (sole nav landmark at ≥768px). Passes as-authored. +- **"Calendar tab meets 44×44px" / "Lists tab meets 44×44px" (lines 43–60, 112–128):** Scoped to the `Main navigation` landmark; DesktopNav sidebar links have `minHeight: 44px` (AppNav.tsx:151). Pass as-authored. +- **"PhoneNav header is visible" (line 76):** `getByText('FamilySync', { exact: true })` matches the DesktopNav title text (AppNav.tsx:181). Passes on desktop. Verify no strict-mode collision (CONTEXT.md confirms the PhoneNav `
` returns null at ≥768px, leaving the DesktopNav title as the sole match). +- **"PhoneNav settings button meets 44×44px" (line 82):** `getByRole('button', { name: /open settings/i })` is present on both PhoneNav and DesktopNav. Passes as-authored. +- **Rule 2 overflow tests (lines 142–165):** Purely DOM measurement. Pass unchanged on desktop. +- **Harness self-validation injected-defect proofs (lines 175–256):** Use `nav[aria-label="Main navigation"]` CSS selector and body width injection. Pass unchanged on desktop. + +#### `describe` block header comments to update + +The jsdoc block at the top of `layout.spec.ts` (lines 1–25) currently says: +``` + * Runs on both device profiles automatically (playwright.config.ts matrix): + * iphone: iPhone 14 / WebKit / 390×844 + * pixel: Pixel 7 / Chromium / 412×915 +``` +Update to list all three profiles (same update applies to all spec file headers). + +--- + +### `apps/pwa/e2e/calendar.spec.ts` — no structural changes needed + +All tests in this file pass unchanged on desktop: + +- **Auth-bypass precondition (line 26):** waits for `Main navigation` landmark — resolves to DesktopNav sidebar on desktop. Passes. +- **SW-block precondition (line 42):** already uses `test.skip(!swAvailable, ...)` — self-healing conditional. No change. +- **Populated state tests (lines 81–111):** `.sx-react-calendar-wrapper` and `getByText('Seeded Test Event')` are not viewport-dependent. Pass unchanged. +- **Error state tests (lines 116–178):** `page.route` + heading/button assertions are not viewport-dependent. Pass unchanged. + +Only the file's header comment block (lines 12–15) needs updating to list the `desktop` project. + +--- + +### `apps/pwa/e2e/lists.spec.ts` — no structural changes needed + +All tests pass unchanged on desktop — no mobile-only assumptions anywhere in this file. Only the header comment (lines 12–15) needs updating to list the `desktop` project. + +--- + +### `apps/pwa/e2e/README.md` — docs update only + +**Current run commands block (lines 37–47):** +```bash +# Full suite — both iPhone (WebKit) and Pixel (Chromium) profiles +pnpm --filter @familysync/pwa test:e2e + +# Single profile (faster local iteration) +pnpm --filter @familysync/pwa exec playwright test --project=pixel + +# Headed (local debug — shows the browser) +pnpm --filter @familysync/pwa exec playwright test --headed + +# UI mode (interactive test explorer) +pnpm --filter @familysync/pwa test:e2e:ui +``` + +Update the comment on the full-suite command and add a desktop-specific example: +```bash +# Full suite — iPhone (WebKit), Pixel (Chromium), Desktop Chrome profiles +pnpm --filter @familysync/pwa test:e2e + +# Single profile (faster local iteration) +pnpm --filter @familysync/pwa exec playwright test --project=pixel +pnpm --filter @familysync/pwa exec playwright test --project=desktop +``` + +Also update the preamble sentence (line 3) which currently says "mobile-emulated (iPhone 14/WebKit + Pixel 7/Chromium)" — add "Desktop Chrome (1280×720)". + +--- + +## Shared Patterns + +### Project-name conditional skip +**Source:** `apps/pwa/e2e/calendar.spec.ts` lines 59–62 (inline `test.skip` form) +**Apply to:** every mobile-only test in `layout.spec.ts` + +Pattern — add as the **first statement** in the test body, before any `await`: +```typescript +test('test name', async ({ page }, testInfo) => { + test.skip(testInfo.project.name === 'desktop', 'reason: mobile-only assertion (describe which element is absent on desktop)'); + // existing test body unchanged below + ... +}); +``` + +### Nav landmark locator (works on all three profiles) +**Source:** `apps/pwa/e2e/layout.spec.ts` lines 45, 65; `calendar.spec.ts` lines 31, 78 +**Apply to:** any new desktop assertion that needs auth-ready confirmation +```typescript +const nav = page.getByRole('navigation', { name: 'Main navigation' }); +await expect(nav).toBeVisible(); +``` +On mobile: resolves to `BottomTabBar` nav. On desktop: resolves to `DesktopNav` sidebar. Same locator, different element — no conditional needed. + +### Desktop "New Event" button locator (CalendarShell.tsx:436–457) +**Source:** `CalendarShell.tsx` lines 424–459 + +The desktop toolbar button is rendered inside `{!phone && (...)}` with plain text `New Event` (no `aria-label` attribute). Playwright resolves it by accessible name from inner text: +```typescript +// Resolves to desktop toolbar button at ≥768px (has minHeight:44px per line 443) +// Resolves to phone FAB at <768px (has aria-label="New Event" per line 466) +page.getByRole('button', { name: 'New Event' }) +``` +At 1280px (`Desktop Chrome`) only the toolbar button renders; the FAB is in `{phone && (...)}` which is false. **No strict-mode collision.** + +--- + +## No Analog Found + +None. All files being modified are established; all new code copies directly from existing patterns in the same files. + +--- + +## Metadata + +**Analog search scope:** `apps/pwa/playwright.config.ts`, `apps/pwa/e2e/`, `apps/pwa/src/components/CalendarShell.tsx` +**Files scanned:** 6 +**Pattern extraction date:** 2026-06-12