From 53498e340820ea40408712db2f7a865c46252fb3 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Thu, 11 Jun 2026 01:50:10 -0400 Subject: [PATCH] =?UTF-8?q?feat(07-02):=20implement=20globalSetup=20?= =?UTF-8?q?=E2=80=94=20/health=20readiness=20gate=20+=20deterministic=20DB?= =?UTF-8?q?=20seed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Poll baseURL/health with 60s timeout; throw on expiry (D-08 readiness gate) - TRUNCATE list_items, list_shares, lists, calendar_events (FK checks toggled) - INSERT IGNORE calendars guard for id=10 (Pitfall 4 CI resilience) - Seed one timed calendar_event on calendar_id=10 (uid: e2e-seed-event-001) - Seed E2E Grocery List (owner_id=1, is_shared=true) + list_shares + Milk/Eggs items - Add mysql2@3.22.4 devDependency to apps/pwa (needed for global-setup types) - No @playwright/test imports — plain Node.js only (Pitfall 2) --- apps/pwa/e2e/global-setup.ts | 115 +++++++++++++++++++++++++++++++++-- apps/pwa/package.json | 1 + pnpm-lock.yaml | 3 + 3 files changed, 113 insertions(+), 6 deletions(-) diff --git a/apps/pwa/e2e/global-setup.ts b/apps/pwa/e2e/global-setup.ts index ca86c58..e34f4d7 100644 --- a/apps/pwa/e2e/global-setup.ts +++ b/apps/pwa/e2e/global-setup.ts @@ -1,15 +1,118 @@ /** - * global-setup.ts — Phase 7 Plan 02 will implement this + * global-setup.ts — Playwright globalSetup: /health readiness gate + deterministic DB seed * - * Placeholder to satisfy Playwright config path resolution. - * Plan 02 replaces this with: - * - health poll on baseURL/health (D-08 readiness gate) - * - mysql2 DB seed: TRUNCATE + INSERT onto calendar id=10 + lists for user 1 (D-05/D-06/D-07) + * Runs ONCE before any spec file. Plain Node.js only (fetch + mysql2/promise). + * No @playwright/test imports — globalSetup runs outside the worker context (Pitfall 2). + * + * Step 1 (D-08 readiness gate): + * Poll baseURL/health until 200 OK (60s timeout, fail fast on expiry). + * + * Step 2 (D-05/D-06/D-07 deterministic seed): + * TRUNCATE → INSERT seed rows onto calendar_id=10 + lists for user_id=1. + * Idempotent: truncate-first guarantees the same row counts on every run. + * + * Anchor values the specs assert on — do NOT change without updating the specs: + * - calendar_events.title = 'Seeded Test Event' + * - lists.name = 'E2E Grocery List' + * - list_items.text = 'Milk', 'Eggs' * * Run: * pnpm --filter @familysync/pwa test:e2e */ +import mysql from 'mysql2/promise' + export default async function globalSetup(): Promise { - // Implemented in Plan 02 + // ── Step 1: Readiness gate (D-08) ─────────────────────────────────────────── + const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:5173' + const deadline = Date.now() + 60_000 + + while (Date.now() < deadline) { + try { + const res = await fetch(`${baseURL}/health`) + if (res.ok) break + } catch { + // ECONNREFUSED or network error — stack not ready yet, keep polling + } + await new Promise((r) => setTimeout(r, 1_000)) + } + + if (Date.now() >= deadline) { + throw new Error( + `health check never returned 200 at ${baseURL}/health — is the dev stack up?\n` + + `Ensure the API is running with DEV_AUTH_BYPASS=true and the Vite dev server is on ${baseURL}.`, + ) + } + + // ── Step 2: Reset-and-seed (D-06 deterministic, D-07 in globalSetup) ──────── + // Uses exact env-var names from apps/api/src/db/client.ts. + // DB_HOST defaults to '127.0.0.1' (NOT 'localhost') — per project memory api-integration-test-db. + const conn = 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', + }) + + try { + // Disable FK checks so TRUNCATE order is unconstrained + await conn.execute('SET FOREIGN_KEY_CHECKS=0') + await conn.execute('TRUNCATE TABLE list_items') + await conn.execute('TRUNCATE TABLE list_shares') + await conn.execute('TRUNCATE TABLE lists') + await conn.execute('TRUNCATE TABLE calendar_events') + await conn.execute('SET FOREIGN_KEY_CHECKS=1') + + // CI guard (Pitfall 4): ensure calendar row id=10 exists before inserting events. + // INSERT IGNORE is a no-op if the row already exists (dev DB), creates it if not (CI fresh DB). + await conn.execute( + `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)`, + ) + + // Seed one timed (not all-day) calendar event on shared calendar id=10. + // Uses a future dtstart_utc so the event is visible in the UI's default "upcoming" view. + const uid = 'e2e-seed-event-001' + const futureStart = new Date(Date.now() + 24 * 60 * 60 * 1000) // tomorrow UTC + const futureStartUtc = futureStart.toISOString().replace(/\.\d+Z$/, 'Z') // trim ms + const rawVevent = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//FamilySync E2E//EN', + 'BEGIN:VEVENT', + `UID:${uid}`, + `DTSTART:${futureStart.toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z')}`, + `DTEND:${new Date(futureStart.getTime() + 60 * 60 * 1000).toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z')}`, + 'SUMMARY:Seeded Test Event', + 'END:VEVENT', + 'END:VCALENDAR', + ].join('\r\n') + + await conn.execute( + `INSERT INTO calendar_events + (calendar_id, uid, etag, raw_vevent, title, dtstart_utc, all_day, has_rrule) + VALUES (10, ?, 'e2e-etag-001', ?, 'Seeded Test Event', ?, false, false)`, + [uid, rawVevent, futureStartUtc], + ) + + // Seed one shared list owned by user 1 (D-05 populated half). + // belt-and-suspenders: seed BOTH owner_id=1 AND a list_shares row for user_id=1 + // so /api/lists returns the list regardless of whether it filters by owner or by share. + const [listResult] = (await conn.execute( + `INSERT INTO lists (owner_id, name, is_shared) VALUES (1, 'E2E Grocery List', true)`, + )) as mysql.ResultSetHeader[] + const listId = listResult.insertId + + await conn.execute(`INSERT INTO list_shares (list_id, user_id) VALUES (?, 1)`, [listId]) + + // Two active items with fractional-indexing rank strings — appear in the active section. + // 'Milk' and 'Eggs' are the stable anchor texts that lists.spec.ts asserts on. + await conn.execute( + `INSERT INTO list_items (list_id, text, checked, rank) VALUES (?, 'Milk', false, 'a0'), (?, 'Eggs', false, 'a1')`, + [listId, listId], + ) + } finally { + await conn.end() + } } diff --git a/apps/pwa/package.json b/apps/pwa/package.json index ceef5f5..c25b3f9 100644 --- a/apps/pwa/package.json +++ b/apps/pwa/package.json @@ -35,6 +35,7 @@ }, "devDependencies": { "@playwright/test": "1.60.0", + "mysql2": "3.22.4", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@types/node": "^22.19.19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 41eb3f0..580ad23 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,6 +147,9 @@ importers: jsdom: specifier: ^26.1.0 version: 26.1.0 + mysql2: + specifier: 3.22.4 + version: 3.22.4(@types/node@22.19.19) typescript: specifier: ^5.5.0 version: 5.9.3