import { test, expect, type Page } from "@playwright/test"; async function getCookie(page: Page, name: string): Promise { const cookies = await page.context().cookies(); return cookies.find((c) => c.name === name)?.value; } // ─── Cookie banner ──────────────────────────────────────────────────────────── test.describe("Cookie banner", () => { test.beforeEach(async ({ context }) => { await context.clearCookies(); }); test("appears on first visit", async ({ page }) => { await page.goto("/"); await expect(page.locator("[data-cookie-banner]")).toBeVisible(); }); test("accept all → banner hidden, cookie = all", async ({ page }) => { await page.goto("/"); await page.click('[data-cookie-accept="all"]'); await expect(page.locator("[data-cookie-banner]")).toBeHidden(); expect(await getCookie(page, "dg_cookie_preferences")).toBe("all"); }); test("preference persists on reload", async ({ page }) => { await page.goto("/"); await page.click('[data-cookie-accept="all"]'); await page.reload(); await expect(page.locator("[data-cookie-banner]")).toBeHidden(); }); test("essential only → cookie = essential", async ({ page }) => { await page.goto("/"); await page.click('[data-cookie-accept="essential"]'); await expect(page.locator("[data-cookie-banner]")).toBeHidden(); expect(await getCookie(page, "dg_cookie_preferences")).toBe("essential"); }); }); // ─── Language switcher ──────────────────────────────────────────────────────── test.describe("Language switcher", () => { test.beforeEach(async ({ context }) => { await context.clearCookies(); }); test("defaults to EN", async ({ page }) => { await page.goto("/"); await expect(page.locator("html")).toHaveAttribute("data-ui-lang", "en"); }); test("switches to NB and persists cookie", async ({ page }) => { await page.goto("/"); await page.click('[data-lang-button="nb"]'); await expect(page.locator("html")).toHaveAttribute("data-ui-lang", "nb"); expect(await getCookie(page, "dg_ui_lang")).toBe("nb"); }); test("switches to FR", async ({ page }) => { await page.goto("/"); await page.click('[data-lang-button="fr"]'); await expect(page.locator("html")).toHaveAttribute("data-ui-lang", "fr"); }); }); // ─── /ai-lab bug regression ─────────────────────────────────────────────────── test("no [object Object] on /ai-lab", async ({ page }) => { await page.goto("/ai-lab"); const content = await page.content(); expect(content).not.toContain("[object Object]"); }); // ─── WritingIssue ───────────────────────────────────────────────────────────── test("WritingIssue resolves to content or graceful error on /writing", async ({ page }) => { await page.goto("/writing"); // Wait for the island to mount and finish its API call (succeeds or fails) await page.waitForSelector(".writing-live, h2:text('The writing issue could not be loaded.')", { timeout: 10_000, }); // Must not still be in loading state await expect(page.locator("h2:has-text('Loading the Boris Vian issue...')")).not.toBeVisible(); // Either resolved content or graceful error must be visible — not blank const hasContent = await page.locator(".writing-live").isVisible(); const hasError = await page.locator("h2:has-text('The writing issue could not be loaded.')").isVisible(); expect(hasContent || hasError).toBeTruthy(); }); // ─── FamilyAtlas ───────────────────────────────────────────────────────────── test.describe("FamilyAtlas", () => { // Pre-set the cookie preference so the banner doesn't intercept clicks test.beforeEach(async ({ context }) => { await context.addCookies([ { name: "dg_cookie_preferences", value: "essential", domain: "localhost", path: "/" }, ]); }); test("renders on /family-lab", async ({ page }) => { await page.goto("/family-lab"); await page.waitForSelector(".family-atlas", { timeout: 8_000 }); await expect(page.locator(".family-atlas")).toBeVisible(); }); test("filter tab switches without crash", async ({ page }) => { await page.goto("/family-lab"); await page.waitForSelector(".family-atlas", { timeout: 8_000 }); const tabs = page.locator('[role="tablist"] [role="tab"]'); const count = await tabs.count(); if (count < 2) return; // skip if fewer than 2 tabs await tabs.nth(1).click(); await expect(page.locator(".family-atlas")).toBeVisible(); }); test("lightbox opens on tile click and closes with Escape", async ({ page }) => { await page.goto("/family-lab"); await page.waitForSelector(".family-atlas", { timeout: 8_000 }); const tiles = page.locator(".family-atlas__tile"); const tileCount = await tiles.count(); if (tileCount === 0) return; // skip if no photos await tiles.first().click(); await expect(page.locator('[role="dialog"]')).toBeVisible(); await page.keyboard.press("Escape"); await expect(page.locator('[role="dialog"]')).not.toBeVisible(); }); }); // ─── Console errors ─────────────────────────────────────────────────────────── for (const path of ["/", "/ai-lab", "/writing", "/family-lab"]) { test(`no unexpected console errors on ${path}`, async ({ page }) => { const errors: string[] = []; page.on("console", (msg) => { if (msg.type() === "error") errors.push(msg.text()); }); page.on("pageerror", (err) => errors.push(err.message)); await page.goto(path); // Allow React islands to mount and any API calls to settle await page.waitForTimeout(3_000); // Filter known benign errors: API 404s on /writing are expected locally const unexpected = errors.filter( (e) => !e.includes("/api/") && !e.includes("Failed to fetch") && !e.includes("NetworkError") && !e.includes("Failed to load resource"), ); expect(unexpected).toHaveLength(0); }); } // ─── Mobile layout ─────────────────────────────────────────────────────────── test("navigation is visible on mobile", async ({ page }) => { await page.goto("/"); await expect(page.locator("nav")).toBeVisible(); });