112 lines
4.7 KiB
TypeScript
112 lines
4.7 KiB
TypeScript
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<SVGGElement>(".model .block")];
|
|
document.querySelector<HTMLButtonElement>(".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<HTMLElement>("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);
|
|
});
|