--- 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'" --- 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`. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.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 Task 1: Add the `desktop` project to playwright.config.ts (SC-1, D-06) apps/pwa/playwright.config.ts - 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). 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. 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 - `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. The `desktop` project exists, resolves via `--project=desktop`, and mirrors the mobile entries with no baseURL/webServer duplication. Task 2: Desktop-skip the two mobile-only layout assertions + add D-04 parity (SC-2, D-01, D-02, D-04, D-05) apps/pwa/e2e/layout.spec.ts - 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). 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', '')` 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. 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 - `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. 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. Task 3: Update spec headers + README, then prove the full suite is green on all three projects (SC-2, SC-3, D-03) apps/pwa/e2e/calendar.spec.ts, apps/pwa/e2e/lists.spec.ts, apps/pwa/e2e/README.md - 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). 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. 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 - `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). 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. ## 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). - `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. - 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). ## 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', '')` 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). Create `.planning/phases/14-desktop-e2e-coverage/14-01-SUMMARY.md` when done.