import { test, expect, type Page } from "@playwright/test"; import { resolveAccent } from "../../common/lib/accent"; import { FIXTURE_PALE_HEX, FIXTURE_SITES } from "./fixture-summary"; // Each Official Instances card names its site with the site's WORDMARK when // the summary carries its lead: the lead heavy and TINTED in the site's own // accent (a named accent's swatch on the base in force, a custom hex fitted to // it), the rest light. A site with a lead and no accent keeps the foreground; // a site with no lead shows its title plain, as before. The link's accessible // name is the plain title throughout. // // The fixture (fixture-summary.ts): Fixture One (Brass, lead "Fixture"), // Fixture Two (a pale custom hex, lead "Fixture"), Fixture Three (no accent, // lead "Fix" — mid-word, so a split name would read "Fix ture Three"), Four and // Five (neither), Six (Vermilion, no lead). const BASE_KEY = "ytdlp-tb:base"; const section = (page: Page) => page.locator("section").filter({ has: page.getByRole("heading", { level: 2, name: "Official Instances", exact: true }), }); const card = (page: Page, title: string) => section(page) .locator("li") .filter({ has: page.getByRole("link", { name: title, exact: true }) }); // The colour a CSS colour resolves to on this page. const resolve = (page: Page, css: string) => page.evaluate((c) => { const el = document.createElement("span"); el.style.color = c; document.body.append(el); const out = getComputedStyle(el).color; el.remove(); return out; }, css); function luminance(rgb: string): number { const [r, g, b] = (rgb.match(/[\d.]+/g) ?? []).slice(0, 3).map((n) => { const c = Number(n) / 255; return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; }); return 0.2126 * r + 0.7152 * g + 0.0722 * b; } const contrast = (a: string, b: string) => { const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x); return (hi + 0.05) / (lo + 0.05); }; async function wordmarkOf(page: Page, title: string) { return card(page, title).evaluate((li) => { const lead = li.querySelector("[data-wordmark-lead]"); const suffix = li.querySelector("[data-wordmark-suffix]"); const cs = (el: Element | null) => (el ? getComputedStyle(el) : null); return { lead: lead?.textContent ?? null, suffix: suffix?.textContent ?? null, leadWeight: Number(cs(lead)?.fontWeight ?? 0), suffixWeight: Number(cs(suffix)?.fontWeight ?? 0), leadColor: cs(lead)?.color ?? null, foreground: getComputedStyle(document.documentElement).getPropertyValue("--foreground").trim(), cardBg: getComputedStyle(li).backgroundColor, }; }); } test("each card's name: the wordmark where there is a lead, tinted in the site's accent at ≥ 3:1 on every base, and the plain title as its name", async ({ page, }) => { await page.goto("/"); for (const s of FIXTURE_SITES) { await expect(section(page).getByRole("link", { name: s.siteTitle, exact: true })).toHaveCount(1); } const pale = resolveAccent(FIXTURE_PALE_HEX); for (const base of ["light", "dark"] as const) { await page.evaluate(([k, v]) => localStorage.setItem(k, v), [BASE_KEY, base]); await page.reload(); await expect(page.locator("html")).toHaveAttribute("data-base", base); const want: Record = { "Fixture One": { lead: "Fixture", colour: await resolve(page, "var(--swatch-brass)") }, "Fixture Two": { lead: "Fixture", colour: await resolve(page, pale[base]) }, "Fixture Three": { lead: "Fix", colour: await resolve(page, "var(--foreground)") }, }; for (const [title, { lead, colour }] of Object.entries(want)) { const w = await wordmarkOf(page, title); expect(w.lead, `${title} lead`).toBe(lead); expect(w.suffix, `${title} suffix`).toBe(title.slice(lead.length)); expect(w.leadWeight, `${title}: the lead is heavier`).toBeGreaterThan(w.suffixWeight); expect(w.leadColor, `${title} on ${base}`).toBe(colour); expect( contrast(w.leadColor!, w.cardBg), `${title}'s lead against its card on ${base}`, ).toBeGreaterThanOrEqual(3); } // The named accent and the custom hex are tinted, not the foreground. const fg = await resolve(page, "var(--foreground)"); expect(want["Fixture One"].colour).not.toBe(fg); expect(want["Fixture Two"].colour).not.toBe(fg); // A lead that ends mid-word leaves the name whole: one accessible name, // and the text as a reader sees it, with no space inserted. const three = card(page, "Fixture Three").getByRole("link"); await expect(three).toHaveAccessibleName("Fixture Three"); expect((await three.innerText()).replace("↗", "").trim()).toBe("Fixture Three"); // No lead: the plain title, no wordmark — accent or not. for (const title of ["Fixture Four", "Fixture Five", "Fixture Six"]) { await expect(card(page, title).locator("[data-wordmark]"), title).toHaveCount(0); await expect(card(page, title).getByRole("link")).toHaveText(new RegExp(`^${title}`)); } } });