import { test as setup, expect } from "@playwright/test"; import { execFileSync } from "node:child_process"; import { mkdirSync, readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; /** * One-time setup for the characterization net (the `setup` project the tests depend on): * 1. preflight — the localdev :8080 stack must be up (owned by `siftlode dev`); * 2. seed — reset the dedicated E2E account to a known baseline, in-container (idempotent); * 3. authenticate ONCE via password-login and persist the session + `en` locale to * storageState, so the real specs never re-auth (auth endpoints are rate-limited). */ const STORAGE = "e2e/.auth/state.json"; const EMAIL = process.env.E2E_EMAIL || "e2e@siftlode.local"; const PASSWORD = process.env.E2E_PASSWORD || "e2e-golden-master"; const CONTAINER = process.env.E2E_API_CONTAINER || "siftlode-api-1"; const BASE_URL = process.env.PW_BASE_URL || "http://127.0.0.1:8080"; const HERE = dirname(fileURLToPath(import.meta.url)); const SEED_SCRIPT = resolve(HERE, "../../scripts/e2e_seed.py"); // The in-container seed only makes sense when the target is the local stack. const isLocalTarget = /\/\/(127\.0\.0\.1|localhost)[:/]/.test(BASE_URL); setup("seed + authenticate", async ({ page }) => { // 1. Preflight: fail fast (with a useful message) if the stack isn't running. const health = await page.request.get("/healthz"); expect( health.ok(), `localdev stack not reachable at ${BASE_URL} — start it with \`siftlode dev\``, ).toBeTruthy(); // 2. Idempotent seed inside the api container (skippable / local-only). if (!process.env.E2E_SKIP_SEED && isLocalTarget) { try { const out = execFileSync( "docker", ["exec", "-i", "-e", `E2E_EMAIL=${EMAIL}`, "-e", `E2E_PASSWORD=${PASSWORD}`, CONTAINER, "python", "-"], { input: readFileSync(SEED_SCRIPT) }, ); console.log(`[e2e seed] ${out.toString().trim()}`); } catch (err: unknown) { const e = err as { stderr?: Buffer; message?: string }; throw new Error(`E2E seed failed: ${e.stderr?.toString() || e.message}`); } } // 3. Authenticate. page.request shares the browser context's cookie jar, so the `session` // cookie it receives is captured by storageState below. const login = await page.request.post("/auth/password-login", { data: { email: EMAIL, password: PASSWORD }, }); expect(login.ok(), "password-login failed — is the E2E account seeded?").toBeTruthy(); const me = await (await page.request.get("/api/me")).json(); expect(me.authenticated, "session not established after login").toBeTruthy(); // 4. Pin the UI locale and suppress the first-login onboarding wizard (a full-screen overlay // that would intercept every click — the test account has no YouTube connection, so it // auto-opens). The dismissed flag is per-account: `siftlode.onboarding.dismissed.`. await page.goto("/"); await page.evaluate((id: number) => { localStorage.setItem("siftlode.lang", "en"); localStorage.setItem(`siftlode.onboarding.dismissed.${id}`, "1"); }, me.id); mkdirSync(dirname(STORAGE), { recursive: true }); await page.context().storageState({ path: STORAGE }); });