Phase 8: Gitea CI — runner probe + PR gating jobs (fast-checks + api) #3
@@ -50,7 +50,12 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
|
||||
- **No stale storage-state** (Pitfall 14): use `DEV_AUTH_BYPASS=true` for the automated harness rather than a checked-in storage-state.json with an expiring session cookie; decide the auth strategy before the first test.
|
||||
- **Service worker block** (Pitfall 15): set `serviceWorkers: 'block'` (or explicitly unregister) in the context so a previous run's SW does not intercept requests / return stale cached responses; verify no SW-sourced responses in the trace.
|
||||
- Hard constraints: targets the dev build via `DEV_AUTH_BYPASS` (DEV_AUTH_BYPASS user 1 has no CalDAV credential/calendars — verify layout/flows, not live event-create); real prod-service-worker / iOS-Safari-standalone mobile testing stays a human/device gate (out of scope).
|
||||
**Plans**: TBD
|
||||
**Plans**: 4 plans (3 waves)
|
||||
Plans:
|
||||
- [ ] 07-01-PLAN.md — Harness foundation: @playwright/test + WebKit/Chromium browsers, playwright.config.ts (iPhone/WebKit + Pixel/Chromium matrix, serviceWorkers block, env baseURL, vite webServer), vitest exclude, scripts (Wave 1)
|
||||
- [ ] 07-02-PLAN.md — global-setup.ts: /health readiness poll + deterministic mysql2 reset-and-seed (calendar id 10 INSERT IGNORE guard, list + items) + e2e README/guardrails (Wave 2)
|
||||
- [ ] 07-03-PLAN.md — layout.spec.ts: tap targets >=44px, no overflow, in-viewport, accessible names (UI-SPEC Rules 1-4) + harness self-validation injected-defect proofs (Wave 3)
|
||||
- [ ] 07-04-PLAN.md — calendar.spec.ts + lists.spec.ts: populated/empty/error states (Rules 4/5) + DEV_AUTH_BYPASS auth-reached + no-SW-controller precondition (Wave 3)
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 8: Gitea CI
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
---
|
||||
phase: 07-mobile-test-harness
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- apps/pwa/package.json
|
||||
- apps/pwa/playwright.config.ts
|
||||
- apps/pwa/vitest.config.ts
|
||||
- apps/pwa/tsconfig.json
|
||||
- package.json
|
||||
autonomous: true
|
||||
requirements: [TEST-01, TEST-02]
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "playwright test --list reports exactly two projects: iphone and pixel"
|
||||
- "Vitest does NOT pick up e2e/*.spec.ts files (no glob collision)"
|
||||
- "Both WebKit and Chromium browser engines are installed for @playwright/test"
|
||||
- "tsc --noEmit passes in apps/pwa with the new playwright.config.ts and e2e/ files in scope"
|
||||
artifacts:
|
||||
- path: "apps/pwa/playwright.config.ts"
|
||||
provides: "Two-project device matrix (iPhone/WebKit, Pixel/Chromium), serviceWorkers block, env baseURL, globalSetup ref, vite-only webServer, trace/artifact config"
|
||||
contains: "devices['iPhone 14']"
|
||||
- path: "apps/pwa/vitest.config.ts"
|
||||
provides: "exclude e2e/** so Vitest's default *.spec.ts glob does not collide with Playwright specs"
|
||||
contains: "exclude"
|
||||
- path: "apps/pwa/package.json"
|
||||
provides: "@playwright/test devDependency + test:e2e scripts"
|
||||
contains: "test:e2e"
|
||||
key_links:
|
||||
- from: "apps/pwa/playwright.config.ts"
|
||||
to: "apps/pwa/e2e/global-setup.ts"
|
||||
via: "globalSetup config option"
|
||||
pattern: "globalSetup.*global-setup"
|
||||
- from: "apps/pwa/playwright.config.ts"
|
||||
to: "PLAYWRIGHT_BASE_URL env"
|
||||
via: "use.baseURL env-driven"
|
||||
pattern: "PLAYWRIGHT_BASE_URL"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Stand up the Playwright test-harness foundation in `apps/pwa`: add `@playwright/test` as a dev dependency, install the WebKit + Chromium browser engines, author `playwright.config.ts` with the two-profile device matrix (iPhone/WebKit + Pixel/Chromium), and isolate the new `e2e/*.spec.ts` glob from the existing Vitest `*.spec.ts` default glob. This is the blocking dependency for the seed plan and all spec plans.
|
||||
|
||||
Purpose: Every downstream plan (global-setup, layout/calendar/lists specs) imports from `@playwright/test` and runs under this config. Nothing else in the phase can land until the config matrix, browser engines, and glob isolation exist.
|
||||
|
||||
Output: `apps/pwa/playwright.config.ts`, the `@playwright/test` dev dep + installed browsers, `vitest.config.ts` exclude, package.json scripts, and `e2e/` brought into the `tsc --noEmit` gate.
|
||||
</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-VALIDATION.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Install @playwright/test + browser engines, wire package.json scripts</name>
|
||||
<files>apps/pwa/package.json, package.json</files>
|
||||
<read_first>
|
||||
- apps/pwa/package.json — current scripts block (`dev`, `build`, `preview`, `typecheck`, `test`) and devDependencies; mirror naming
|
||||
- package.json (root) — workspace script convention: `pnpm --filter @familysync/<app> <script>`, `verb:modifier` naming (e.g. `dev:pwa`, `typecheck`)
|
||||
- .planning/phases/07-mobile-test-harness/07-RESEARCH.md § "Installation" + § "Package Legitimacy Audit" — pinned versions and the SUS-but-approved mysql2 note
|
||||
- .planning/phases/07-mobile-test-harness/07-PATTERNS.md § "apps/pwa/package.json (modify)" — exact script + devDependency additions
|
||||
</read_first>
|
||||
<action>
|
||||
Install via the pnpm workspace filter (NOT npx, NOT root): `pnpm --filter @familysync/pwa add -D @playwright/test` — pin to 1.60.0 (verified current in RESEARCH.md; do not float to `latest`). Then install both engines with system deps: `pnpm --filter @familysync/pwa exec playwright install --with-deps webkit chromium` (the `--with-deps` flag is mandatory — WebKit on Linux needs system libraries; this is the iphone profile's engine per D-04). Do NOT rely on the global `playwright-cli` Chromium — the harness brings its own browser store. Add three scripts to `apps/pwa/package.json` scripts block: `test:e2e` = `playwright test`, `test:e2e:ui` = `playwright test --ui`, `test:e2e:headed` = `playwright test --headed`. Add a root `package.json` workspace script `test:e2e` = `pnpm --filter @familysync/pwa test:e2e`. Confirm `@playwright/test` lands in `devDependencies` (not `dependencies`). mysql2 is already a project dep (used by apps/api) — do NOT add it here; global-setup (Plan 02) imports the existing one. No checkpoint is needed for the mysql2 SUS verdict per RESEARCH.md (already installed, official package).
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- `pnpm --filter @familysync/pwa exec playwright --version` prints a 1.60.x version
|
||||
- `apps/pwa/package.json` lists `@playwright/test` under `devDependencies` and has a `test:e2e` script equal to `playwright test`
|
||||
- root `package.json` has a `test:e2e` script delegating to `pnpm --filter @familysync/pwa test:e2e`
|
||||
- WebKit and Chromium binaries are resolvable: `pnpm --filter @familysync/pwa exec playwright install --dry-run webkit chromium` reports both already installed (or installs cleanly)
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>pnpm --filter @familysync/pwa exec playwright --version</automated>
|
||||
</verify>
|
||||
<done>@playwright/test@1.60.x is a devDependency in apps/pwa, WebKit+Chromium engines installed, and `test:e2e` scripts exist in both apps/pwa and root package.json.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Author playwright.config.ts (two-profile matrix, SW block, env baseURL, vite webServer)</name>
|
||||
<files>apps/pwa/playwright.config.ts</files>
|
||||
<read_first>
|
||||
- .planning/phases/07-mobile-test-harness/07-RESEARCH.md § "Code Examples — playwright.config.ts (complete)" + § "Pattern 1" — canonical config shape
|
||||
- .planning/phases/07-mobile-test-harness/07-PATTERNS.md § "apps/pwa/playwright.config.ts" — config-file shape, analog `defineConfig` convention from vitest.config.ts
|
||||
- .planning/phases/07-mobile-test-harness/07-UI-SPEC.md § "Device / Viewport Matrix" + § "Rule 7" + § "Rule 8" — descriptor strings, SW-block precondition, env-driven baseURL contract
|
||||
- apps/pwa/vitest.config.ts — `defineConfig` wrapper convention to mirror
|
||||
- apps/pwa/vite.config.ts — confirms dev server is :5173 and proxies /api, /health, /callback to :3000 (baseURL points at the vite origin; readiness hits proxied /health)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `apps/pwa/playwright.config.ts` importing `defineConfig, devices` from `@playwright/test`. Set `testDir: './e2e'`, `testMatch: '**/*.spec.ts'`, `fullyParallel: true`, `retries: process.env.CI ? 2 : 0`, `workers: process.env.CI ? 1 : undefined`, `reporter: process.env.CI ? 'github' : 'list'`, and `globalSetup: './e2e/global-setup.ts'` (Plan 02 creates that file — the reference is forward-declared and resolves at run time). In top-level `use`: `baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:5173'` (env-driven per D-08/Rule 8 — never a hardcoded host), `trace: 'on-first-retry'`, `video: 'on-first-retry'`, `screenshot: 'only-on-failure'`. Define exactly two `projects`: `{ name: 'iphone', use: { ...devices['iPhone 14'], serviceWorkers: 'block' } }` and `{ name: 'pixel', use: { ...devices['Pixel 7'], serviceWorkers: 'block' } }` — exact descriptor strings `'iPhone 14'` (WebKit) and `'Pixel 7'` (Chromium) per D-03/D-04; `serviceWorkers: 'block'` on BOTH per D-02/Pitfall 15. Add a `webServer` block managing vite ONLY (D-10): `command: 'pnpm --filter @familysync/pwa dev'`, `url:` same as baseURL, `reuseExistingServer: !process.env.CI`, `timeout: 120_000`. Do NOT add `storageState` anywhere (D-01/Pitfall 14 — auth comes from DEV_AUTH_BYPASS on the API, not a checked-in state file). Do NOT add `toHaveScreenshot` expectations or snapshot config (UI-SPEC Rule 6 — structural assertions only; Schedule-X drift). The webServer manages vite only — API + MariaDB + Redis stay caller-managed (D-09); do not try to start the API from webServer.
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- `pnpm --filter @familysync/pwa exec playwright test --list` lists exactly two projects named `iphone` and `pixel` (it will report 0 tests until specs land — that is expected; the project count is what matters here)
|
||||
- the config file contains `serviceWorkers: 'block'` in both project `use` blocks and contains no `storageState` key
|
||||
- the config references `PLAYWRIGHT_BASE_URL` for `baseURL` and `globalSetup: './e2e/global-setup.ts'`
|
||||
- the config contains `reuseExistingServer: !process.env.CI` and `command: 'pnpm --filter @familysync/pwa dev'` in webServer
|
||||
- grep finds no `toHaveScreenshot` and no `storageState` in the file
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>cd apps/pwa && npx playwright test --list 2>&1 | grep -E '\[iphone\]|\[pixel\]|projects' | head; grep -c "serviceWorkers: 'block'" playwright.config.ts</automated>
|
||||
</verify>
|
||||
<done>`playwright.config.ts` exists with two projects (iphone/WebKit, pixel/Chromium), `serviceWorkers: 'block'` on both, env-driven baseURL, globalSetup ref, vite-only webServer, no storageState, no screenshot assertions.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Isolate Vitest glob + bring e2e/ into the typecheck gate</name>
|
||||
<files>apps/pwa/vitest.config.ts, apps/pwa/tsconfig.json</files>
|
||||
<read_first>
|
||||
- apps/pwa/vitest.config.ts — current `test:` block (environment jsdom, globals, setupFiles, env TZ); no explicit include/exclude today
|
||||
- .planning/phases/07-mobile-test-harness/07-PATTERNS.md § "apps/pwa/vitest.config.ts (modify)" — the one-line `exclude` diff
|
||||
- .planning/phases/07-mobile-test-harness/07-RESEARCH.md § "Pitfall 1" + § "Anti-Patterns" (Vitest picks up *.spec.ts) — why exclude is mandatory
|
||||
- apps/pwa/tsconfig.json — current `include`; confirm whether `e2e/**` is already covered or must be added so playwright.config.ts + e2e specs pass `tsc --noEmit`
|
||||
- .planning/phases/07-mobile-test-harness/07-RESEARCH.md § "Project Constraints" — `tsc --noEmit` gate must cover e2e/
|
||||
</read_first>
|
||||
<action>
|
||||
In `apps/pwa/vitest.config.ts`, add `exclude: ['e2e/**', 'node_modules/**']` inside the existing `test:` block. This stops Vitest's default `**/*.{test,spec}.{js,ts,tsx}` glob from picking up `e2e/*.spec.ts` (which import `@playwright/test` and would throw `devices is not defined`/import errors under jsdom — Pitfall 1). Do NOT remove or narrow the existing `environment`, `globals`, `setupFiles`, or `env` keys. Then ensure `apps/pwa/tsconfig.json` brings `playwright.config.ts` and `e2e/**/*.ts` into the typecheck program so they pass the project-wide `tsc --noEmit` gate: if the current `include` is `["src"]` or similar and excludes the new files, add `"playwright.config.ts"` and `"e2e"` to `include` (or widen the glob). Verify `tsc --noEmit` is green after the change — but note e2e/global-setup.ts and the spec files do not exist yet (Plan 02/03/04 create them), so at this point the only e2e file to typecheck is whatever exists; the config + tsconfig wiring is the deliverable here, full e2e typecheck is re-verified per spec plan.
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- `apps/pwa/vitest.config.ts` `test:` block contains `exclude: ['e2e/**', 'node_modules/**']`
|
||||
- `pnpm --filter @familysync/pwa test` (vitest run) does NOT attempt to run any `e2e/*.spec.ts` file (no `@playwright/test` import errors); existing unit suite still passes
|
||||
- `pnpm --filter @familysync/pwa exec tsc --noEmit` exits 0 with `playwright.config.ts` in scope
|
||||
- `apps/pwa/tsconfig.json` include covers `playwright.config.ts` and `e2e`
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>cd apps/pwa && pnpm exec tsc --noEmit && pnpm exec vitest run 2>&1 | grep -vi 'e2e/.*spec' | tail -5</automated>
|
||||
</verify>
|
||||
<done>Vitest excludes `e2e/**`, the existing unit suite is green, and `tsc --noEmit` covers `playwright.config.ts` + `e2e/`.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| harness → dev API | Playwright drives the PWA which calls the API; the API runs with `DEV_AUTH_BYPASS=true` (dev only) |
|
||||
| repo → CI/production | config + scripts checked into the repo; must not leak dev-only auth posture into production |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-07-01 | Elevation of Privilege | `DEV_AUTH_BYPASS=true` reaching production | mitigate | Config sets no auth-bypass itself — bypass is API-side and guarded by `NODE_ENV !== 'production'` (devBypass.ts). This plan documents in the README (Plan 02/04) that production compose MUST NOT set `DEV_AUTH_BYPASS`. The harness config only assumes the dev stack already has it. |
|
||||
| T-07-02 | Information Disclosure | checked-in `storageState.json` with a real OIDC session | accept (designed out) | N/A by D-01 — `playwright.config.ts` deliberately omits `storageState`; auth comes from the dev bypass, never a session file. No session cookie is ever serialized into the repo. |
|
||||
| T-07-03 | Tampering | `@playwright/test` package install | mitigate | Pinned to 1.60.0 (official Microsoft package, 38.6M wk downloads — RESEARCH.md Package Legitimacy Audit, verdict OK/Approved). No `[ASSUMED]`/`[SUS]`/`[SLOP]` packages introduced; mysql2 (SUS-but-approved) is already a project dep and is not added here. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `playwright test --list` reports exactly the two projects `iphone` and `pixel`.
|
||||
- `vitest run` ignores `e2e/**` (no Playwright import errors); existing unit suite stays green.
|
||||
- `tsc --noEmit` green in apps/pwa with `playwright.config.ts` in scope.
|
||||
- Config contains `serviceWorkers: 'block'` (both profiles), env-driven baseURL, no `storageState`, no `toHaveScreenshot`.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- `@playwright/test@1.60.x` installed as a devDependency in apps/pwa with WebKit + Chromium engines available.
|
||||
- `playwright.config.ts` defines the iPhone/WebKit + Pixel/Chromium matrix with `serviceWorkers: 'block'`, env baseURL, globalSetup ref, and vite-only webServer.
|
||||
- Vitest no longer collides with `*.spec.ts`; typecheck gate covers e2e/.
|
||||
- `test:e2e` scripts exposed at apps/pwa and root.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/07-mobile-test-harness/07-01-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
phase: 07-mobile-test-harness
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["07-01"]
|
||||
files_modified:
|
||||
- apps/pwa/e2e/global-setup.ts
|
||||
- apps/pwa/e2e/README.md
|
||||
autonomous: true
|
||||
requirements: [TEST-02]
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "global-setup polls baseURL+/health and only proceeds once it returns 200 (fails fast with a clear message on timeout)"
|
||||
- "global-setup deterministically resets (TRUNCATE) then seeds: >=1 calendar_event on calendar_id=10, >=1 list owned by user 1, >=2 list_items, >=1 list_shares row for user 1 (D-05 populated half / D-07 seeding in global-setup)"
|
||||
- "Seeding is idempotent run-over-run (a second run produces the same row counts, no stale rows, no duplicate-key errors)"
|
||||
- "calendar_id=10 is guaranteed present via INSERT IGNORE INTO calendars before the event insert (works on a fresh CI DB and a populated dev DB)"
|
||||
artifacts:
|
||||
- path: "apps/pwa/e2e/global-setup.ts"
|
||||
provides: "Playwright globalSetup: /health readiness poll + mysql2 reset-and-seed against dev MariaDB"
|
||||
contains: "TRUNCATE"
|
||||
- path: "apps/pwa/e2e/README.md"
|
||||
provides: "Operator/CI run instructions + the DEV_AUTH_BYPASS / NODE_ENV production guardrail documentation"
|
||||
contains: "DEV_AUTH_BYPASS"
|
||||
key_links:
|
||||
- from: "apps/pwa/e2e/global-setup.ts"
|
||||
to: "dev MariaDB :3306"
|
||||
via: "mysql2 createConnection with DB_* env vars"
|
||||
pattern: "mysql.*createConnection"
|
||||
- from: "apps/pwa/e2e/global-setup.ts"
|
||||
to: "calendar_events.calendar_id=10"
|
||||
via: "INSERT IGNORE calendars guard then INSERT calendar_events"
|
||||
pattern: "INSERT IGNORE INTO calendars"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the Playwright `globalSetup` (seeding runs in global-setup per D-07) that the harness runs once before any spec: poll the PWA `/health` endpoint until the dev stack is ready (D-08 readiness gate), then deterministically reset-and-seed the dev MariaDB (D-06) so dev-bypass user 1 — who natively has no calendars or lists — renders populated calendar and list views. This is the populated half of the D-05 hybrid strategy (the explicit empty-state assertions live in Plan 04). Document the run/CI invocation and the `DEV_AUTH_BYPASS`/production guardrail.
|
||||
|
||||
Purpose: Without seeding, user 1's views are empty and the "populated state" assertions (UI-SPEC Rules 3/5) have nothing to assert against. Without the readiness poll, specs flake on ECONNREFUSED when the stack is still booting (especially in Phase 8 CI). This is the data + readiness precondition every spec plan depends on (TEST-02).
|
||||
|
||||
Output: `apps/pwa/e2e/global-setup.ts` and `apps/pwa/e2e/README.md`.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/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-VALIDATION.md
|
||||
@apps/pwa/playwright.config.ts
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: global-setup.ts — /health readiness poll + deterministic reset-and-seed</name>
|
||||
<files>apps/pwa/e2e/global-setup.ts</files>
|
||||
<read_first>
|
||||
- .planning/phases/07-mobile-test-harness/07-PATTERNS.md § "apps/pwa/e2e/global-setup.ts" — full global-setup shape, exact column names, the INSERT IGNORE calendars guard, the seed SQL
|
||||
- .planning/phases/07-mobile-test-harness/07-RESEARCH.md § "Pattern 2" + § "Pitfall 4" + § "Pitfall 2" — health poll, seed, calendar_id=10 FK guard, globalSetup has no Playwright fixtures (plain Node only)
|
||||
- apps/api/src/db/client.ts — EXACT mysql2 env-var names to reuse: `DB_HOST` (default 127.0.0.1, NOT localhost — per memory api-integration-test-db), `DB_PORT` (3306), `DB_USER` (familysync), `DB_PASSWORD`, `DB_NAME` (familysync)
|
||||
- apps/api/src/db/schema.ts — confirm exact column names: calendars(`id`,`user_id`,`url`,`display_name`,`color`,`is_shared`); calendar_events(`calendar_id`,`uid`,`etag`,`raw_vevent`,`title`,`dtstart_utc` TIMESTAMP,`dtstart_date` DATE,`all_day`,`has_rrule`); lists(`id`,`owner_id`,`name`,`is_shared`); list_shares(`list_id`,`user_id`); list_items(`list_id`,`text`,`checked`,`rank`)
|
||||
- apps/api/src/auth/devBypass.ts — DEV_USER.id === 1 (seed targets user_id=1 / owner_id=1)
|
||||
- apps/api/tests/routes/lists.test.ts — existing seed-helper INSERT shapes for lists/list_items/list_shares (Drizzle there; global-setup uses raw mysql2 but the table/column shape is identical)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `apps/pwa/e2e/global-setup.ts` exporting a default async function (Playwright globalSetup signature; seeding-in-global-setup is D-07). Use ONLY plain Node APIs — `fetch` (native in Node 22) and `mysql2/promise` — NO `@playwright/test` imports (globalSetup runs outside the worker context; importing `page`/`test` throws — Pitfall 2). Step 1 (readiness, D-08): read `baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:5173'`; loop with a 60s deadline polling `fetch(baseURL + '/health')`, break on `res.ok`, swallow ECONNREFUSED, sleep 1000ms between attempts; if the deadline passes without a 200, throw an Error with a clear message (e.g. ``health check never returned 200 at ${baseURL}/health — is the dev stack up?``) so the run fails fast. Step 2 (seed the populated half of the D-05 hybrid, deterministically per D-06): `mysql.createConnection` using the exact env-var names from db/client.ts (`DB_HOST` default `'127.0.0.1'`, `DB_PORT` 3306, `DB_USER` familysync, `DB_PASSWORD`, `DB_NAME` familysync). In a try/finally (finally calls `conn.end()`): `SET FOREIGN_KEY_CHECKS=0`; TRUNCATE in FK-safe order `list_items`, `list_shares`, `lists`, `calendar_events`; `SET FOREIGN_KEY_CHECKS=1`. Then the calendar_id=10 FK guard (Pitfall 4): `INSERT IGNORE INTO calendars (id, user_id, url, display_name, color, is_shared) VALUES (10, 1, 'https://caldav.fastmail.com/dav/calendars/e2e/', 'FamilySync', '#4A90D9', true)` — IGNORE makes it a no-op on the dev DB where row 10 already exists and creates it on a fresh CI DB. Then seed one TIMED (not all-day) calendar_event onto calendar_id=10 with a deterministic uid `'e2e-seed-event-001'`, title `'Seeded Test Event'`, a future `dtstart_utc` (ISO UTC string ~tomorrow), a minimal VCALENDAR/VEVENT `raw_vevent`, `etag='e2e-etag-001'`, `all_day=false`, `has_rrule=false`. Then seed one list `INSERT INTO lists (owner_id, name, is_shared) VALUES (1, 'E2E Grocery List', true)`, capture `insertId` as `listId`, then `INSERT INTO list_shares (list_id, user_id) VALUES (?, 1)` and `INSERT INTO list_items (list_id, text, checked, rank) VALUES (?, 'Milk', false, 'a0'), (?, 'Eggs', false, 'a1')`. Use fractional-indexing rank strings `'a0'`/`'a1'` (active items render top-section). Seed BOTH `lists.owner_id=1` AND a `list_shares` row (Open Question 3 — belt-and-suspenders so `/api/lists` returns the list whether it filters by owner or by share). Determinism note: because every run truncates first, a second run yields identical row counts — no INSERT IGNORE on the seed rows themselves (D-06 mandates truncate+insert, NOT insert-if-absent). The list name `'E2E Grocery List'` and item texts `'Milk'`/`'Eggs'` are the stable anchors the lists spec (Plan 04) asserts on — do not change them without updating that spec.
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- `apps/pwa/e2e/global-setup.ts` imports `mysql2/promise` and contains NO `@playwright/test` import
|
||||
- it polls `${baseURL}/health` in a bounded loop and throws on timeout
|
||||
- it executes TRUNCATE on list_items, list_shares, lists, calendar_events (FK checks toggled around it)
|
||||
- it contains `INSERT IGNORE INTO calendars` with `VALUES (10, 1, ...)` before the calendar_events insert
|
||||
- it inserts a calendar_event with `calendar_id` 10, a list owned by user 1, a list_shares row (user_id 1), and exactly two list_items (`Milk`, `Eggs`)
|
||||
- running it twice in a row against the dev DB leaves exactly: 1 calendar_event on cal 10, 1 list, 1 list_shares, 2 list_items (idempotent) — verify with the SQL count in the verify block
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>cd apps/pwa && PLAYWRIGHT_BASE_URL="${PLAYWRIGHT_BASE_URL:-http://localhost:5173}" node --import tsx -e "import('./e2e/global-setup.ts').then(m=>m.default()).then(async()=>{const mysql=await import('mysql2/promise');const c=await mysql.createConnection({host:process.env.DB_HOST??'127.0.0.1',port:Number(process.env.DB_PORT??3306),user:process.env.DB_USER??'familysync',password:process.env.DB_PASSWORD??'',database:process.env.DB_NAME??'familysync'});const[e]=await c.query('SELECT COUNT(*) n FROM calendar_events WHERE calendar_id=10');const[li]=await c.query('SELECT COUNT(*) n FROM list_items');console.log('events_cal10=',e[0].n,'list_items=',li[0].n);await c.end();process.exit((e[0].n>=1&&li[0].n>=2)?0:1)})"</automated>
|
||||
</verify>
|
||||
<done>global-setup polls /health, then deterministically resets and seeds calendar_id=10 (with the INSERT IGNORE guard), one shared list owned by user 1, a list_shares row, and two list_items — idempotent run-over-run.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: e2e/README.md — run instructions + DEV_AUTH_BYPASS / production guardrail</name>
|
||||
<files>apps/pwa/e2e/README.md</files>
|
||||
<read_first>
|
||||
- .planning/phases/07-mobile-test-harness/07-RESEARCH.md § "Security Domain" + § "Pitfall 5" — the DEV_AUTH_BYPASS production guardrail and propagation requirement
|
||||
- .planning/phases/07-mobile-test-harness/07-CONTEXT.md § "D-09" — stack bring-up is the caller's responsibility (operator locally, compose in CI)
|
||||
- .planning/phases/07-mobile-test-harness/07-VALIDATION.md § "Manual-Only Verifications" — the auth-bypass-off manual check
|
||||
- docs/deployment.md (the "Running locally (host-side, no Docker)" subsection) — the canonical local dev-stack bring-up command to reference, not duplicate
|
||||
</read_first>
|
||||
<action>
|
||||
Create `apps/pwa/e2e/README.md` documenting how to run the harness and the security guardrails. Cover: (1) Prerequisites — the caller brings up the dev stack first (D-09): API + PWA dev servers + dev MariaDB (:3306) + Redis, with `DEV_AUTH_BYPASS=true` set in the API's environment BEFORE the API starts (Pitfall 5 — the harness cannot set it; it must already be active for the API to resolve to Dev User id 1). Reference docs/deployment.md's host-side run command rather than copying it. (2) Run commands: full suite `pnpm --filter @familysync/pwa test:e2e`; single fast profile `pnpm --filter @familysync/pwa exec playwright test --project=pixel`; headed debug `--headed`. (3) Env vars the harness reads: `PLAYWRIGHT_BASE_URL` (default http://localhost:5173), `DB_HOST` (default 127.0.0.1), `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` — all DB creds come from env, NEVER hardcoded (Information Disclosure mitigation). (4) SECURITY GUARDRAIL (Elevation of Privilege): `DEV_AUTH_BYPASS=true` is dev-only — the API guards it behind `NODE_ENV !== 'production'`; the production compose file MUST NOT set `DEV_AUTH_BYPASS`. State this explicitly. (5) No `storageState`: the harness never reads or writes a session-state file (D-01); there is no expiring cookie to refresh, which is why it runs repeatably day-over-day (SC #3). (6) Note that Phase 8 CI consumes these specs unchanged and owns only the stack bring-up + readiness wait. Keep it concise — this is operator-facing reference, not a tutorial.
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- `apps/pwa/e2e/README.md` exists and documents the `pnpm --filter @familysync/pwa test:e2e` run command
|
||||
- it states `DEV_AUTH_BYPASS=true` must be set before the API starts and MUST NOT be set in production (NODE_ENV !== 'production' guard)
|
||||
- it lists the DB_* and PLAYWRIGHT_BASE_URL env vars and states DB creds are env-only (never hardcoded)
|
||||
- it states no storageState file is used (D-01)
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>grep -E 'DEV_AUTH_BYPASS|NODE_ENV|storageState|PLAYWRIGHT_BASE_URL|test:e2e' apps/pwa/e2e/README.md | grep -vc '^#'</automated>
|
||||
</verify>
|
||||
<done>e2e/README.md documents run commands, the env-var contract, and the DEV_AUTH_BYPASS/production + no-storageState guardrails.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| global-setup → dev MariaDB | direct mysql2 connection writes seed rows; credentials cross this boundary |
|
||||
| harness → dev API | the API runs with `DEV_AUTH_BYPASS=true`; the bypass must never be active in production |
|
||||
| repo → production | README + seed code checked into the repo; must not normalize the dev-bypass posture for production |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-07-04 | Elevation of Privilege | `DEV_AUTH_BYPASS=true` leaking to production | mitigate | README documents that the bypass is dev-only and guarded by `NODE_ENV !== 'production'` (devBypass.ts); production compose MUST NOT set `DEV_AUTH_BYPASS`. global-setup does not set it (it cannot — it must already be active on the API). |
|
||||
| T-07-05 | Information Disclosure | DB seed credentials | mitigate | global-setup reads `DB_HOST`/`DB_PORT`/`DB_USER`/`DB_PASSWORD`/`DB_NAME` from env exclusively (mirrors db/client.ts); no credential is hardcoded in the seed script or README. |
|
||||
| T-07-06 | Information Disclosure | checked-in OIDC session state | accept (designed out) | N/A by D-01 — no `storageState.json` is written; the seed touches only fixture rows, never an auth artifact. README states this explicitly. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- global-setup polls /health and fails fast on timeout.
|
||||
- Reset-and-seed yields >=1 calendar_event on calendar_id=10 and >=2 list_items, idempotent across two consecutive runs.
|
||||
- INSERT IGNORE calendars guard satisfies the FK on both a fresh CI DB and the populated dev DB.
|
||||
- README documents the DEV_AUTH_BYPASS/production guardrail, the env-only DB creds, and no-storageState.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- `apps/pwa/e2e/global-setup.ts` provides a readiness gate + deterministic reset-and-seed using only plain Node (fetch + mysql2), targeting user 1 / calendar 10.
|
||||
- Seeding is repeatable run-over-run with no stale rows or duplicate-key failures.
|
||||
- `apps/pwa/e2e/README.md` documents run, env, and security guardrails.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/07-mobile-test-harness/07-02-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
phase: 07-mobile-test-harness
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: ["07-01", "07-02"]
|
||||
files_modified:
|
||||
- apps/pwa/e2e/layout.spec.ts
|
||||
autonomous: true
|
||||
requirements: [TEST-01]
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "On both iphone and pixel profiles (D-03/D-04), BottomTabBar Calendar/Lists tabs each measure >=44x44 CSS px"
|
||||
- "The New Event FAB measures >=56x56 and the PhoneNav settings button >=44x44 on both profiles"
|
||||
- "Neither /calendar nor /lists has horizontal overflow (documentElement.scrollWidth <= clientWidth) on either profile"
|
||||
- "BottomTabBar is visible and fully in-viewport (bottom edge <= viewport height) on both mobile profiles"
|
||||
- "Every asserted interactive element is locatable by ARIA role + accessible name (no CSS-selector fallback)"
|
||||
- "The harness PROVABLY fails on injected defects: a forced 20px tap target fails Rule 1; a forced 2000px body width fails Rule 2; both pass after the injection is removed"
|
||||
artifacts:
|
||||
- path: "apps/pwa/e2e/layout.spec.ts"
|
||||
provides: "UI-SPEC Rules 1-4 assertions (tap targets, overflow, in-viewport, accessible names) + harness self-validation injected-defect proofs"
|
||||
contains: "boundingBox"
|
||||
key_links:
|
||||
- from: "apps/pwa/e2e/layout.spec.ts"
|
||||
to: "BottomTabBar aria-label='Main navigation' + 'Calendar'/'Lists' links"
|
||||
via: "getByRole('navigation'/'link', { name })"
|
||||
pattern: "getByRole"
|
||||
- from: "apps/pwa/e2e/layout.spec.ts"
|
||||
to: "page.addStyleTag injected-defect proof"
|
||||
via: "self-validation must-fail assertions"
|
||||
pattern: "addStyleTag"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Author `apps/pwa/e2e/layout.spec.ts`: the cross-route structural quality-bar assertions (UI-SPEC Rules 1-4) running on both the iPhone/WebKit and Pixel/Chromium profiles (per D-03 two-profile matrix + D-04 faithful engines) with `serviceWorkers: 'block'` on each context (D-02) — tap targets ≥44px, no horizontal overflow, critical elements visible and in-viewport, accessible names present. Bake in the harness self-validation: prove each core assertion FAILS on a deliberately injected defect, then PASSES once removed. This is the proof that the harness measures rendered geometry, not CSS source (TEST-01).
|
||||
|
||||
Purpose: These are the mobile-only defect classes the harness exists to catch (sub-44px touch targets, Schedule-X horizontal overflow). A green suite alone does not prove the assertions are live — the injected-defect proofs are the acceptance bar (07-VALIDATION.md § Harness Self-Validation).
|
||||
|
||||
Output: `apps/pwa/e2e/layout.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/src/components/BottomTabBar.tsx
|
||||
@apps/pwa/src/components/CalendarShell.tsx
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: layout.spec.ts — Rules 1-4 (tap targets, overflow, in-viewport, accessible names) on both profiles</name>
|
||||
<files>apps/pwa/e2e/layout.spec.ts</files>
|
||||
<read_first>
|
||||
- .planning/phases/07-mobile-test-harness/07-UI-SPEC.md § "Rule 1" (explicit element table + 44/56px thresholds + locator strategies), § "Rule 2" (overflow), § "Rule 3" (in-viewport + safe-area-inset), § "Rule 4" (accessible names table), § "Copywriting Contract" (exact aria-labels) — the authoritative assertion contract
|
||||
- .planning/phases/07-mobile-test-harness/07-PATTERNS.md § "apps/pwa/e2e/layout.spec.ts" — Playwright getByRole/boundingBox/page.evaluate pattern, analog from CalendarShell.test.tsx
|
||||
- .planning/phases/07-mobile-test-harness/07-RESEARCH.md § "Pattern 3" + § "Pattern 4" + § "layout.spec.ts skeleton" — boundingBox + overflow eval idioms
|
||||
- apps/pwa/src/components/BottomTabBar.tsx — confirms `aria-label="Main navigation"` (nav), `aria-label="Calendar"` / `aria-label="Lists"` links, `minHeight: '44px'`, and that it renders null on desktop via matchMedia('(max-width: 767px)')
|
||||
- apps/pwa/src/components/AppNav.tsx — settings button `aria-label` contains `— open settings`; AppNav ALSO exposes a nav with aria-label="Main navigation" AND Calendar/Lists links (see action: scope to avoid strict-mode double match)
|
||||
- apps/pwa/src/components/CalendarShell.tsx — FAB `aria-label="New Event"`; Retry `<button>Retry</button>` (text name); error heading `<h2>Couldn't load events</h2>`
|
||||
</read_first>
|
||||
<action>
|
||||
Create `apps/pwa/e2e/layout.spec.ts` importing `test, expect` from `@playwright/test`. Tests run against BOTH projects automatically (config matrix per D-03/D-04) — write profile-agnostic specs; do not hardcode viewport widths (read `page.viewportSize()` when needed). STRICT-MODE CAVEAT: both `BottomTabBar` (phone) and `AppNav`/`PhoneNav` expose a `navigation` landmark named "Main navigation" and `link`s named "Calendar"/"Lists" — a bare `getByRole('link', { name: 'Calendar' })` may match 2 elements and throw a strict-mode violation. Scope tap-target assertions to the BottomTabBar specifically: locate the bar via its nav landmark, then query links WITHIN it (e.g. `const bar = page.getByRole('navigation', { name: 'Main navigation' }).last()` or scope by the bottom-bar container, then `bar.getByRole('link', { name: 'Calendar' })`). Confirm the correct scoping by reading BottomTabBar.tsx vs AppNav.tsx before writing the locator; if both share the exact landmark name, disambiguate by position (bottom bar is the fixed-bottom one) or add a `.last()`/filter — document the chosen disambiguation in a comment. Implement, on `/calendar` (and `/lists` where the route applies):
|
||||
Rule 1 (tap targets, UI-SPEC table): BottomTabBar Calendar tab ≥44×44, Lists tab ≥44×44, PhoneNav settings button (`getByRole('button', { name: /open settings/i })`) ≥44×44, New Event FAB (`getByRole('button', { name: 'New Event' })`) ≥56×56 — measure via `await locator.boundingBox()`, assert non-null and width/height thresholds. (Retry button tap target is covered in the calendar error-state spec, Plan 04 — do not duplicate here.)
|
||||
Rule 2 (overflow): on `/calendar` and `/lists`, `page.evaluate(() => ({ scrollWidth: document.documentElement.scrollWidth, clientWidth: document.documentElement.clientWidth }))`, assert `scrollWidth <= clientWidth`. No allowed exceptions (Schedule-X overflow is the defect to catch).
|
||||
Rule 3 (in-viewport): assert the BottomTabBar nav `isVisible()` is true and its `boundingBox().y + height <= page.viewportSize()!.height` (safe-area-inset is 0 in emulation); assert the PhoneNav header is visible.
|
||||
Rule 4 (accessible names): the fact that the Rule 1 locators resolve by role+name already proves accessible names exist; additionally assert the navigation landmark `getByRole('navigation', { name: 'Main navigation' })` is present (scoped per the caveat above). Use relative `page.goto('/calendar')` / `page.goto('/lists')` — NEVER an absolute URL (resolves against config baseURL, Rule 8). Add the standard file header comment (PATTERNS.md § "Test file header comment convention") naming TEST-01 and the run command.
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- `pnpm --filter @familysync/pwa exec playwright test layout.spec.ts` passes on BOTH `iphone` and `pixel` projects (dev stack + seed up)
|
||||
- the spec asserts boundingBox width AND height ≥44 for BottomTabBar Calendar and Lists tabs and the settings button, and ≥56 for the New Event FAB
|
||||
- the spec asserts `scrollWidth <= clientWidth` on both `/calendar` and `/lists`
|
||||
- every interactive locator uses `getByRole(...)` with a `name` (no `page.locator('css=...')` / testid fallback for the asserted elements)
|
||||
- no absolute URL appears in the file (`grep -E "https?://" layout.spec.ts` returns nothing)
|
||||
- no strict-mode "resolved to N elements" error appears in the run output
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>cd apps/pwa && pnpm exec playwright test layout.spec.ts 2>&1 | tail -15; grep -cE "https?://localhost" e2e/layout.spec.ts</automated>
|
||||
</verify>
|
||||
<done>layout.spec.ts asserts UI-SPEC Rules 1-4 (tap targets, overflow, in-viewport, accessible names) on both profiles with role+name locators and relative URLs, no strict-mode collisions.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Harness self-validation — injected-defect must-fail proofs (Rules 1 and 2)</name>
|
||||
<files>apps/pwa/e2e/layout.spec.ts</files>
|
||||
<read_first>
|
||||
- .planning/phases/07-mobile-test-harness/07-VALIDATION.md § "Harness Self-Validation" — the four self-validation proofs; items 1 (tap-target injection) and 2 (overflow injection) are automatable here
|
||||
- .planning/phases/07-mobile-test-harness/07-RESEARCH.md § "Validation Architecture — Harness Self-Validation" — addStyleTag injection approach
|
||||
- Context7 /microsoft/playwright.dev — `page.addStyleTag`, `expect(...).rejects` / asserting a failing expectation, `test.step` (use Query Documentation if the addStyleTag + must-fail-then-pass pattern needs confirmation)
|
||||
</read_first>
|
||||
<action>
|
||||
Add a `test.describe('harness self-validation — injected defects', ...)` block to `layout.spec.ts` proving the Rule 1 and Rule 2 assertions are LIVE (measuring rendered geometry, not passing vacuously). Approach: do NOT structure these as tests that intentionally fail the suite — structure each as a single passing test that internally proves the assertion would have failed under a defect and passes after removal. For the tap-target proof: navigate to `/calendar`, inject `page.addStyleTag({ content: 'nav[aria-label="Main navigation"] a { min-height: 20px !important; height: 20px !important; }' })` (or the equivalently-scoped BottomTabBar selector), measure the Calendar tab boundingBox, assert its height is now < 44 (proving the measurement tracks the rendered box, not the source CSS). Then remove the injected style — use `page.addStyleTag` returning a handle and `handle.evaluate(el => el.remove())`, OR reload the page to drop the injected tag — re-measure and assert height ≥ 44 again. For the overflow proof: on `/calendar`, inject `page.addStyleTag({ content: 'body { width: 2000px !important; }' })`, evaluate scrollWidth/clientWidth, assert `scrollWidth > clientWidth` (defect detected), then remove/reload and assert `scrollWidth <= clientWidth` (clean). Each proof is one test that PASSES by demonstrating the fail→clean transition; the suite stays green while proving the assertions detect real defects. Keep these in the same file so they share the config matrix (run on both profiles). Confirm the addStyleTag-remove / reload approach against Context7 before finalizing if uncertain about handle lifecycle.
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- `layout.spec.ts` contains a self-validation describe block using `page.addStyleTag`
|
||||
- the tap-target proof asserts boundingBox height < 44 WHILE the 20px style is injected, and ≥ 44 after removal/reload
|
||||
- the overflow proof asserts scrollWidth > clientWidth WHILE the 2000px-width style is injected, and ≤ clientWidth after removal/reload
|
||||
- the full `layout.spec.ts` suite (Rules 1-4 + self-validation) is green on both profiles
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>cd apps/pwa && pnpm exec playwright test layout.spec.ts 2>&1 | tail -15; grep -c "addStyleTag" e2e/layout.spec.ts</automated>
|
||||
</verify>
|
||||
<done>Self-validation proofs in layout.spec.ts demonstrate the tap-target and overflow assertions fail under injected defects and pass once removed — confirming the harness measures rendered geometry. Suite green on both profiles.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| spec → dev PWA/API | Playwright drives the authed PWA (DEV_AUTH_BYPASS); read-only assertions, no form submission |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-07-07 | Tampering | injected `addStyleTag` defect styles leaking between tests | mitigate | Each self-validation proof removes its injected style (handle.remove or page.reload) within the same test before completing; styles are page-scoped and do not persist across navigations/contexts. No global state is mutated. |
|
||||
| T-07-08 | Information Disclosure | spec hardcoding a host/credential | mitigate | All navigation uses relative paths against the env-driven baseURL (Rule 8); no absolute URL or credential appears in the spec (grep-gated in acceptance). |
|
||||
| T-07-12 | Tampering | `serviceWorkers: 'block'` (D-02) not applied → SW intercepts and masks a real layout defect | mitigate | The block is set per-context in playwright.config.ts (Plan 01); these specs assume it and Plan 04 asserts no SW controller. A stale Workbox response cannot satisfy a boundingBox/overflow measurement, so the geometry assertions remain authoritative. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `playwright test layout.spec.ts` green on both `iphone` and `pixel`.
|
||||
- Tap-target (≥44/≥56) + overflow + in-viewport + accessible-name assertions present, role+name locators only, relative URLs only.
|
||||
- Self-validation proves Rule 1 and Rule 2 assertions fail under injected defects and recover.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- layout.spec.ts enforces UI-SPEC Rules 1-4 on both device profiles (D-03/D-04).
|
||||
- Harness self-validation proves the assertions are live (injected-defect must-fail-then-pass).
|
||||
- No strict-mode collisions; no absolute URLs.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/07-mobile-test-harness/07-03-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,166 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user