Files
davegilligan-new/tests/smoke.spec.ts
T
daveadmin c5464aa0aa test: add Playwright smoke suite (cookie banner, lang switcher, FamilyAtlas, WritingIssue)
34 tests across desktop + mobile (Chromium/Pixel 5). Verifies all client-side
React islands and interactive features that astro build/WebFetch cannot confirm.
API-dependent WritingIssue shows graceful error against local preview; resolves
on the live site where the PHP backend is available.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 19:40:41 +02:00

172 lines
6.9 KiB
TypeScript

import { test, expect, type Page } from "@playwright/test";
async function getCookie(page: Page, name: string): Promise<string | undefined> {
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();
});