Merge pull request 'Phase 14: desktop e2e coverage (add blocking Desktop Chrome Playwright gate)' (#10) from gsd/phase-14-desktop-e2e-coverage into main
Publish / publish (push) Successful in 19s

Reviewed-on: #10
This commit was merged in pull request #10.
This commit is contained in:
2026-06-12 09:32:26 -04:00
16 changed files with 1140 additions and 37 deletions
+2 -2
View File
@@ -252,7 +252,7 @@ jobs:
# CI=true makes Playwright start Vite :5173 itself (reuseExistingServer=false), use
# retries:2/workers:1, and apply reporter:'github' — which --reporter=list,html overrides
# because Gitea does not render github annotations (Pitfall 5 / D-06). Both projects run.
- name: Run harness (start API + Playwright iphone + pixel)
- name: Run harness (start API + Playwright iphone + pixel + desktop)
env:
CI: 'true'
# Use 127.0.0.1 (not localhost): the runner image resolves `localhost` to ::1 first,
@@ -284,7 +284,7 @@ jobs:
done
echo "API ready at :3000"
# Run the Phase 7 harness across both profiles; preserve its exit code, always kill the API.
# Run the Phase 7/14 harness across all three profiles (iphone, pixel, desktop); preserve its exit code, always kill the API.
# Call the pwa test:e2e script DIRECTLY (single pnpm layer) and append --reporter without a
# `--` separator: `pnpm <root> test:e2e -- <args>` double-forwards the `--` into
# `playwright test -- <args>`, where playwright treats --reporter as a test-file filter →
+9 -4
View File
@@ -32,7 +32,7 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
- [ ] **Phase 11: Per-Event Reminders** - Reminder selector on the event form (incl. "None") serialized as VALARM, with a variable-lead scheduler that honors each event's choice
- [ ] **Phase 12: Initial Setup Wizard** - First-run validated bootstrap of env/VAPID/DB/OIDC + first app password, reusing the admin route surface
- [x] **Phase 13: Real Lint Gate (ESLint)** - Wire ESLint flat config (typescript-eslint + React) across both apps so the Phase 8 CI lint slot actually fails on violations instead of no-op'ing (completed 2026-06-12)
- [ ] **Phase 14: Desktop E2E Coverage** - Add a Desktop Chrome Playwright profile + make the mobile-authored specs desktop-safe so the Phase 8 regression gate validates desktop, not just mobile
- [x] **Phase 14: Desktop E2E Coverage** - Add a Desktop Chrome Playwright profile + make the mobile-authored specs desktop-safe so the Phase 8 regression gate validates desktop, not just mobile (completed 2026-06-12)
## Phase Details
@@ -252,7 +252,11 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
- The real work is the spec-compat pass, not CI plumbing — Phase 8 reused the Phase 7 harness unchanged, so the config addition is small but specs authored for touch/mobile need per-spec review.
- 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**: TBD
**Plans**: 1 plan
Plans:
- [x] 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
## Progress
@@ -272,7 +276,7 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
| 11. Per-Event Reminders | v1.1 | 0/? | Not started | - |
| 12. Initial Setup Wizard | v1.1 | 0/? | Not started | - |
| 13. Real Lint Gate (ESLint) | v1.1 | 3/3 | Complete | 2026-06-12 |
| 14. Desktop E2E Coverage | v1.1 | 0/? | Not started | - |
| 14. Desktop E2E Coverage | v1.1 | 1/1 | Complete | 2026-06-12 |
## Backlog
@@ -280,7 +284,7 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
**Goal:** [Captured for future planning] Abstract the calendar backend behind a provider interface so Fastmail/CalDAV is one implementation among potentially many. Shipping with a single provider is fine, but the broker, sync, and event-expansion layers should be structured so additional providers (e.g. other CalDAV hosts, Google Calendar, generic ICS feeds) can be added without rework. Captures the "provider" seam as an explicit architectural concern.
**Requirements:** TBD
**Plans:** 3/4 plans executed
**Plans:** 1/1 plans complete
Plans:
@@ -470,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`.
+15 -14
View File
@@ -2,16 +2,16 @@
gsd_state_version: 1.0
milestone: v1.1
milestone_name: Operability & Polish
status: verifying
stopped_at: Phase 13 Plan 03 complete (Prettier reformat + CI format gate + green baseline)
last_updated: "2026-06-12T00:59:51.634Z"
status: executing
stopped_at: Phase 14 context gathered
last_updated: "2026-06-12T13:10:04.656Z"
last_activity: 2026-06-12
progress:
total_phases: 17
completed_phases: 3
total_plans: 11
completed_plans: 11
percent: 18
total_phases: 18
completed_phases: 4
total_plans: 12
completed_plans: 12
percent: 22
---
# Project State
@@ -21,20 +21,20 @@ progress:
See: .planning/PROJECT.md (updated 2026-06-10)
**Core value:** One color-coded family calendar (shared + personal) and shared lists from a single low-friction PWA — cross-ecosystem, no app store
**Current focus:** Phase 13real-lint-gate-eslint
**Current focus:** Phase 14desktop-e2e-coverage
## Current Position
Phase: 999.1
Plan: Not started
Status: Phase complete — ready for verification
Status: Executing Phase 14
Last activity: 2026-06-12
## Performance Metrics
**Velocity:**
- Total plans completed: 24
- Total plans completed: 25
- Average duration: -
- Total execution time: 0 hours
@@ -46,6 +46,7 @@ Last activity: 2026-06-12
| 03 | 12 | - | - |
| 07 | 4 | - | - |
| 13 | 3 | - | - |
| 14 | 1 | - | - |
**Recent Trend:**
@@ -209,9 +210,9 @@ Recent decisions affecting current work:
## Session Continuity
Last session: 2026-06-12T00:44:39.612Z
Stopped at: Phase 13 Plan 03 complete (Prettier reformat + CI format gate + green baseline)
Resume file: None
Last session: 2026-06-12T11:57:10.391Z
Stopped at: Phase 14 context gathered
Resume file: .planning/phases/14-desktop-e2e-coverage/14-CONTEXT.md
## Operator Next Steps
+2
View File
@@ -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
@@ -0,0 +1,252 @@
---
phase: 14-desktop-e2e-coverage
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- apps/pwa/playwright.config.ts
- apps/pwa/e2e/layout.spec.ts
- apps/pwa/e2e/calendar.spec.ts
- apps/pwa/e2e/lists.spec.ts
- apps/pwa/e2e/README.md
autonomous: true
requirements: [SC-1, SC-2, SC-3]
must_haves:
truths:
- "A `desktop` Playwright project (Desktop Chrome, 1280×720, no hasTouch) exists in playwright.config.ts (SC-1, D-06)"
- "`pnpm --filter @familysync/pwa test:e2e` exits 0 on all three projects: iphone, pixel, desktop (SC-2)"
- "The two mobile-only assertions (BottomTabBar safe-area in-viewport, 56×56 FAB geometry) are skipped on desktop only, not deleted, and still run on iphone/pixel (SC-2, D-01, D-02)"
- "Cross-cutting tests (no-overflow, nav-landmark visible, tap targets ≥44px, populated/empty/error states) run unchanged on desktop (SC-2, D-04)"
- "The desktop gate is blocking — same severity as mobile, no continue-on-error carve-out (SC-3, D-03)"
artifacts:
- path: "apps/pwa/playwright.config.ts"
provides: "desktop project entry mirroring iphone/pixel"
contains: "name: 'desktop'"
- path: "apps/pwa/e2e/layout.spec.ts"
provides: "desktop-skip guards on the two mobile-only tests + optional D-04 parity assertion"
contains: "testInfo.project.name === 'desktop'"
- path: "apps/pwa/e2e/README.md"
provides: "harness docs listing the desktop profile"
contains: "desktop"
key_links:
- from: "apps/pwa/playwright.config.ts"
to: "devices['Desktop Chrome']"
via: "spread into the desktop project use block"
pattern: "devices\\['Desktop Chrome'\\]"
- from: "apps/pwa/e2e/layout.spec.ts"
to: "testInfo.project.name"
via: "test.skip first-statement guard keyed on the desktop project name"
pattern: "test\\.skip\\(\\s*testInfo\\.project\\.name === 'desktop'"
---
<objective>
Add a third `desktop` Playwright project (Desktop Chrome, 1280×720, no `hasTouch`) to
`apps/pwa/playwright.config.ts` and make the existing Phase 7 mobile-authored e2e specs
green on that no-touch desktop viewport — so the Phase 8 CI regression gate validates the
desktop layout/flows in addition to the `iphone`/`pixel` mobile profiles.
The real work is the spec-compat pass, not CI plumbing. Phase 8's harness job already runs
`pnpm --filter @familysync/pwa test:e2e`, which executes every configured project; the new
`desktop` project is picked up automatically. `Desktop Chrome` uses the Chromium engine
already installed in CI (`playwright install --with-deps webkit chromium`), so no browser
install or CI plumbing change is required.
Purpose: Earn a desktop-layout regression gate that is real (blocking, deterministic) without
duplicating the shared cross-cutting assertions (D-01) or weakening the mobile assertions (D-02).
Output: an extended `playwright.config.ts`, desktop-skip-guarded `layout.spec.ts`, header-comment
updates across the spec files, and an updated `e2e/README.md`.
</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/14-desktop-e2e-coverage/14-CONTEXT.md
@.planning/phases/14-desktop-e2e-coverage/14-PATTERNS.md
@apps/pwa/playwright.config.ts
@apps/pwa/e2e/layout.spec.ts
@apps/pwa/e2e/calendar.spec.ts
@apps/pwa/e2e/lists.spec.ts
@apps/pwa/e2e/README.md
@apps/pwa/e2e/global-setup.ts
</context>
<tasks>
<task type="execute">
<name>Task 1: Add the `desktop` project to playwright.config.ts (SC-1, D-06)</name>
<files>apps/pwa/playwright.config.ts</files>
<read_first>
- apps/pwa/playwright.config.ts — the file being modified; mirror the existing `iphone` (lines 36-46) and `pixel` (lines 47-54) project entries exactly.
- .planning/phases/14-desktop-e2e-coverage/14-PATTERNS.md §"apps/pwa/playwright.config.ts — add `desktop` project entry" (the exact new-entry shape and the four key invariants).
- .planning/phases/14-desktop-e2e-coverage/14-CONTEXT.md D-06 (single Desktop Chrome project, no Desktop WebKit, keep the default 1280×720 viewport).
</read_first>
<action>
Append a third project object to the `projects` array (after the `pixel` entry, before the closing `]`):
`name: 'desktop'` with `use: { ...devices['Desktop Chrome'], serviceWorkers: 'block' }`. Mirror the
iphone/pixel shape exactly. Do NOT add a `baseURL` override (inherited from the top-level `use` block,
line 29). Do NOT touch the shared `webServer` block (lines 57-64) or `globalSetup`. Keep `serviceWorkers:
'block'` — mandatory on every project per D-02/Pitfall 15. Do NOT add Desktop WebKit (D-06). Keep the
`devices['Desktop Chrome']` default viewport (1280×720, no `hasTouch`); no custom width override.
Update the file header jsdoc (lines 4-12) so the device-matrix line names all three profiles
(iphone / pixel / desktop) and the example run-commands block reflects a desktop example.
</action>
<verify>
<automated>cd apps/pwa && grep -q "name: 'desktop'" playwright.config.ts && grep -q "devices\['Desktop Chrome'\]" playwright.config.ts && grep -q "serviceWorkers: 'block'" playwright.config.ts && npx playwright test --list --project=desktop >/dev/null 2>&1 && echo OK</automated>
</verify>
<acceptance_criteria>
- `apps/pwa/playwright.config.ts` contains the literal `name: 'desktop'`.
- The desktop entry spreads `devices['Desktop Chrome']` and sets `serviceWorkers: 'block'`.
- No `baseURL` key appears inside the desktop project's `use` block (it is inherited).
- `npx playwright test --list --project=desktop` exits 0 (the project resolves and tests enumerate) — run from apps/pwa.
- The file header jsdoc lists three profiles, not two.
</acceptance_criteria>
<done>The `desktop` project exists, resolves via `--project=desktop`, and mirrors the mobile entries with no baseURL/webServer duplication.</done>
</task>
<task type="execute">
<name>Task 2: Desktop-skip the two mobile-only layout assertions + add D-04 parity (SC-2, D-01, D-02, D-04, D-05)</name>
<files>apps/pwa/e2e/layout.spec.ts</files>
<read_first>
- apps/pwa/e2e/layout.spec.ts — the file being modified; note the exact tests at line 62 ("BottomTabBar is fully in-viewport (Rule 3 — safe-area-inset)"), line 91 ("New Event FAB meets 56×56px touch-target minimum (Rule 1)"), and line 130 ("BottomTabBar is fully in-viewport on /lists (Rule 3)").
- apps/pwa/e2e/calendar.spec.ts lines 56-62 — the established inline `test.skip(condition, reason)` first-statement pattern (the live skip reference in this suite).
- .planning/phases/14-desktop-e2e-coverage/14-PATTERNS.md §"apps/pwa/e2e/layout.spec.ts — spec-compat pass" (which tests skip, which pass unchanged, the D-04 parity block, and the testInfo signature) + §"Shared Patterns — Project-name conditional skip".
- .planning/phases/14-desktop-e2e-coverage/14-CONTEXT.md D-01 (one spec set, no desktop.spec.ts), D-02 (scope/skip, never weaken or delete mobile assertions), D-04 (parity-adapted: desktop toolbar button ≥44px), D-05 (do NOT add "mobile chrome absent on desktop" negative assertions — `New Event` is a shared accessible name).
</read_first>
<action>
Add a desktop-only skip guard to exactly the two mobile-only tests, plus the FAB test, using the
first-statement form `test.skip(testInfo.project.name === 'desktop', '<factual reason>')` with the
`testInfo` fixture added as the second body argument (`async ({ page }, testInfo) => {`):
(a) the line-62 "BottomTabBar is fully in-viewport (Rule 3 — safe-area-inset)" test — reason: on desktop
the `Main navigation` landmark resolves to the DesktopNav sidebar, for which the safe-area-inset
bottom-edge assertion is semantically wrong;
(b) the line-130 "BottomTabBar is fully in-viewport on /lists (Rule 3)" test — same reason;
(c) the line-91 "New Event FAB meets 56×56px" test — reason: on desktop `New Event` resolves to the
toolbar button (CalendarShell.tsx:436-457), not the 56×56 FAB.
For D-04 parity, add a desktop toolbar-button ≥44px height assertion using
`page.getByRole('button', { name: 'New Event' })` and `boundingBox()`; place it so it runs ONLY on
desktop (e.g. a sibling test guarded by `test.skip(testInfo.project.name !== 'desktop', ...)`, or a
conditional desktop branch) — keep it from running on mobile where that name is the FAB.
Do NOT add any negative "mobile chrome absent" assertion (D-05). Do NOT modify the nav-landmark-visible,
44×44 tab-target, PhoneNav-header, settings-button, Rule 2 overflow, or harness self-validation tests —
per PATTERNS they pass unchanged on desktop (the landmark resolves to the sidebar, sidebar links have
minHeight:44px, the DesktopNav title matches `getByText('FamilySync', { exact: true })`). Do NOT weaken
or delete any existing mobile assertion (D-02). Update the file header jsdoc (lines 10-12) to list all
three profiles.
</action>
<verify>
<automated>cd apps/pwa && grep -c "testInfo.project.name === 'desktop'" e2e/layout.spec.ts | grep -qx 3 && grep -q "test.skip(\s*testInfo.project.name !== 'desktop'" e2e/layout.spec.ts && echo OK</automated>
</verify>
<acceptance_criteria>
- `e2e/layout.spec.ts` contains exactly three `test.skip(testInfo.project.name === 'desktop', ...)` guards (the two safe-area tests + the FAB test).
- A desktop parity assertion exists that checks `New Event` toolbar button height ≥ 44px and runs only on desktop (guarded by `testInfo.project.name !== 'desktop'` skip or equivalent desktop-only branch).
- No existing mobile assertion is removed or weakened: the file still contains the `>= 56` FAB-width/height checks and the `>= 44` tab-target checks (grep for `toBeGreaterThanOrEqual(56)` and `toBeGreaterThanOrEqual(44)` both still present).
- No `desktop.spec.ts` file is created (D-01).
- No negative "absent on desktop" assertion is added (D-05) — no new `toHaveCount(0)` / `not.toBeVisible()` keyed on the `New Event` name.
</acceptance_criteria>
<done>layout.spec.ts skips exactly the three mobile-geometry tests on desktop, adds a desktop ≥44px parity assertion, preserves every mobile assertion, and stays a single spec set.</done>
</task>
<task type="execute">
<name>Task 3: Update spec headers + README, then prove the full suite is green on all three projects (SC-2, SC-3, D-03)</name>
<files>apps/pwa/e2e/calendar.spec.ts, apps/pwa/e2e/lists.spec.ts, apps/pwa/e2e/README.md</files>
<read_first>
- apps/pwa/e2e/calendar.spec.ts header (lines 13-15) and apps/pwa/e2e/lists.spec.ts header (lines 16-19) — the "Runs on both device profiles" comment blocks to update; per PATTERNS no structural test changes are needed in these files.
- apps/pwa/e2e/README.md — preamble line 3 ("mobile-emulated (iPhone 14/WebKit + Pixel 7/Chromium)") and the run-commands block lines 37-47; mirror PATTERNS §"apps/pwa/e2e/README.md — docs update only".
- .gitea/workflows/ci.yml lines 241-296 — confirm the harness job runs `pnpm --filter @familysync/pwa test:e2e` (all projects) and installs the `chromium` engine that Desktop Chrome uses; assert no CI plumbing change is required beyond a cosmetic comment.
- .planning/phases/14-desktop-e2e-coverage/14-CONTEXT.md D-03 (desktop gate is blocking; fix flaky specs, never downgrade the gate) and 14-PATTERNS.md §calendar.spec.ts / §lists.spec.ts (all tests pass unchanged on desktop).
</read_first>
<action>
Update the header jsdoc comment block in `calendar.spec.ts` and `lists.spec.ts` to list all three
profiles (iphone / pixel / desktop) — comment-only, no test-body changes (PATTERNS confirms both files
pass unchanged on desktop). In `e2e/README.md`: update the preamble (line 3) to add "Desktop Chrome
(1280×720)" alongside the iPhone/Pixel emulation note, change the full-suite command comment (line 37) to
name all three profiles, and add a `--project=desktop` single-profile example. Confirm — by reading
`.gitea/workflows/ci.yml` — that the harness job already runs every project via `test:e2e` and installs
the `chromium` engine Desktop Chrome uses; the desktop gate is therefore blocking automatically (CI fails
the PR if any desktop test fails) with NO continue-on-error carve-out, satisfying D-03. Do NOT add
`continue-on-error`. If a desktop test flakes, fix the spec (proper waits/locators), never weaken the
assertion or downgrade the gate. Then run the FULL suite locally against all three projects and confirm
exit 0; the dev stack must be up with `DEV_AUTH_BYPASS=true` per e2e/README.md before running.
</action>
<verify>
<automated>set -a; source .env 2>/dev/null; set +a; export DEV_AUTH_BYPASS=true DB_HOST=127.0.0.1 DB_PORT=3306; pnpm --filter @familysync/pwa test:e2e</automated>
</verify>
<acceptance_criteria>
- `pnpm --filter @familysync/pwa test:e2e` exits 0 with all three projects (iphone, pixel, desktop) reported — no failures, no unexpected skips beyond the three desktop-guarded geometry tests + the SW-availability skip.
- `apps/pwa/e2e/README.md` line 3 mentions Desktop Chrome, and the run-commands block contains a `--project=desktop` example.
- `calendar.spec.ts` and `lists.spec.ts` header blocks each list the `desktop` profile.
- `.gitea/workflows/ci.yml` is unchanged except (optionally) the cosmetic step-name comment on line 255 — no `continue-on-error` added, no new browser install needed (chromium already installed for Desktop Chrome).
</acceptance_criteria>
<done>The full e2e suite is green on iphone + pixel + desktop, docs name the desktop profile, and the CI gate blocks on desktop with zero plumbing change.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
This phase adds a test-only Playwright `desktop` project and adjusts existing e2e specs/docs.
It introduces NO new runtime code, NO new request handlers, NO new data flows, and NO new
attack surface in the shipped application. No new trust boundary is created.
| Boundary | Description |
|----------|-------------|
| (none new) | All changes are confined to the e2e test harness (`apps/pwa/e2e/*`, `playwright.config.ts`) and docs; nothing in this phase is reachable in the production app. |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-14-01 | Elevation of Privilege | desktop Playwright project (`serviceWorkers`, `DEV_AUTH_BYPASS`) | mitigate | The `desktop` project MUST set `serviceWorkers: 'block'` (mirrors mobile profiles) and rely ONLY on the test-only `DEV_AUTH_BYPASS` path that is a no-op when `NODE_ENV === 'production'` (devBypass.ts) and absent from `docker-compose.yml`. This phase adds no real-auth bypass and no production-reachable code. |
| T-14-02 | Tampering | npm/pip/cargo installs | accept | No new packages installed. `Desktop Chrome` uses the already-installed Chromium engine (`playwright install --with-deps webkit chromium`); no dependency or lockfile change. Package Legitimacy Gate N/A. |
| T-14-SC | Tampering | supply chain | accept | No package-manager install tasks in this plan; no `[ASSUMED]`/`[SUS]` packages to gate. |
Honest scope statement: there is no genuine new application attack surface here. The only
test-infra concern worth recording is keeping `serviceWorkers: 'block'` on the desktop project
and never letting `DEV_AUTH_BYPASS` leak into a production-reachable path — both already enforced
by existing guards (T-14-01).
</threat_model>
<verification>
- `npx playwright test --list --project=desktop` resolves the new project (run from apps/pwa).
- `pnpm --filter @familysync/pwa test:e2e` exits 0 across iphone + pixel + desktop.
- `grep -c "testInfo.project.name === 'desktop'" apps/pwa/e2e/layout.spec.ts` == 3.
- Mobile assertions intact: `grep "toBeGreaterThanOrEqual(56)" apps/pwa/e2e/layout.spec.ts` still matches the FAB checks.
- No `desktop.spec.ts` exists under `apps/pwa/e2e/`.
- `.gitea/workflows/ci.yml` has no `continue-on-error` on the harness job (desktop gate is blocking, D-03).
- `apps/pwa/e2e/README.md` documents the desktop profile.
</verification>
<success_criteria>
- SC-1: A `desktop` project (Desktop Chrome, 1280×720, no `hasTouch`) exists in `playwright.config.ts`.
- SC-2: The existing e2e specs pass (or are explicitly, justifiably skipped) on the desktop profile — the two mobile-only geometry assertions are desktop-skipped, everything else runs; `pnpm test:e2e` is green on all three projects.
- SC-3: The desktop gate is blocking in CI (no `continue-on-error`), runs automatically via the unchanged `test:e2e` invocation, and the specs are deterministic (D-03).
- D-01..D-06 all satisfied: one spec set (D-01), no weakened/deleted mobile assertions (D-02), blocking gate (D-03), parity ≥44px desktop assertion (D-04), no negative "absent on desktop" assertion (D-05), single Desktop Chrome project no WebKit (D-06).
</success_criteria>
<artifacts_produced>
## Artifacts this phase produces
New symbols / identifiers created by this phase (none are application runtime code — all test-harness):
- **`desktop` Playwright project** — new entry in `apps/pwa/playwright.config.ts` `projects` array; `name: 'desktop'`, `use: { ...devices['Desktop Chrome'], serviceWorkers: 'block' }`. Referenceable via `--project=desktop`.
- **Desktop skip guards in `apps/pwa/e2e/layout.spec.ts`** — three `test.skip(testInfo.project.name === 'desktop', '<reason>')` first-statement guards on: "BottomTabBar is fully in-viewport (Rule 3 — safe-area-inset)" (line ~62), "BottomTabBar is fully in-viewport on /lists (Rule 3)" (line ~130), "New Event FAB meets 56×56px touch-target minimum (Rule 1)" (line ~91).
- **Desktop parity assertion (D-04)** — a new desktop-only check (test or branch) asserting the `New Event` toolbar button height ≥ 44px, guarded so it runs only on the `desktop` project.
- **Header-comment updates**`layout.spec.ts`, `calendar.spec.ts`, `lists.spec.ts`, and `playwright.config.ts` jsdoc blocks updated to list all three profiles.
- **README update**`apps/pwa/e2e/README.md` preamble + run-commands block document the `desktop` profile and a `--project=desktop` example.
No new helper module (e.g. no `isDesktop` helper) is introduced — the inline `test.skip` form is used per PATTERNS.md, consistent with the existing skip pattern. No `desktop.spec.ts` file is created (D-01).
</artifacts_produced>
<output>
Create `.planning/phases/14-desktop-e2e-coverage/14-01-SUMMARY.md` when done.
</output>
@@ -0,0 +1,109 @@
---
phase: 14-desktop-e2e-coverage
plan: "01"
subsystem: e2e-test-harness
tags: [playwright, e2e, desktop, testing]
dependency_graph:
requires: [07-mobile-test-harness, 08-gitea-ci]
provides: [desktop-e2e-gate]
affects: [ci-harness-job]
tech_stack:
added: []
patterns: [playwright-project-matrix, test.skip-project-name-guard, testInfo-fixture, D-04-parity-assertion]
key_files:
created: []
modified:
- apps/pwa/playwright.config.ts
- apps/pwa/e2e/layout.spec.ts
- apps/pwa/e2e/calendar.spec.ts
- apps/pwa/e2e/lists.spec.ts
- apps/pwa/e2e/README.md
- .gitea/workflows/ci.yml
decisions:
- "D-06: Single Desktop Chrome project only (no Desktop WebKit) — Apple member covered by iphone/WebKit profile"
- "D-04 parity assertion: desktop-only test asserting New Event toolbar button height >=44px"
- "inline test.skip(testInfo.project.name === 'desktop') pattern used — consistent with calendar.spec.ts SW-block skip"
- "CI step-name comment updated cosmetically only — no plumbing change required"
metrics:
duration: "~5 minutes"
completed: "2026-06-12"
tasks_completed: 3
files_modified: 6
---
# Phase 14 Plan 01: Desktop E2E Coverage Summary
Added a third `desktop` Playwright project (Desktop Chrome, 1280x720, no hasTouch) to `playwright.config.ts` and made the existing Phase 7 mobile-authored e2e specs green on desktop by desktop-skipping three mobile-only geometry assertions and adding a D-04 parity assertion for the desktop toolbar button.
## Tasks Completed
| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | Add desktop project to playwright.config.ts | d3bc696 | apps/pwa/playwright.config.ts |
| 2 | Desktop-skip mobile-only assertions + D-04 parity | 2903599 | apps/pwa/e2e/layout.spec.ts |
| 3 | Update spec headers + README, prove full suite green | bfc49d1 | apps/pwa/e2e/calendar.spec.ts, lists.spec.ts, README.md, .gitea/workflows/ci.yml |
## What Was Built
**SC-1:** A `desktop` Playwright project entry was appended to the `projects` array in `playwright.config.ts` using `devices['Desktop Chrome']` with `serviceWorkers: 'block'`. No `baseURL` override (inherited from top-level `use` block). Header jsdoc updated to list three profiles.
**SC-2:** `layout.spec.ts` received three `test.skip(testInfo.project.name === 'desktop', reason)` guards on the mobile-only geometry tests:
- "BottomTabBar is fully in-viewport (Rule 3 — safe-area-inset)" — BottomTabBar returns null at >=768px; nav resolves to DesktopNav sidebar where safe-area-inset assertion is semantically wrong
- "BottomTabBar is fully in-viewport on /lists (Rule 3)" — same reason
- "New Event FAB meets 56x56px touch-target minimum (Rule 1)" — on desktop resolves to toolbar button (CalendarShell.tsx:436-457), not the 56px FAB
A D-04 parity test was added asserting `New Event` toolbar button height >=44px, guarded by `test.skip(testInfo.project.name !== 'desktop')` so it runs only on desktop.
All mobile assertions are preserved: `toBeGreaterThanOrEqual(56)` and `toBeGreaterThanOrEqual(44)` both still present.
`calendar.spec.ts` and `lists.spec.ts` required no structural changes — all tests pass unchanged on desktop (nav landmark resolves to DesktopNav sidebar, overflow/state assertions are viewport-agnostic).
**SC-3:** CI gate is blocking automatically. The harness job in `.gitea/workflows/ci.yml` runs `pnpm --filter @familysync/pwa test:e2e` which executes all configured projects. The `chromium` engine (used by Desktop Chrome) is already installed via `playwright install --with-deps webkit chromium`. No `continue-on-error` added. Step-name comment updated cosmetically only.
**Full suite result (local verification):** 85 passed, 5 skipped (3 desktop geometry guards + 2 non-desktop parity guards), 0 failed across iphone + pixel + desktop.
## Decisions Made
- **testInfo fixture pattern:** Used `test('name', async ({ page }, testInfo) => { test.skip(...); ... })` — consistent with the inline conditional skip in `calendar.spec.ts`
- **D-04 parity as sibling test:** Added as a new test in the same `describe` block guarded by `!== 'desktop'`, rather than a conditional branch inside the FAB test, to keep the skip reason explicit and discoverable
- **No `desktop.spec.ts`:** D-01 maintained — one spec set, no duplication
- **CI cosmetic only:** Step-name comment updated to name all three profiles; no plumbing change required
## Deviations from Plan
None — plan executed exactly as written.
## Verification Results
All success criteria met:
- `playwright.config.ts` contains `name: 'desktop'` with `devices['Desktop Chrome']` and `serviceWorkers: 'block'`
- `npx playwright test --list --project=desktop` exits 0 (30 tests enumerated)
- `grep -c "testInfo.project.name === 'desktop'" apps/pwa/e2e/layout.spec.ts` == 3
- Mobile FAB assertions intact: `toBeGreaterThanOrEqual(56)` present
- No `desktop.spec.ts` under `apps/pwa/e2e/`
- `.gitea/workflows/ci.yml` has no `continue-on-error` on the harness job
- `apps/pwa/e2e/README.md` documents Desktop Chrome and `--project=desktop`
- Full suite: 85 passed, 5 skipped (expected), 0 failed
## Known Stubs
None.
## Threat Flags
No new application runtime code introduced. All changes confined to e2e test harness and docs. `serviceWorkers: 'block'` correctly set on the desktop project (T-14-01 satisfied). No new trust boundary created.
## Self-Check: PASSED
Files exist:
- apps/pwa/playwright.config.ts: FOUND
- apps/pwa/e2e/layout.spec.ts: FOUND
- apps/pwa/e2e/calendar.spec.ts: FOUND
- apps/pwa/e2e/lists.spec.ts: FOUND
- apps/pwa/e2e/README.md: FOUND
Commits exist:
- d3bc696 (Task 1): FOUND
- 2903599 (Task 2): FOUND
- bfc49d1 (Task 3): FOUND
@@ -0,0 +1,122 @@
---
phase: 14-desktop-e2e-coverage
verified: 2026-06-12T00:00:00Z
status: passed
score: 5/5 must-haves verified
overrides_applied: 0
---
# Phase 14: Desktop E2E Coverage Verification Report
**Phase Goal:** The Phase 8 regression gate exercises the desktop layout and flows, not just mobile. A `desktop` Playwright project (`devices['Desktop Chrome']`, no touch, wide viewport) is added to `apps/pwa/playwright.config.ts`, and the existing mobile-authored specs are reviewed/adjusted (or appropriately skipped) so `pnpm test:e2e` passes on a no-touch desktop viewport as well as the `iphone`/`pixel` profiles.
**Verified:** 2026-06-12
**Status:** passed
**Re-verification:** No — initial verification
---
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | A `desktop` Playwright project (Desktop Chrome, 1280×720, no hasTouch) exists in playwright.config.ts (SC-1, D-06) | VERIFIED | `name: 'desktop'` at line 58; `...devices['Desktop Chrome']` at line 60; `serviceWorkers: 'block'` at line 61; no `baseURL` inside the desktop project block (inherited from top-level `use`); comment explicitly states "no hasTouch (D-06)" |
| 2 | `pnpm --filter @familysync/pwa test:e2e` exits 0 on all three projects: iphone, pixel, desktop (SC-2) | VERIFIED | CI run #32 (PR #10 → main): harness job ran `pnpm --filter @familysync/pwa test:e2e --reporter=list,html` with no `--project` filter — all three projects execute; SUMMARY records 85 passed / 5 skipped / 0 failed; commits d3bc696, 2903599, bfc49d1 are present in git history |
| 3 | The two mobile-only assertions (BottomTabBar safe-area in-viewport, 56×56 FAB geometry) are skipped on desktop only, not deleted, and still run on iphone/pixel (SC-2, D-01, D-02) | VERIFIED | `grep -c "testInfo.project.name === 'desktop'" layout.spec.ts` == 3 (three skip guards: line 66, 99, 161); `toBeGreaterThanOrEqual(56)` still present at lines 107108; `toBeGreaterThanOrEqual(44)` still present at lines 5051, 5960, 9495, 127, 147148, 156157, 248; no test body removed |
| 4 | Cross-cutting tests (no-overflow, nav-landmark visible, tap targets ≥44px, populated/empty/error states) run unchanged on desktop (SC-2, D-04); D-04 parity (New Event toolbar button ≥44px) asserting only on desktop | VERIFIED | D-04 parity test at lines 114128 guarded by `testInfo.project.name !== 'desktop'` (confirmed by grep count == 1); overflow, nav-landmark, calendar/lists populated/error/empty state tests have no project-name guards and run on all three profiles; `calendar.spec.ts` and `lists.spec.ts` contain no structural test changes |
| 5 | The desktop gate is blocking — no `continue-on-error` carve-out (SC-3, D-03) | VERIFIED | `grep "continue-on-error" .gitea/workflows/ci.yml` returns nothing; harness step runs `pnpm --filter @familysync/pwa test:e2e` (all projects) with no isolation or continue-on-error; Chromium engine already installed via `playwright install --with-deps webkit chromium` (line 242); CI step name updated to name all three profiles (line 255, cosmetic only) |
**Score:** 5/5 truths verified
---
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `apps/pwa/playwright.config.ts` | desktop project entry mirroring iphone/pixel | VERIFIED | `name: 'desktop'`, `...devices['Desktop Chrome']`, `serviceWorkers: 'block'`; no baseURL in project block; header jsdoc lists three profiles |
| `apps/pwa/e2e/layout.spec.ts` | desktop-skip guards on two mobile-only tests + D-04 parity assertion | VERIFIED | Exactly 3 `test.skip(testInfo.project.name === 'desktop', ...)` guards; 1 `test.skip(testInfo.project.name !== 'desktop', ...)` parity guard; all prior mobile assertions intact |
| `apps/pwa/e2e/README.md` | harness docs listing the desktop profile | VERIFIED | Line 1 preamble mentions "Desktop Chrome (1280×720)"; `--project=desktop` example at line 42; run commands block names all three profiles |
---
### Key Link Verification
| From | To | Via | Status | Details |
|------|-----|-----|--------|---------|
| `apps/pwa/playwright.config.ts` | `devices['Desktop Chrome']` | spread into desktop project `use` block | VERIFIED | Line 60: `...devices['Desktop Chrome']` |
| `apps/pwa/e2e/layout.spec.ts` | `testInfo.project.name` | `test.skip` first-statement guard keyed on `=== 'desktop'` | VERIFIED | Three guards at lines 6669, 99102, 161164; all use first-statement form inside `async ({ page }, testInfo)` |
| CI harness | all three projects | `pnpm --filter @familysync/pwa test:e2e` with no `--project` filter | VERIFIED | `.gitea/workflows/ci.yml` line 293; picks up `desktop` automatically |
---
### Data-Flow Trace (Level 4)
Not applicable. Phase produces only test-harness artifacts — no runtime components rendering dynamic data.
---
### Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| desktop project resolves in Playwright | `grep -c "name: 'desktop'" apps/pwa/playwright.config.ts` | 1 | PASS |
| Exactly 3 desktop skip guards | `grep -c "testInfo.project.name === 'desktop'" apps/pwa/e2e/layout.spec.ts` | 3 | PASS |
| D-04 parity guard present | `grep -c "testInfo.project.name !== 'desktop'" apps/pwa/e2e/layout.spec.ts` | 1 | PASS |
| Mobile 56px FAB assertions intact | `grep "toBeGreaterThanOrEqual(56)" apps/pwa/e2e/layout.spec.ts` | 2 matches (lines 107108) | PASS |
| No `desktop.spec.ts` created (D-01) | `ls apps/pwa/e2e/desktop.spec.ts` | NOT FOUND | PASS |
| No `continue-on-error` in CI harness | `grep "continue-on-error" .gitea/workflows/ci.yml` | no output | PASS |
| Commits from SUMMARY exist | `git log --oneline d3bc696 2903599 bfc49d1` | all three found in history | PASS |
---
### Probe Execution
No probes declared in PLAN or found at `scripts/*/tests/probe-*.sh`. Skipped.
---
### Requirements Coverage
| Requirement | Description | Status | Evidence |
|-------------|-------------|--------|---------|
| SC-1 | `desktop` project (Desktop Chrome, 1280×720, no `hasTouch`) exists in `playwright.config.ts` | SATISFIED | Verified at `playwright.config.ts:5663` |
| SC-2 | Existing e2e specs pass (or justifiably skipped) on desktop; `pnpm test:e2e` green on all three projects | SATISFIED | 3 mobile-geometry tests desktop-skipped with factual reasons; D-04 parity added; CI run #32 green |
| SC-3 | Desktop gate is blocking in CI (no `continue-on-error`), runs automatically, specs deterministic | SATISFIED | No `continue-on-error` in CI yml; harness runs all projects via single `test:e2e` invocation with no filter |
---
### Anti-Patterns Found
| File | Pattern | Severity | Impact |
|------|---------|----------|--------|
| `apps/pwa/e2e/layout.spec.ts:8396` | `PhoneNav header is visible` and `PhoneNav settings button` tests run unguarded on desktop, silently re-targeting DesktopNav elements (WR-01 from code review) | INFO | Tests pass by coincidence on desktop; a PhoneNav-specific regression would not be caught on the desktop profile. Not a blocker — both tests still pass and the phase goal (desktop gate green) is achieved. Flagged in 14-REVIEW.md. |
| `apps/pwa/e2e/calendar.spec.ts:16` / `lists.spec.ts:19` | Header comment claims "all tests pass unchanged" without qualifying the month-grid/DesktopNav view divergence on desktop (WR-02, WR-03 from code review) | INFO | Comment is misleading but not a correctness failure. No test is broken. |
No TBD / FIXME / XXX markers found in any phase-modified file.
---
### Human Verification Required
None. All must-haves are verifiable programmatically. CI evidence (PR #10, run #32) confirms full-suite green. No visual, real-time, or device-only checks are required to confirm the phase goal.
---
## Gaps Summary
No gaps. All five must-have truths are verified by direct codebase inspection:
- `playwright.config.ts` contains the correct `desktop` project entry with all required properties.
- `layout.spec.ts` has exactly the right guard count (3 desktop-skip, 1 desktop-only parity) and all prior mobile assertions are intact.
- No `desktop.spec.ts` was created.
- The CI harness runs all three projects without `continue-on-error`.
- README and spec headers document the desktop profile.
The code review (14-REVIEW.md) identified three warnings (WR-01 misleading test names, WR-02 month-grid claim, WR-03 comment accuracy) and three info items (IN-01 serviceWorkers hoisting, IN-02 missing auth-wait in parity test, IN-03 implicit viewport assumption). None of these affect the phase goal or the correctness of the desktop gate. They are documentation/quality items for a follow-up if desired.
---
_Verified: 2026-06-12_
_Verifier: Claude (gsd-verifier)_
@@ -0,0 +1,151 @@
# Phase 14: Desktop E2E Coverage - Context
**Gathered:** 2026-06-12
**Status:** Ready for planning
<domain>
## Phase Boundary
Add a `desktop` Playwright project (Desktop Chrome, wide viewport, no `hasTouch`) to
`apps/pwa/playwright.config.ts` and make the existing Phase 7 mobile-authored e2e specs
pass on a no-touch desktop viewport, so the Phase 8 CI regression gate validates the
**desktop** layout/flows in addition to the `iphone`/`pixel` mobile profiles.
The real work is the **spec-compat pass**, not CI plumbing — Phase 8 already runs
`pnpm test:e2e` and will pick up the new project automatically. No new test *capabilities*
are added; this is desktop-profile coverage of the layout/state bars Phase 7 already asserts.
**In scope:** desktop project config; per-spec review so the suite is green on desktop;
desktop-equivalent assertions where the mobile layout diverges.
**Out of scope:** new flows/features; live event-create against CalDAV (dev user 1 still has
no calendar credential — carried from Phase 7); Desktop WebKit; prod-SW / iOS-standalone testing.
</domain>
<decisions>
## Implementation Decisions
### Spec-Compat Strategy
- **D-01:** Keep **one set of spec files** (no separate `desktop.spec.ts`). Gate the genuinely
mobile-only tests with `test.skip()` keyed on project name (`desktop`), and add desktop-equivalent
assertions where the layout diverges. Cross-cutting tests (no-overflow, populated/empty/error
states) stay shared and run on all three profiles. Rationale: one source of truth, least
duplication, avoids drift between mobile and desktop copies of the same cross-cutting assertions.
- **D-02:** Prefer scoping/skip over deletion — do not weaken or remove the existing mobile
assertions to make desktop pass. The mobile profiles must remain exactly as authored in Phase 7.
### Desktop Gate Severity
- **D-03:** Desktop-profile failures are **blocking** — same severity as `iphone`/`pixel`.
A desktop regression must not be able to land on `main`. No `continue-on-error` / advisory
carve-out. (Resolves ROADMAP Phase 14 Success Criterion 3, which deferred blocking-vs-advisory
to planning.) Implication: the desktop specs must be deterministic before this merges —
if they flake, fix the spec, don't downgrade the gate.
### Desktop Assertion Depth
- **D-04:** **Parity-adapted** coverage. On desktop, positively assert the desktop equivalents:
DesktopNav sidebar (`nav[aria-label="Main navigation"]`) visible, nav tap targets, no horizontal
overflow, populated/empty/error states render (calendar grid, seeded event, lists card, empty
state, error heading + Retry). Skip only the assertions that have no desktop analog.
- **D-05:** Do **not** add "mobile chrome is absent on desktop" negative assertions (the rejected
"full divergence" option). Reason grounded in code: the `New Event` accessible name is **not**
mobile-only — `CalendarShell.tsx:424` renders a desktop-only toolbar "New Event" button sharing
the same name as the phone FAB. Asserting its absence would be wrong. Only the **56×56 FAB
geometry** check is mobile-specific.
### Desktop Profile / Viewport
- **D-06:** Single desktop project using `devices['Desktop Chrome']` (1280×720), **no Desktop WebKit**.
The Apple member is already covered on mobile Safari via the `iphone`/WebKit profile; Desktop
Chrome is sufficient for the shared/wall-browser use case. Keep the default viewport — no custom
width override unless planning finds the sidebar layout needs more horizontal room than 1280 gives.
### Claude's Discretion
- Exact mechanism for the skip gate (e.g. `test.skip(testInfo.project.name === 'desktop', ...)` vs.
`test.describe` blocks split by a `isDesktop` helper vs. project-level `grep`/`grepInvert`) — pick
the cleanest that keeps the shared cross-cutting tests un-duplicated. Planner/researcher to choose.
- Whether the desktop "New Event" toolbar button gets its own ≥44px tap-target assertion as the
desktop parity equivalent of the FAB's 56×56 check (recommended, but optional).
</decisions>
<canonical_refs>
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### The harness being extended (Phase 7)
- `apps/pwa/playwright.config.ts` — the two-profile matrix to extend with a third `desktop` project;
documents the carried-forward auth/SW/baseURL/webServer decisions (D-01..D-10 in its header).
- `apps/pwa/e2e/layout.spec.ts` — the spec with the most mobile-only assumptions (BottomTabBar
safe-area/in-viewport, 56×56 FAB, PhoneNav chrome, tap targets). Primary compat-pass target.
- `apps/pwa/e2e/calendar.spec.ts` — auth-bypass precondition, no-SW precondition, populated/error states.
- `apps/pwa/e2e/lists.spec.ts` — populated/empty states.
- `apps/pwa/e2e/global-setup.ts``/health` readiness poll + deterministic DB reset/seed (shared by
all profiles, unchanged by this phase).
- `apps/pwa/e2e/README.md` — harness run/guardrail docs; update for the new profile.
### Phase context / roadmap
- `.planning/ROADMAP.md` §"Phase 14: Desktop E2E Coverage" — goal, 3 success criteria, pitfalls.
- `.planning/phases/07-mobile-test-harness/07-CONTEXT.md` — original harness decisions to carry forward
(DEV_AUTH_BYPASS not storageState, `serviceWorkers: 'block'`, env baseURL, Vite-only webServer).
- `.planning/phases/08-gitea-ci/08-CONTEXT.md` — how CI runs `pnpm test:e2e`; confirms desktop project
is picked up automatically (only watch desktop-profile runtime/wait in CI).
### CI entry point (no plumbing change expected, but verify)
- `.gitea/workflows/ci.yml` — the harness job that runs the Playwright specs; confirm the new desktop
project runs and gates without a config change beyond runtime/wait budget.
</canonical_refs>
<code_context>
## Existing Code Insights
### Reusable Assets
- **Existing `playwright.config.ts` project array** — add a third `{ name: 'desktop', use: {
...devices['Desktop Chrome'], serviceWorkers: 'block' } }` entry. `serviceWorkers: 'block'`,
env baseURL, and the Vite `webServer` are already shared across projects.
- **`global-setup.ts`** — DB reset/seed is profile-agnostic; reused unchanged for desktop.
### Established Patterns
- **Breakpoint = 767/768px**, driven by `window.matchMedia('(max-width: 767px)')` in
`AppNav.tsx:32`, `BottomTabBar.tsx:24`, `CalendarShell.tsx:75`. Desktop Chrome at 1280px is
unambiguously in the desktop branch.
- **`getByRole('navigation', { name: 'Main navigation' })`** resolves to the **BottomTabBar** on
mobile but the **DesktopNav sidebar** on desktop — same locator, different element, both valid.
- **No strict-mode collision on desktop:** `BottomTabBar.tsx:53-57` returns `null` at ≥768px, so the
sidebar is the *sole* `Main navigation` landmark on desktop. Most nav-visibility/tap-target tests
pass as-authored, just resolving to the sidebar.
### Integration Points — desktop divergences the compat pass must handle
- **BottomTabBar safe-area / "fully in-viewport" tests** (`layout.spec.ts` describe blocks):
bottom bar is `null` on desktop → **skip on desktop**.
- **"New Event FAB meets 56×56px"** (`layout.spec.ts:91`): on desktop the `New Event` name resolves
to the toolbar button (`CalendarShell.tsx:424`), which is not 56×56 → **skip the FAB-geometry test
on desktop**; optionally assert the desktop toolbar button's ≥44px target instead (D-04 parity).
- **"PhoneNav header is visible"** (`layout.spec.ts:76`): `getByText('FamilySync', {exact:true})` also
matches the DesktopNav title (`AppNav.tsx:181`) → likely passes on desktop; verify no second match.
- **Settings button** (`/open settings/i`): present on both PhoneNav and DesktopNav → resolves on both.
- **Tap-target tests scoped to the nav landmark**: desktop sidebar links have `minHeight: 44px`
(`AppNav.tsx:151`) → pass on desktop.
</code_context>
<specifics>
## Specific Ideas
- The gate's whole point is to be real: desktop must be **blocking**, and the specs must be made
deterministic to earn that (D-03). "Make it green by weakening the assertion" is explicitly rejected.
- Mirror Phase 7's self-validation ethos where cheap: a desktop run should fail loudly if the desktop
layout actually regresses, not silently pass because everything was skipped.
</specifics>
<deferred>
## Deferred Ideas
- **Desktop WebKit (Desktop Safari) profile** — considered and declined for this phase (D-06).
Revisit only if a desktop-Safari-specific rendering bug is ever reported.
- **Live desktop event-create round-trip** — still blocked by dev user 1 having no CalDAV calendar
(carried from Phase 7); out of scope here, same as on mobile.
None of the above are blockers — discussion stayed within phase scope.
</deferred>
---
*Phase: 14-Desktop E2E Coverage*
*Context gathered: 2026-06-12*
@@ -0,0 +1,69 @@
# Phase 14: Desktop E2E Coverage - Discussion Log
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
**Date:** 2026-06-12
**Phase:** 14-Desktop E2E Coverage
**Areas discussed:** Spec-compat strategy, Desktop gate severity, Desktop assertion depth, Desktop profile/viewport
---
## Spec-Compat Strategy
| Option | Description | Selected |
|--------|-------------|----------|
| Conditional skips, shared specs | One set of spec files; gate mobile-only tests with `test.skip(project==='desktop')`, assert desktop equivalents on divergence; cross-cutting tests shared. | ✓ |
| Separate desktop spec files | New `desktop.spec.ts`; mobile specs project-filtered. Clear separation, duplicates cross-cutting assertions. | |
| Locator-only, no skips | Viewport-agnostic locators, no skips. Impossible where elements don't exist on desktop (FAB, bottom bar). | |
**User's choice:** Conditional skips, shared specs
**Notes:** One source of truth, least duplication. Mobile profiles stay exactly as authored in Phase 7.
---
## Desktop Gate Severity
| Option | Description | Selected |
|--------|-------------|----------|
| Blocking (same as mobile) | Desktop failures block the merge exactly like iphone/pixel. | ✓ |
| Advisory (non-blocking) | Desktop runs/reports but doesn't block merge (continue-on-error). | |
**User's choice:** Blocking (same as mobile)
**Notes:** Resolves ROADMAP Success Criterion 3 (deferred to planning). Implies specs must be deterministic before merge — fix flakes, don't downgrade the gate.
---
## Desktop Assertion Depth
| Option | Description | Selected |
|--------|-------------|----------|
| Parity-adapted | Assert desktop equivalents (sidebar nav, no overflow, populated/empty/error states, nav tap targets); skip FAB/bottom-bar. | ✓ |
| Render-only smoke | Just no-overflow + states render; no desktop-chrome assertions. | |
| Full divergence assertions | Also assert mobile chrome absent + desktop-only elements present. | |
**User's choice:** Parity-adapted
**Notes:** Full-divergence rejected on code grounds — `New Event` is not mobile-only (desktop toolbar button shares the name, `CalendarShell.tsx:424`); only the 56×56 FAB geometry is mobile-specific.
---
## Desktop Profile / Viewport
| Option | Description | Selected |
|--------|-------------|----------|
| Desktop Chrome default, no WebKit | `devices['Desktop Chrome']` (1280×720), single desktop project. | ✓ |
| Desktop Chrome at wider width | Override to ~1440×900 for more sidebar/grid real estate. | |
| Add Desktop WebKit too | Desktop Chrome + Desktop Safari/WebKit. | |
**User's choice:** Desktop Chrome default, no WebKit
**Notes:** Apple member already covered on mobile Safari (iphone/WebKit). Desktop Chrome sufficient for shared/wall browser.
## Claude's Discretion
- Exact skip-gate mechanism (`test.skip(testInfo.project.name === 'desktop')` vs. `isDesktop` describe split vs. project `grep`/`grepInvert`) — pick the cleanest that avoids duplicating cross-cutting tests.
- Whether the desktop "New Event" toolbar button gets its own ≥44px tap-target assertion as the desktop parity equivalent of the FAB check (recommended, optional).
## Deferred Ideas
- Desktop WebKit profile — declined for this phase; revisit only on a desktop-Safari-specific bug.
- Live desktop event-create round-trip — still blocked by dev user 1 having no CalDAV calendar (carried from Phase 7).
@@ -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 3554)
**Current `projects` array** (lines 3555 — 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 5764 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 5962) 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:436457), 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 3541 and 108110):** `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 4360, 112128):** 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 `<header>` 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 142165):** Purely DOM measurement. Pass unchanged on desktop.
- **Harness self-validation injected-defect proofs (lines 175256):** 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 125) 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 81111):** `.sx-react-calendar-wrapper` and `getByText('Seeded Test Event')` are not viewport-dependent. Pass unchanged.
- **Error state tests (lines 116178):** `page.route` + heading/button assertions are not viewport-dependent. Pass unchanged.
Only the file's header comment block (lines 1215) 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 1215) needs updating to list the `desktop` project.
---
### `apps/pwa/e2e/README.md` — docs update only
**Current run commands block (lines 3747):**
```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 5962 (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:436457)
**Source:** `CalendarShell.tsx` lines 424459
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
@@ -0,0 +1,94 @@
---
phase: 14-desktop-e2e-coverage
reviewed: 2026-06-12T00:00:00Z
depth: standard
files_reviewed: 6
files_reviewed_list:
- apps/pwa/playwright.config.ts
- apps/pwa/e2e/layout.spec.ts
- apps/pwa/e2e/calendar.spec.ts
- apps/pwa/e2e/lists.spec.ts
- apps/pwa/e2e/README.md
- .gitea/workflows/ci.yml
findings:
critical: 0
warning: 3
info: 3
total: 6
status: issues_found
---
# Phase 14: Code Review Report
**Reviewed:** 2026-06-12
**Depth:** standard
**Files Reviewed:** 6
**Status:** issues_found
## Summary
Phase 14 adds a third Playwright project (`desktop` — Desktop Chrome, 1280×720, no `hasTouch`) and makes the existing mobile-authored e2e specs green on desktop. The diff is test-harness-only — no runtime/app code changed. I verified the load-bearing source assumptions encoded in the new skip guards against the actual components:
- **Skip guards are correct.** On desktop (≥768px) `AppNav` renders `DesktopNav` with the sole `<nav aria-label="Main navigation">` (`AppNav.tsx:168`), and `BottomTabBar` returns `null` (`BottomTabBar.tsx:56`). There is no strict-mode collision and the safe-area-inset bottom-edge assertion is genuinely meaningless for a left sidebar — the two `BottomTabBar is fully in-viewport` skips are right.
- **FAB skip / desktop-parity test are consistent.** On desktop the only `button` with accessible name "New Event" is the toolbar button (`CalendarShell.tsx:436-457`, text "New Event", `minHeight: '44px'`). The FAB is gated behind `phone &&` (`CalendarShell.tsx:464`), so the 56×56 FAB assertion correctly skips and the new ≥44px parity test (`testInfo.project.name !== 'desktop'` inverse-guard) correctly runs only on desktop. The two guards target the exact same locator — internally consistent.
- **No mobile assertion was weakened or deleted.** `calendar.spec.ts` and `lists.spec.ts` changes are comment-only. `layout.spec.ts` only adds guards + one new test; every prior mobile assertion is intact. The CI workflow change is comment-only (step name + comment), the `desktop` project runs automatically because the harness invokes `playwright test` with no `--project` filter.
Findings below are quality/determinism concerns, not correctness blockers. The harness logic is sound.
## Warnings
### WR-01: Two "PhoneNav" tests silently re-target DesktopNav on desktop and no longer test what their names claim
**File:** `apps/pwa/e2e/layout.spec.ts:83-96`
**Issue:** `PhoneNav header is visible (Rule 3)` and `PhoneNav settings button meets 44×44px (Rule 1)` are NOT guarded, so they run on the `desktop` project. On desktop `PhoneNav` is never rendered (`AppNav.tsx:34` returns `DesktopNav` at ≥768px). The tests pass only by coincidence:
- `getByText('FamilySync', { exact: true })` matches the `DesktopNav` sidebar title (`AppNav.tsx:181`) instead of the PhoneNav header.
- `getByRole('button', { name: /open settings/i })` matches the `DesktopNav` avatar button (`AppNav.tsx:220-222`, `minHeight: '44px'`) instead of the PhoneNav settings button.
This is misleading green: a future regression that breaks PhoneNav specifically would still pass on desktop, and the test name asserts a component that isn't on screen. The phase scope ("right tests skipped, no over/under-skipping") implies these mobile-named geometry tests should either be desktop-skipped (like the FAB) or renamed to reflect that on desktop they validate the DesktopNav equivalent.
**Fix:** Either guard them mobile-only and add explicit DesktopNav counterparts, e.g.:
```ts
test('PhoneNav header is visible (Rule 3)', async ({ page }, testInfo) => {
test.skip(testInfo.project.name === 'desktop',
'PhoneNav is not rendered at ≥768px; DesktopNav renders the FamilySync title instead');
// ...
});
```
or rename to "App header/title is visible" and "Settings affordance meets 44×44px" so the assertion is honest across all three profiles.
### WR-02: calendar.spec "all tests pass unchanged" relies on an untested Schedule-X view (month-grid) on desktop
**File:** `apps/pwa/e2e/calendar.spec.ts:16`, `apps/pwa/e2e/calendar.spec.ts:92-101`
**Issue:** The header comment claims desktop runs "all tests pass unchanged", but the desktop default Schedule-X view differs from mobile: `readPersistedView()` returns `month-grid` for ≥768px vs `month-agenda` for ≤767px (`apps/pwa/src/store/calendarStore.ts:14-15,119-120`). The `seeded event "Seeded Test Event" is rendered (DB→UI proof)` test asserts `getByText('Seeded Test Event').first()` is visible — on desktop this now exercises month-grid rendering that the iphone/pixel profiles never covered. In month-grid, an event title can be collapsed into a "+N more" overflow affordance when a day cell is crowded. With a single seeded event today this won't trigger, so the risk is low, but the claim "unchanged" understates that desktop adds a new rendering path. If global-setup is ever extended to seed multiple same-day events, this assertion could flake/fail on desktop only.
**Fix:** Note the view divergence in the comment, and make the assertion view-robust if multi-event seeds are anticipated — e.g. assert the event is reachable, falling back to an explicit week/day view switch, or pin the desktop test to a deterministic view. At minimum, change the comment to "all tests pass; desktop renders month-grid (mobile renders month-agenda)".
### WR-03: lists.spec / calendar.spec "all tests pass unchanged" claim is unverified by the diff and contradicted by view/landmark divergence
**File:** `apps/pwa/e2e/lists.spec.ts:19`, `apps/pwa/e2e/calendar.spec.ts:16`
**Issue:** Both comment headers added by this phase assert "(all tests pass unchanged)" for the desktop profile. The lists `at least one list item is present` test (`lists.spec.ts:43-48`) and `seeded "E2E Grocery List" card` test depend on `ListsIndex.tsx:152` `role="list"` + `ListCard.tsx:52` `role="listitem"`, which are viewport-independent — those are fine. But the blanket "unchanged" wording is an unverifiable assertion baked into source comments; it papers over WR-01 (PhoneNav tests retarget) and WR-02 (month-grid). Comments that assert test outcomes drift out of date and mislead future readers into trusting desktop coverage they don't have.
**Fix:** Replace "all tests pass unchanged" with a factual statement of what desktop exercises (e.g. "desktop renders DesktopNav sidebar + month-grid; mobile-geometry tests are skipped, see layout.spec.ts"). Keep outcome claims out of source comments — let the CI run be the source of truth.
## Info
### IN-01: `serviceWorkers: 'block'` is duplicated across all three projects instead of hoisted to shared `use`
**File:** `apps/pwa/playwright.config.ts:45,53,62`
**Issue:** Every project repeats `serviceWorkers: 'block'`. The config comment (`line 6`) states it applies to all profiles, so it is a global invariant, not a per-device override. Repeating it invites a future profile being added without it (silent SW-block regression).
**Fix:** Hoist `serviceWorkers: 'block'` into the top-level `use` block (line 28-34) and drop it from each project. Per-project `use` only needs the device descriptor spread.
### IN-02: New desktop parity test has no explicit auth/mount wait before locating the toolbar button
**File:** `apps/pwa/e2e/layout.spec.ts:114-128`
**Issue:** The describe-level `beforeEach` (`layout.spec.ts:32-34`) only does `page.goto('/calendar')` with no `await expect(nav).toBeVisible()` gate (unlike the calendar/lists specs which wait for the nav landmark). The new test relies entirely on `boundingBox()` auto-waiting for the toolbar button to mount after auth + CalendarShell render. This works because Playwright retries, but it's inconsistent with the explicit auth-wait pattern used in `calendar.spec.ts:79` and `lists.spec.ts:33`, and a slow desktop mount could eat into the default timeout.
**Fix:** Add `await expect(page.getByRole('navigation', { name: 'Main navigation' })).toBeVisible();` at the top of the new test (or in a desktop-scoped beforeEach) to gate on auth before measuring, matching the established pattern.
### IN-03: Desktop project lacks an explicit viewport assertion / pin
**File:** `apps/pwa/playwright.config.ts:56-63`
**Issue:** The `desktop` project relies entirely on `devices['Desktop Chrome']`'s bundled 1280×720 viewport. The spec comments hard-code "1280×720" in three files (`layout.spec.ts:13`, `calendar.spec.ts:16`, `lists.spec.ts:19`). If Playwright's `Desktop Chrome` descriptor viewport ever changes across a version bump, the comments silently lie and the ≥768px breakpoint assumptions could (in extreme cases) break with no guard. Low risk — 1280×720 is stable — but the breakpoint dependency (≥768px) is implicit.
**Fix:** Optionally pin `viewport: { width: 1280, height: 720 }` explicitly in the desktop project `use` so the ≥768px DesktopNav assumption is self-documenting and version-stable.
---
_Reviewed: 2026-06-12_
_Reviewer: Claude (gsd-code-reviewer)_
_Depth: standard_
+3 -2
View File
@@ -1,6 +1,6 @@
# E2E Test Harness
Playwright test harness for the FamilySync PWA — mobile-emulated (iPhone 14/WebKit + Pixel 7/Chromium), authenticated via `DEV_AUTH_BYPASS`, deterministically seeded, runs headlessly in CI.
Playwright test harness for the FamilySync PWA — mobile-emulated (iPhone 14/WebKit + Pixel 7/Chromium) and Desktop Chrome (1280×720), authenticated via `DEV_AUTH_BYPASS`, deterministically seeded, runs headlessly in CI.
---
@@ -34,11 +34,12 @@ environment (not only the API's). Source the DB credentials from the repo-root `
set -a; source .env; set +a
export DEV_AUTH_BYPASS=true DB_HOST=127.0.0.1 DB_PORT=3306
# Full suite — both iPhone (WebKit) and Pixel (Chromium) profiles
# 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
# Headed (local debug — shows the browser)
pnpm --filter @familysync/pwa exec playwright test --headed
+2 -1
View File
@@ -10,9 +10,10 @@
* Requires the dev stack running with DEV_AUTH_BYPASS=true (see e2e/README.md).
* global-setup seeds 'Seeded Test Event' on calendar_id=10 for user_id=1.
*
* Runs on both device profiles automatically (playwright.config.ts matrix):
* Runs on all three device profiles automatically (playwright.config.ts matrix):
* iphone: iPhone 14 / WebKit / 390×844
* pixel: Pixel 7 / Chromium / 412×915
* desktop: Desktop Chrome / Chromium / 1280×720 (all tests pass unchanged)
*
* Run:
* pnpm --filter @familysync/pwa test:e2e
+38 -4
View File
@@ -7,9 +7,10 @@
* Rule 3: Critical elements visible and in-viewport on initial load
* Rule 4: Accessible names on all interactive elements (role+name locators)
*
* Runs on both device profiles automatically (playwright.config.ts matrix):
* Runs on all three device profiles automatically (playwright.config.ts matrix):
* iphone: iPhone 14 / WebKit / 390×844
* pixel: Pixel 7 / Chromium / 412×915
* desktop: Desktop Chrome / Chromium / 1280×720 (mobile-only geometry tests skipped)
*
* STRICT-MODE NOTE:
* On mobile viewports (767px), AppNav renders PhoneNav as a <header> element
@@ -59,7 +60,13 @@ test.describe('Rule 1/3/4 — BottomTabBar tap targets and in-viewport position'
expect(box!.height, 'Lists tab height ≥ 44px').toBeGreaterThanOrEqual(44);
});
test('BottomTabBar is fully in-viewport (Rule 3 — safe-area-inset)', async ({ page }) => {
test('BottomTabBar is fully in-viewport (Rule 3 — safe-area-inset)', async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name === 'desktop',
'On desktop the Main navigation landmark resolves to the DesktopNav sidebar; the safe-area-inset bottom-edge assertion is semantically wrong for a sidebar (BottomTabBar returns null at ≥768px)',
);
// 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' });
@@ -88,7 +95,11 @@ test.describe('Rule 1/3/4 — BottomTabBar tap targets and in-viewport position'
expect(box!.height, 'Settings button height ≥ 44px').toBeGreaterThanOrEqual(44);
});
test('New Event FAB meets 56×56px touch-target minimum (Rule 1)', async ({ page }) => {
test('New Event FAB meets 56×56px touch-target minimum (Rule 1)', async ({ page }, testInfo) => {
test.skip(
testInfo.project.name === 'desktop',
'On desktop getByRole(button, New Event) resolves to the toolbar button (CalendarShell.tsx:436-457), not the 56×56 FAB; FAB geometry assertion does not apply to the toolbar button',
);
// 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();
@@ -96,6 +107,25 @@ test.describe('Rule 1/3/4 — BottomTabBar tap targets and in-viewport position'
expect(box!.width, 'New Event FAB width ≥ 56px').toBeGreaterThanOrEqual(56);
expect(box!.height, 'New Event FAB height ≥ 56px').toBeGreaterThanOrEqual(56);
});
// D-04 parity: desktop toolbar "New Event" button meets ≥44px minimum (Rule 1)
// CalendarShell.tsx:443 sets minHeight:'44px' on the desktop toolbar button.
// Guarded to run only on desktop — the FAB test above covers mobile profiles.
test('New Event toolbar button meets 44px touch-target minimum on desktop (Rule 1 — D-04 parity)', async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name !== 'desktop',
'Desktop parity assertion — toolbar button only renders at ≥768px (Desktop Chrome); FAB test covers iphone/pixel',
);
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);
});
});
// ── Rule 1/3/4 repeated on /lists ──
@@ -127,7 +157,11 @@ test.describe('Rule 1/3/4 — BottomTabBar on /lists', () => {
expect(box!.height).toBeGreaterThanOrEqual(44);
});
test('BottomTabBar is fully in-viewport on /lists (Rule 3)', async ({ page }) => {
test('BottomTabBar is fully in-viewport on /lists (Rule 3)', async ({ page }, testInfo) => {
test.skip(
testInfo.project.name === 'desktop',
'On desktop the Main navigation landmark resolves to the DesktopNav sidebar; the safe-area-inset bottom-edge assertion is semantically wrong for a sidebar (BottomTabBar returns null at ≥768px)',
);
const nav = page.getByRole('navigation', { name: 'Main navigation' });
await expect(nav).toBeVisible();
const box = await nav.boundingBox();
+2 -1
View File
@@ -13,9 +13,10 @@
* The empty state is simulated by routing /api/lists to return [] BEFORE navigation
* this keeps the seeded DB intact (T-07-11 / D-06 deterministic seed).
*
* Runs on both device profiles automatically (playwright.config.ts matrix):
* Runs on all three device profiles automatically (playwright.config.ts matrix):
* iphone: iPhone 14 / WebKit / 390×844
* pixel: Pixel 7 / Chromium / 412×915
* desktop: Desktop Chrome / Chromium / 1280×720 (all tests pass unchanged)
*
* Run:
* pnpm --filter @familysync/pwa test:e2e
+12 -3
View File
@@ -1,15 +1,16 @@
/**
* Playwright configuration Phase 7 Mobile Test Harness
* Playwright configuration Phase 7 Mobile Test Harness + Phase 14 Desktop
*
* Two-profile device matrix: iPhone 14/WebKit + Pixel 7/Chromium
* Three-profile device matrix: iPhone 14/WebKit + Pixel 7/Chromium + Desktop Chrome
* Auth: DEV_AUTH_BYPASS=true on the API (never storageState D-01/Pitfall 14)
* SW: serviceWorkers: 'block' on both profiles (D-02/Pitfall 15)
* SW: serviceWorkers: 'block' on all profiles (D-02/Pitfall 15)
* baseURL: env-driven PLAYWRIGHT_BASE_URL (D-08/Rule 8)
* webServer: manages Vite only API+MariaDB+Redis stay compose-managed (D-10)
*
* Run:
* pnpm --filter @familysync/pwa test:e2e
* pnpm --filter @familysync/pwa exec playwright test --project=pixel
* pnpm --filter @familysync/pwa exec playwright test --project=desktop
*/
import { defineConfig, devices } from '@playwright/test';
@@ -52,6 +53,14 @@ export default defineConfig({
serviceWorkers: 'block',
},
},
{
// Desktop Chrome: 1280×720 viewport, Chromium engine, no hasTouch (D-06)
name: 'desktop',
use: {
...devices['Desktop Chrome'],
serviceWorkers: 'block',
},
},
],
// D-10: manage Vite only; API+MariaDB+Redis are compose-managed