Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
170 lines
16 KiB
Markdown
170 lines
16 KiB
Markdown
---
|
|
phase: 07-mobile-test-harness
|
|
plan: 04
|
|
type: execute
|
|
wave: 3
|
|
depends_on: ['07-01', '07-02']
|
|
files_modified:
|
|
- apps/pwa/e2e/calendar.spec.ts
|
|
- apps/pwa/e2e/lists.spec.ts
|
|
autonomous: true
|
|
requirements: [TEST-01, TEST-02]
|
|
user_setup: []
|
|
|
|
must_haves:
|
|
truths:
|
|
- "On both profiles, /calendar renders the populated (seeded) calendar: the Schedule-X grid is visible and the EmptyState 'Nothing here' is NOT present"
|
|
- "On both profiles, /calendar error state (API mocked to 500) shows the 'Couldn't load events' heading and a Retry button >=44px, with no horizontal overflow"
|
|
- "On both profiles, /lists renders the populated (seeded) list: the 'E2E Grocery List' card is visible and ListsEmptyState 'No lists yet' is NOT present"
|
|
- "On both profiles, /lists empty state (after seed teardown) shows 'No lists yet' + 'Tap + to create...'"
|
|
- 'The authed PWA is reached via DEV_AUTH_BYPASS — no Authelia login page, no OIDC mock — and the run produces no SW-sourced responses'
|
|
artifacts:
|
|
- path: 'apps/pwa/e2e/calendar.spec.ts'
|
|
provides: 'Calendar populated + empty + error states (UI-SPEC Rules 4/5) + auth-bypass precondition assertion'
|
|
contains: "Couldn't load events"
|
|
- path: 'apps/pwa/e2e/lists.spec.ts'
|
|
provides: 'Lists populated + empty states (UI-SPEC Rules 4/5)'
|
|
contains: 'No lists yet'
|
|
key_links:
|
|
- from: 'apps/pwa/e2e/calendar.spec.ts'
|
|
to: "page.route('/api/events*') fulfill 500"
|
|
via: 'error-state simulation registered before goto'
|
|
pattern: 'page.route'
|
|
- from: 'apps/pwa/e2e/lists.spec.ts'
|
|
to: "seeded 'E2E Grocery List' card (role=listitem / link 'Open list: ...')"
|
|
via: 'getByRole / getByText on seeded data'
|
|
pattern: 'E2E Grocery List'
|
|
---
|
|
|
|
<objective>
|
|
Author `apps/pwa/e2e/calendar.spec.ts` and `apps/pwa/e2e/lists.spec.ts`: the route-specific populated / empty / error state assertions (UI-SPEC Rules 4/5) on both device profiles, plus the TEST-02 precondition assertion that the harness reached the authenticated PWA via `DEV_AUTH_BYPASS` (no Authelia login, no OIDC mock) with no service-worker-sourced responses.
|
|
|
|
Purpose: These specs prove the seeded data (Plan 02) renders correctly, that empty and error states degrade gracefully, and that the auth + SW-block preconditions (D-01/D-02) actually hold at runtime — the heart of TEST-01 (state coverage) and TEST-02 (authed reach).
|
|
|
|
Output: `apps/pwa/e2e/calendar.spec.ts`, `apps/pwa/e2e/lists.spec.ts`.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/07-mobile-test-harness/07-CONTEXT.md
|
|
@.planning/phases/07-mobile-test-harness/07-RESEARCH.md
|
|
@.planning/phases/07-mobile-test-harness/07-PATTERNS.md
|
|
@.planning/phases/07-mobile-test-harness/07-UI-SPEC.md
|
|
@.planning/phases/07-mobile-test-harness/07-VALIDATION.md
|
|
@apps/pwa/playwright.config.ts
|
|
@apps/pwa/e2e/global-setup.ts
|
|
@apps/pwa/src/components/CalendarShell.tsx
|
|
@apps/pwa/src/components/EmptyState.tsx
|
|
@apps/pwa/src/components/ListsEmptyState.tsx
|
|
@apps/pwa/src/routes/ListsIndex.tsx
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: calendar.spec.ts — populated + error states + auth-bypass / SW-block precondition (TEST-01, TEST-02)</name>
|
|
<files>apps/pwa/e2e/calendar.spec.ts</files>
|
|
<read_first>
|
|
- .planning/phases/07-mobile-test-harness/07-UI-SPEC.md § "Rule 5" (state assertion table for /calendar) + § "Copywriting Contract" (exact strings) + § "Rule 7" (auth + SW preconditions)
|
|
- .planning/phases/07-mobile-test-harness/07-RESEARCH.md § "Pattern 5" (page.route error-state simulation) + § "layout.spec.ts skeleton" (error-state example) + § "Pitfall 5" (auth-bypass propagation)
|
|
- .planning/phases/07-mobile-test-harness/07-PATTERNS.md § "apps/pwa/e2e/calendar.spec.ts" — analog CalendarShell.test.tsx, seeded event title 'Seeded Test Event', route is /calendar, prefer ARIA roles over data-testid
|
|
- apps/pwa/src/components/CalendarShell.tsx — error branch renders `<h2>Couldn't load events</h2>` + `<button>Retry</button>` (minHeight 44px); FAB `aria-label="New Event"`; the Schedule-X grid wrapper; isError triggers after `retries: 2` on the events query
|
|
- apps/pwa/src/components/EmptyState.tsx — empty heading `Nothing here`, body `No events in this period. Try a different date or switch views.`
|
|
- apps/pwa/e2e/global-setup.ts (Plan 02) — confirms the seeded event title 'Seeded Test Event' on calendar_id=10 that the populated assertion targets
|
|
</read_first>
|
|
<action>
|
|
Create `apps/pwa/e2e/calendar.spec.ts` importing `test, expect` from `@playwright/test`; runs on both profiles via the config matrix; relative `page.goto('/calendar')` only (Rule 8 — no absolute URL).
|
|
POPULATED state (Rule 5, after global-setup seed): goto `/calendar`; assert the Schedule-X calendar grid is visible (prefer a stable ARIA/role anchor; if none exists the codebase exposes `data-testid="schedule-x-calendar"` — use role/landmark first, testid only as the documented fallback for the widget wrapper since Schedule-X provides no semantic role); assert the EmptyState text `Nothing here` is NOT present (`await expect(page.getByText('Nothing here')).toHaveCount(0)`). Optionally assert the seeded event title `Seeded Test Event` is visible — but note Schedule-X renders the current week/month by default and the seed is ~tomorrow, so the chip is only guaranteed visible if tomorrow falls in the default view; if it may not, assert grid-present + empty-absent rather than chip-visible to keep the spec date-stable (DO NOT introduce date-dependent flakiness — this is exactly the Schedule-X drift the phase avoids).
|
|
ERROR state (Rule 5): register `await page.route('/api/events*', route => route.fulfill({ status: 500, body: JSON.stringify({ error: 'simulated' }) }))` BEFORE `page.goto('/calendar')` (route must be registered before navigation — Pattern 5); the events query retries twice (config), so allow time for the error branch; assert `getByRole('heading', { name: "Couldn't load events" })` is visible and `getByRole('button', { name: 'Retry' })` is visible; assert the Retry button boundingBox height ≥44 (Rule 1 within error state); assert no horizontal overflow (scrollWidth ≤ clientWidth) in the error state too. Call `await page.unroute('/api/events*')` (or use `{ times: ... }`) so the mock does not leak to later tests (Pattern 5 caution).
|
|
AUTH-BYPASS / SW precondition (TEST-02, Rule 7): add a test that, on goto `/calendar`, asserts the page did NOT land on the Authelia login (assert authed content is present — the BottomTabBar nav `getByRole('navigation', { name: 'Main navigation' })` and that the URL is not redirected to an external auth host) — proving DEV_AUTH_BYPASS reached the authed PWA without an OIDC mock. For SW-block evidence (D-02/Pitfall 15): the `serviceWorkers: 'block'` config option prevents registration; assert no service worker is registered via `await page.evaluate(() => navigator.serviceWorker?.controller)` returning null (no controlling SW), documenting in a comment that trace-level SW-source audit is the post-hoc check per UI-SPEC Rule 7. Header comment names TEST-01/TEST-02 + the run command (PATTERNS.md convention).
|
|
</action>
|
|
<acceptance_criteria>
|
|
- `pnpm --filter @familysync/pwa exec playwright test calendar.spec.ts` passes on both `iphone` and `pixel`
|
|
- populated test asserts the calendar grid visible AND `getByText('Nothing here')` count is 0
|
|
- error test registers `page.route('/api/events*', ...500)` before goto, asserts the `Couldn't load events` heading + `Retry` button visible, Retry ≥44px, and no horizontal overflow; then unroutes
|
|
- auth test asserts authed content (Main navigation landmark) present and no redirect to an external auth host
|
|
- SW assertion confirms `navigator.serviceWorker.controller` is null (no controlling SW)
|
|
- no absolute URL in the file
|
|
</acceptance_criteria>
|
|
<verify>
|
|
<automated>cd apps/pwa && pnpm exec playwright test calendar.spec.ts 2>&1 | tail -15; grep -cE "https?://localhost" e2e/calendar.spec.ts</automated>
|
|
</verify>
|
|
<done>calendar.spec.ts asserts populated + error states on both profiles, the Retry tap target + overflow in the error state, and the DEV_AUTH_BYPASS + no-SW-controller preconditions.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: lists.spec.ts — populated + empty states (TEST-01)</name>
|
|
<files>apps/pwa/e2e/lists.spec.ts</files>
|
|
<read_first>
|
|
- .planning/phases/07-mobile-test-harness/07-UI-SPEC.md § "Rule 5" (state table for /lists) + § "Copywriting Contract" (lists empty heading 'No lists yet', body 'Tap + to create your first shared list')
|
|
- .planning/phases/07-mobile-test-harness/07-PATTERNS.md § "apps/pwa/e2e/lists.spec.ts" — analog ListDetail.test.tsx, seeded list name 'E2E Grocery List', items 'Milk'/'Eggs', the empty-vs-populated distinction
|
|
- apps/pwa/src/routes/ListsIndex.tsx — container `role="list"`; renders `<ListsEmptyState/>` when `lists.length === 0`; list cards render via ListCard
|
|
- apps/pwa/src/components/ListCard.tsx — each card is `role="listitem"` with a link `aria-label={`Open list: ${list.name}`}` and the name as visible text
|
|
- apps/pwa/src/components/ListsEmptyState.tsx — heading `No lists yet`, body contains `Tap + to create your first shared list`
|
|
- apps/pwa/e2e/global-setup.ts (Plan 02) — the seeded list is named 'E2E Grocery List' (owner_id 1, is_shared, + list_shares row) — the stable anchor for the populated assertion
|
|
</read_first>
|
|
<action>
|
|
Create `apps/pwa/e2e/lists.spec.ts` importing `test, expect` from `@playwright/test`; both profiles via the matrix; relative `page.goto('/lists')` only.
|
|
POPULATED state (Rule 5, after global-setup seed): goto `/lists`; assert the seeded list card is visible — locate by its accessible link name `getByRole('link', { name: 'Open list: E2E Grocery List' })` (preferred, stable aria-label) OR `getByText('E2E Grocery List')`; assert `getByRole('listitem')` count ≥1; assert the ListsEmptyState text `No lists yet` is NOT present (`toHaveCount(0)`). Assert no horizontal overflow on the populated list view.
|
|
EMPTY state (Rule 5): the harness needs an emptied lists view. DO NOT mutate the shared seeded DB mid-suite (that would race the populated test and break determinism — D-06). Instead simulate the empty response at the network layer: register `await page.route('/api/lists', route => route.fulfill({ status: 200, body: JSON.stringify([]) }))` BEFORE goto `/lists`, then assert `getByText('No lists yet')` is visible and `getByText(/Tap \+ to create/)` is visible; assert no horizontal overflow in the empty state; then `page.unroute('/api/lists')`. (Confirm the exact lists endpoint path — `/api/lists` — by reading ListsIndex.tsx's query before finalizing the route glob.) This keeps the seeded populated state intact for the rest of the suite while still proving the empty state renders.
|
|
Both states must also satisfy Rule 2 (overflow) — assert it in each. Header comment names TEST-01 + run command.
|
|
</action>
|
|
<acceptance_criteria>
|
|
- `pnpm --filter @familysync/pwa exec playwright test lists.spec.ts` passes on both `iphone` and `pixel`
|
|
- populated test asserts the `E2E Grocery List` card visible (by link aria-label or text) AND `getByText('No lists yet')` count is 0
|
|
- empty test routes `/api/lists` to a 200 empty array before goto, asserts `No lists yet` + `Tap + to create` visible, then unroutes
|
|
- both states assert `scrollWidth <= clientWidth`
|
|
- no absolute URL in the file; the seeded DB is not mutated by the spec (empty state is network-simulated)
|
|
</acceptance_criteria>
|
|
<verify>
|
|
<automated>cd apps/pwa && pnpm exec playwright test lists.spec.ts 2>&1 | tail -15; grep -cE "https?://localhost" e2e/lists.spec.ts</automated>
|
|
</verify>
|
|
<done>lists.spec.ts asserts the seeded populated list and the (network-simulated) empty state on both profiles, with overflow checks and no mutation of the shared seed.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
| spec → dev PWA/API | Playwright drives the authed PWA via DEV_AUTH_BYPASS; read-only assertions + in-process page.route mocks; no real form writes |
|
|
| harness → production | the auth posture asserted here (dev bypass) must never be the production posture |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
| --------- | ---------------------- | -------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
| T-07-09 | Elevation of Privilege | DEV_AUTH_BYPASS reaching production | mitigate | The spec asserts the authed PWA was reached via the bypass on the DEV stack only; the bypass is API-side and guarded by `NODE_ENV !== 'production'` (devBypass.ts). Production compose must not set it (documented in Plan 02 README). The spec does not enable the bypass; it depends on the dev stack having it. |
|
|
| T-07-10 | Spoofing | OIDC/auth mocking masking a broken auth path | accept (designed out) | No OIDC mock is used (D-01) — auth comes from the real dev-bypass middleware; the auth-precondition test asserts genuine authed content, not a faked session. |
|
|
| T-07-11 | Tampering | page.route mocks leaking between tests | mitigate | Every `page.route` (events 500, lists empty) is paired with `page.unroute` (or `{ times }`) so the mock does not bleed into the populated/auth tests; the shared seeded DB is never mutated by a spec (empty state is network-simulated, not a DB delete). |
|
|
|
|
</threat_model>
|
|
|
|
<verification>
|
|
- `playwright test calendar.spec.ts lists.spec.ts` green on both `iphone` and `pixel`.
|
|
- Calendar: populated (grid visible, empty-absent), error (heading + Retry ≥44px + no overflow, mock unrouted), auth-bypass reached + no SW controller.
|
|
- Lists: populated (seeded card visible, empty-absent), empty (network-simulated 'No lists yet'), overflow clean in both.
|
|
- No absolute URLs; no DB mutation from specs.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
|
|
- calendar.spec.ts + lists.spec.ts cover populated / empty / error states on both profiles (UI-SPEC Rules 4/5).
|
|
- TEST-02 precondition (authed reach via DEV_AUTH_BYPASS, no OIDC mock, no SW controller) asserted at runtime.
|
|
- Mocks are scoped and unrouted; seeded data is left intact.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/07-mobile-test-harness/07-04-SUMMARY.md` when done.
|
|
</output>
|