import { expect, test, type Page } from "@playwright/test"; const PAGE = "/"; const DESK = { light: "rgb(230, 230, 226)", dark: "rgb(58, 58, 58)" }; let errors: string[] = []; test.beforeEach(async ({ page }) => { errors = []; page.on("pageerror", (e) => errors.push(String(e))); }); test.afterEach(() => expect(errors).toEqual([])); const open = async (page: Page) => { await page.goto(PAGE); await expect(page.locator(".model .block").first()).toBeAttached(); }; /** The hero desk's top face colour. */ const desk = (page: Page) => page.evaluate(() => { const top = document.querySelector(".model .block .face.top")!; return getComputedStyle(top).fill; }); const lamp = (page: Page) => page.getByRole("button", { name: "Switch to dark" }); test("the lamp turns the page dark in a wave", async ({ page }) => { await open(page); expect(await page.evaluate(() => document.documentElement.dataset.theme)).toBe("light"); expect(await desk(page)).toBe(DESK.light); // Read the nearest and farthest hero blocks mid-wave. const mid = await page.evaluate(async () => { const tops = [...document.querySelectorAll(".model .block")]; document.querySelector(".night")!.click(); const wave = (g: SVGGElement) => parseFloat(g.style.getPropertyValue("--wave")); tops.sort((a, b) => wave(a) - wave(b)); const [near, far] = [tops[0], tops[tops.length - 1]]; const fill = (g: SVGGElement) => getComputedStyle(g.querySelector(".face.top")!).fill; // Sample between the near landing and the far start. const gap = (wave(near) + 0.2 + wave(far)) / 2; await new Promise((r) => setTimeout(r, gap * 1000)); return { steps: [wave(near), wave(far)], near: [fill(near), near.style.getPropertyValue("--tone-dark")], far: [fill(far), far.style.getPropertyValue("--tone")], }; }); expect(mid.steps[0] + 0.25).toBeLessThan(mid.steps[1]); expect(mid.steps[1]).toBeLessThanOrEqual(1); const rgb = (hex: string) => `rgb(${[1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)).join(", ")})`; expect(mid.near[0]).toBe(rgb(mid.near[1])); expect(mid.far[0]).toBe(rgb(mid.far[1])); await expect.poll(() => desk(page), { timeout: 1200 }).toBe(DESK.dark); expect(await page.evaluate(() => document.documentElement.dataset.theme)).toBe("dark"); await expect(page.getByRole("button", { name: "Switch to light" })).toBeAttached(); await expect(page.locator(".night-sky")).toHaveCount(0); await expect(page.locator("html")).toHaveCSS("background-color", "rgb(37, 37, 37)"); }); test("dark text keeps 4.5:1 against the page", async ({ page }) => { await page.emulateMedia({ colorScheme: "dark" }); await open(page); const worst = await page.evaluate(() => { const parse = (c: string) => c.match(/[\d.]+/g)!.slice(0, 3).map(Number); const lum = (c: string) => { const [r, g, b] = parse(c).map((v) => { const s = v / 255; return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4; }); return 0.2126 * r + 0.7152 * g + 0.0722 * b; }; const page = lum(getComputedStyle(document.documentElement).backgroundColor); const ratios = [...document.querySelectorAll("h1, h2, h3, p, a, li, button")] .filter((el) => el.textContent?.trim() && getComputedStyle(el).color !== "rgba(0, 0, 0, 0)") .map((el) => { const text = lum(getComputedStyle(el).color); return { ratio: (Math.max(text, page) + 0.05) / (Math.min(text, page) + 0.05), text: el.textContent }; }); return ratios.sort((a, b) => a.ratio - b.ratio)[0]; }); expect(worst.ratio, String(worst.text)).toBeGreaterThanOrEqual(4.5); }); test("the system scheme sets the first theme; the lamp's choice stays", async ({ page }) => { await page.emulateMedia({ colorScheme: "dark" }); await open(page); expect(await page.evaluate(() => document.documentElement.dataset.theme)).toBe("dark"); expect(await desk(page)).toBe(DESK.dark); await page.emulateMedia({ colorScheme: "light" }); await expect.poll(() => page.evaluate(() => document.documentElement.dataset.theme)).toBe("light"); await page.emulateMedia({ reducedMotion: "reduce" }); await lamp(page).click(); expect(await desk(page)).toBe(DESK.dark); await page.emulateMedia({ colorScheme: "light" }); await page.reload(); expect(await page.evaluate(() => document.documentElement.dataset.theme)).toBe("dark"); }); test("reduced motion switches at once", async ({ page }) => { await page.emulateMedia({ reducedMotion: "reduce", colorScheme: "light" }); await open(page); await lamp(page).click(); expect(await desk(page)).toBe(DESK.dark); await expect(page.locator(".night-sky")).toHaveCount(0); });