---
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>
