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