Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
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):
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 ]):
{
// 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, nohasTouch, desktop UA, Chromium engine- No
baseURLoverride in the project entry — it is inherited from the top-leveluseblock (line 29) - No
webServerchange — 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:
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:
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)":
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)":
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):
// 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 navigationlandmark; DesktopNav sidebar links haveminHeight: 44px(AppNav.tsx:151). Pass as-authored. - "PhoneNav header is visible" (line 76):
getByText('FamilySync', { exact: true })matches the DesktopNav title text (AppNav.tsx:181). Passes on desktop. Verify no strict-mode collision (CONTEXT.md confirms the PhoneNav<header>returns null at ≥768px, leaving the DesktopNav title as the sole match). - "PhoneNav settings button meets 44×44px" (line 82):
getByRole('button', { name: /open settings/i })is present on both PhoneNav and DesktopNav. Passes as-authored. - Rule 2 overflow tests (lines 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 navigationlandmark — 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-wrapperandgetByText('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):
# 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:
# 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:
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
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:
// 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