Files
familysync/.planning/phases/14-desktop-e2e-coverage/14-PATTERNS.md
T

253 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 14: Desktop E2E Coverage - Pattern Map
**Mapped:** 2026-06-12
**Files analyzed:** 5 modified files
**Analogs found:** 5 / 5 (all files are self-analogs — modifications to existing code)
---
## File Classification
| Modified File | Role | Data Flow | Closest Analog | Match Quality |
|---|---|---|---|---|
| `apps/pwa/playwright.config.ts` | config | N/A | itself (iphone/pixel project entries) | exact |
| `apps/pwa/e2e/layout.spec.ts` | test | request-response | itself (existing describe/skip patterns) | exact |
| `apps/pwa/e2e/calendar.spec.ts` | test | request-response | itself (test.skip at line 59 is live reference) | exact |
| `apps/pwa/e2e/lists.spec.ts` | test | request-response | itself (all tests pass unchanged on desktop) | exact |
| `apps/pwa/e2e/README.md` | docs | N/A | itself | exact |
---
## Pattern Assignments
### `apps/pwa/playwright.config.ts` — add `desktop` project entry
**Analog:** existing `iphone` and `pixel` project entries (lines 3554)
**Current `projects` array** (lines 3555 — mirror this shape exactly for the third entry):
```typescript
projects: [
{
// iPhone 14: 390×844 viewport, WebKit engine, Mobile Safari UA, hasTouch: true
name: 'iphone',
use: {
...devices['iPhone 14'],
serviceWorkers: 'block',
},
},
{
// Pixel 7: 412×915 viewport, Chromium engine, Chrome Android UA, hasTouch: true
name: 'pixel',
use: {
...devices['Pixel 7'],
serviceWorkers: 'block',
},
},
],
```
**New `desktop` entry to add** (append after the `pixel` entry, before the closing `]`):
```typescript
{
// Desktop Chrome: 1280×720 viewport, Chromium engine, no hasTouch (D-06)
name: 'desktop',
use: {
...devices['Desktop Chrome'],
serviceWorkers: 'block',
},
},
```
Key invariants to preserve (from file header comments):
- `serviceWorkers: 'block'` — same as both mobile profiles; mandatory on every project (D-02/Pitfall 15)
- `devices['Desktop Chrome']` — provides 1280×720, no `hasTouch`, desktop UA, Chromium engine
- No `baseURL` override in the project entry — it is inherited from the top-level `use` block (line 29)
- No `webServer` change — shared block at lines 5764 applies to all projects automatically
---
### `apps/pwa/e2e/layout.spec.ts` — spec-compat pass (primary target)
**Self-analog** — read carefully; two `test.describe` blocks need desktop skip guards, one test needs a desktop parity assertion, one test passes unchanged.
#### Skip mechanism in the existing codebase
The one existing `test.skip` in the suite (from `calendar.spec.ts` lines 5962) uses the inline conditional form:
```typescript
test.skip(
!swAvailable,
'navigator.serviceWorker is unavailable in this context (e.g. WebKit over http://localhost) — block is unobservable here',
);
```
This form — `test.skip(condition, reason)` called at the top of the test body — is the established pattern. Do **not** use `test.skip(testInfo.project.name === 'desktop', ...)` with a `testInfo` parameter; the simpler form without `testInfo` is consistent with what already exists.
For project-name gating the correct signature requires the `testInfo` fixture:
```typescript
test('...', async ({ page }, testInfo) => {
test.skip(testInfo.project.name === 'desktop', 'reason string');
// ...rest of test
});
```
Use this signature for every mobile-only test that needs a desktop skip. Keep the reason string explicit and factual (consistent with the SW-block skip's style).
#### Tests in `layout.spec.ts` that need a desktop skip
**Test at line 62 — "BottomTabBar is fully in-viewport (Rule 3 — safe-area-inset)":**
```typescript
test('BottomTabBar is fully in-viewport (Rule 3 — safe-area-inset)', async ({ page }) => {
// The bar uses env(safe-area-inset-bottom, 0px). In emulation there is no
// safe-area-inset, so the bar's bottom edge must be ≤ viewport height.
const nav = page.getByRole('navigation', { name: 'Main navigation' });
await expect(nav).toBeVisible();
const box = await nav.boundingBox();
// ...
```
On desktop `BottomTabBar.tsx:53-57` returns `null` at ≥768px, so this `nav` would resolve to DesktopNav sidebar and the safe-area-inset assertion is semantically wrong for a sidebar. **Skip on desktop.**
**Test at line 130 — "BottomTabBar is fully in-viewport on /lists (Rule 3)"** — same reason. **Skip on desktop.**
**Test at line 91 — "New Event FAB meets 56×56px touch-target minimum (Rule 1)":**
```typescript
test('New Event FAB meets 56×56px touch-target minimum (Rule 1)', async ({ page }) => {
// Phone-only FAB — aria-label="New Event", fixed 56×56px (CalendarShell.tsx)
const fab = page.getByRole('button', { name: 'New Event' });
const box = await fab.boundingBox();
expect(box, 'New Event FAB bounding box must not be null').not.toBeNull();
expect(box!.width, 'New Event FAB width ≥ 56px').toBeGreaterThanOrEqual(56);
expect(box!.height, 'New Event FAB height ≥ 56px').toBeGreaterThanOrEqual(56);
});
```
On desktop `getByRole('button', { name: 'New Event' })` resolves to the **desktop toolbar button** (CalendarShell.tsx:436457), not the 56×56px FAB. The toolbar button has `minHeight: 44px` but no 56px constraint. **Skip the FAB-geometry (56×56) assertion on desktop.** Per D-04, add a desktop parity block asserting ≥44px instead.
Desktop parity assertion to add (new test or a conditional branch in the same test):
```typescript
// Desktop parity: toolbar "New Event" button meets ≥44px minimum (D-04)
// CalendarShell.tsx:443 sets minHeight:'44px' on the desktop toolbar button.
const toolbarBtn = page.getByRole('button', { name: 'New Event' });
const box = await toolbarBtn.boundingBox();
expect(box, 'New Event toolbar button bounding box must not be null').not.toBeNull();
expect(box!.height, 'New Event toolbar button height ≥ 44px (Rule 1 desktop parity)').toBeGreaterThanOrEqual(44);
```
#### Tests that pass unchanged on desktop (no modification needed)
- **"BottomTabBar navigation landmark is visible" (lines 3541 and 108110):** `getByRole('navigation', { name: 'Main navigation' })` resolves to DesktopNav sidebar on desktop (sole nav landmark at ≥768px). Passes as-authored.
- **"Calendar tab meets 44×44px" / "Lists tab meets 44×44px" (lines 4360, 112128):** Scoped to the `Main navigation` landmark; DesktopNav sidebar links have `minHeight: 44px` (AppNav.tsx:151). Pass as-authored.
- **"PhoneNav header is visible" (line 76):** `getByText('FamilySync', { exact: true })` matches the DesktopNav title text (AppNav.tsx:181). Passes on desktop. Verify no strict-mode collision (CONTEXT.md confirms the PhoneNav `<header>` returns null at ≥768px, leaving the DesktopNav title as the sole match).
- **"PhoneNav settings button meets 44×44px" (line 82):** `getByRole('button', { name: /open settings/i })` is present on both PhoneNav and DesktopNav. Passes as-authored.
- **Rule 2 overflow tests (lines 142165):** Purely DOM measurement. Pass unchanged on desktop.
- **Harness self-validation injected-defect proofs (lines 175256):** Use `nav[aria-label="Main navigation"]` CSS selector and body width injection. Pass unchanged on desktop.
#### `describe` block header comments to update
The jsdoc block at the top of `layout.spec.ts` (lines 125) currently says:
```
* Runs on both device profiles automatically (playwright.config.ts matrix):
* iphone: iPhone 14 / WebKit / 390×844
* pixel: Pixel 7 / Chromium / 412×915
```
Update to list all three profiles (same update applies to all spec file headers).
---
### `apps/pwa/e2e/calendar.spec.ts` — no structural changes needed
All tests in this file pass unchanged on desktop:
- **Auth-bypass precondition (line 26):** waits for `Main navigation` landmark — resolves to DesktopNav sidebar on desktop. Passes.
- **SW-block precondition (line 42):** already uses `test.skip(!swAvailable, ...)` — self-healing conditional. No change.
- **Populated state tests (lines 81111):** `.sx-react-calendar-wrapper` and `getByText('Seeded Test Event')` are not viewport-dependent. Pass unchanged.
- **Error state tests (lines 116178):** `page.route` + heading/button assertions are not viewport-dependent. Pass unchanged.
Only the file's header comment block (lines 1215) needs updating to list the `desktop` project.
---
### `apps/pwa/e2e/lists.spec.ts` — no structural changes needed
All tests pass unchanged on desktop — no mobile-only assumptions anywhere in this file. Only the header comment (lines 1215) needs updating to list the `desktop` project.
---
### `apps/pwa/e2e/README.md` — docs update only
**Current run commands block (lines 3747):**
```bash
# Full suite — both iPhone (WebKit) and Pixel (Chromium) profiles
pnpm --filter @familysync/pwa test:e2e
# Single profile (faster local iteration)
pnpm --filter @familysync/pwa exec playwright test --project=pixel
# Headed (local debug — shows the browser)
pnpm --filter @familysync/pwa exec playwright test --headed
# UI mode (interactive test explorer)
pnpm --filter @familysync/pwa test:e2e:ui
```
Update the comment on the full-suite command and add a desktop-specific example:
```bash
# Full suite — iPhone (WebKit), Pixel (Chromium), Desktop Chrome profiles
pnpm --filter @familysync/pwa test:e2e
# Single profile (faster local iteration)
pnpm --filter @familysync/pwa exec playwright test --project=pixel
pnpm --filter @familysync/pwa exec playwright test --project=desktop
```
Also update the preamble sentence (line 3) which currently says "mobile-emulated (iPhone 14/WebKit + Pixel 7/Chromium)" — add "Desktop Chrome (1280×720)".
---
## Shared Patterns
### Project-name conditional skip
**Source:** `apps/pwa/e2e/calendar.spec.ts` lines 5962 (inline `test.skip` form)
**Apply to:** every mobile-only test in `layout.spec.ts`
Pattern — add as the **first statement** in the test body, before any `await`:
```typescript
test('test name', async ({ page }, testInfo) => {
test.skip(testInfo.project.name === 'desktop', 'reason: mobile-only assertion (describe which element is absent on desktop)');
// existing test body unchanged below
...
});
```
### Nav landmark locator (works on all three profiles)
**Source:** `apps/pwa/e2e/layout.spec.ts` lines 45, 65; `calendar.spec.ts` lines 31, 78
**Apply to:** any new desktop assertion that needs auth-ready confirmation
```typescript
const nav = page.getByRole('navigation', { name: 'Main navigation' });
await expect(nav).toBeVisible();
```
On mobile: resolves to `BottomTabBar` nav. On desktop: resolves to `DesktopNav` sidebar. Same locator, different element — no conditional needed.
### Desktop "New Event" button locator (CalendarShell.tsx:436457)
**Source:** `CalendarShell.tsx` lines 424459
The desktop toolbar button is rendered inside `{!phone && (...)}` with plain text `New Event` (no `aria-label` attribute). Playwright resolves it by accessible name from inner text:
```typescript
// Resolves to desktop toolbar button at ≥768px (has minHeight:44px per line 443)
// Resolves to phone FAB at <768px (has aria-label="New Event" per line 466)
page.getByRole('button', { name: 'New Event' })
```
At 1280px (`Desktop Chrome`) only the toolbar button renders; the FAB is in `{phone && (...)}` which is false. **No strict-mode collision.**
---
## No Analog Found
None. All files being modified are established; all new code copies directly from existing patterns in the same files.
---
## Metadata
**Analog search scope:** `apps/pwa/playwright.config.ts`, `apps/pwa/e2e/`, `apps/pwa/src/components/CalendarShell.tsx`
**Files scanned:** 6
**Pattern extraction date:** 2026-06-12