import fs from "node:fs"; import path from "node:path"; import { test, expect, type Page } from "@playwright/test"; import { ACCENTS, MIN_ACCENT_CONTRAST, contrastRatio } from "../../common/lib/brand"; import { resolveAccent } from "../../common/lib/accent"; import { installRoutes } from "./helpers"; // A site shows its OWN accent, and a reader does not pick one. The fixture's // site.json sets the custom hex #cc3366, so the layout renders // data-accent="custom" with the hex fitted to each ground inline; `--brand` // follows the base in force. A reader's stored pick from an earlier build is // ignored — before paint and after hydration — and left where it is. const ACCENT_KEY = "ytdlp-tb:accent"; const BASE_KEY = "ytdlp-tb:base"; const FIXTURE = resolveAccent("#cc3366"); // {id: "custom", light, dark} async function state(page: Page) { return page.evaluate((key) => { const d = document.documentElement; const cs = getComputedStyle(d); return { accent: d.getAttribute("data-accent"), base: d.getAttribute("data-base"), stored: localStorage.getItem(key), brand: cs.getPropertyValue("--brand").trim(), background: cs.getPropertyValue("--background").trim(), }; }, ACCENT_KEY); } async function onBase(page: Page, base: "light" | "dark") { await page.evaluate(([k, b]) => localStorage.setItem(k, b), [BASE_KEY, base]); await page.reload({ waitUntil: "commit" }); await page.waitForFunction(() => document.documentElement?.dataset.themeReady === "1"); await page.waitForLoadState("load"); } test("a site wears its own accent, fitted to each base, and offers no accent picker", async ({ page, }) => { await page.emulateMedia({ colorScheme: "light" }); await page.goto("/"); await expect.poll(() => state(page)).toMatchObject({ accent: "custom", base: "light", stored: null, brand: FIXTURE.light, }); await expect(page.getByRole("button", { name: "Choose theme" })).toHaveCount(0); await expect(page.getByRole("menuitemradio")).toHaveCount(0); await onBase(page, "dark"); await expect.poll(() => state(page)).toMatchObject({ accent: "custom", base: "dark", background: "#0c0a08", brand: FIXTURE.dark, }); }); // Every value `data-accent` ever holds is recorded by a MutationObserver // installed before the page's first script, so a flash between two samples // cannot pass. test("a stored accent from before is ignored, with no flash, and left in place", async ({ page, }) => { await page.emulateMedia({ colorScheme: "light" }); await page.addInitScript((key) => { try { localStorage.setItem(key, "violet"); } catch {} const held: (string | null)[] = []; (window as unknown as { __accents: (string | null)[] }).__accents = held; new MutationObserver((records) => { for (const r of records) if (r.target === document.documentElement) held.push(r.oldValue); }).observe(document, { subtree: true, attributes: true, attributeFilter: ["data-accent"], attributeOldValue: true, }); }, ACCENT_KEY); await page.goto("/", { waitUntil: "commit" }); await page.waitForFunction(() => document.documentElement?.dataset.themeReady === "1"); expect((await state(page)).accent).toBe("custom"); await page.waitForLoadState("load"); await page.waitForTimeout(300); const held = await page.evaluate(() => [ ...(window as unknown as { __accents: (string | null)[] }).__accents, document.documentElement.getAttribute("data-accent"), ]); expect(held.filter((v) => v !== null && v !== "custom"), JSON.stringify(held)).toEqual([]); expect(await state(page)).toMatchObject({ accent: "custom", stored: "violet", brand: FIXTURE.light }); }); // The search bar's "Press Enter or click Search to apply" wears the site's // accent (release 10). It was `text-warning`, the same yellow on every site. // It follows the base like any `text-brand`, and stays readable on the page // ground it sits on. test("the unapplied-search hint wears the site's accent, readable on each base", async ({ page, }) => { await installRoutes(page); await page.emulateMedia({ colorScheme: "light" }); await page.goto("/"); const hint = page.getByText("Press Enter or click Search to apply"); // The hint's colour and the ground under it, as "#rrggbb". const paint = () => hint.evaluate((el) => { const hex = (rgb: string) => "#" + (rgb.match(/\d+/g) ?? []) .slice(0, 3) .map((n) => Number(n).toString(16).padStart(2, "0")) .join(""); return { color: hex(getComputedStyle(el).color), ground: hex(getComputedStyle(document.body).backgroundColor), }; }); const expectAccent = async (value: string) => { // Typing is an unapplied edit (the input mounts after hydration). await page.locator('input[data-testid^="leaf-query-"]').first().fill("alpha"); await expect(hint).toBeVisible(); await expect(hint).toHaveClass(/(^|\s)text-brand(\s|$)/); await expect(hint).not.toHaveClass(/text-warning/); await expect.poll(async () => (await paint()).color).toBe(value); const { color, ground } = await paint(); expect(contrastRatio(color, ground)).toBeGreaterThanOrEqual(MIN_ACCENT_CONTRAST); }; // The fixture site's own (custom) colour, fitted to each ground. await expectAccent(FIXTURE.light); await onBase(page, "dark"); await expectAccent(FIXTURE.dark); }); // A site with a NAMED accent (the fixture's is a custom hex): its accent's own // value on each base, and a reader's stored pick from before does not override // it. The fixture site is rewritten for the test and put back after; the // original rides along in `_e2ePristine`, which playwright.config.ts restores // if a run dies mid-test. test.describe("a site with a named accent", () => { const SITE_FILE = path.resolve(process.cwd(), "e2e", "fixtures", "sites", "testsite", "site.json"); let pristine = ""; test.describe.configure({ mode: "serial" }); test.beforeAll(() => { pristine = fs.readFileSync(SITE_FILE, "utf8"); }); test.afterEach(() => { fs.writeFileSync(SITE_FILE, pristine); }); test("wears its accent's value on Light and on Dark, and a stored pick does not override it", async ({ page, }) => { const site = JSON.parse(pristine) as Record; fs.writeFileSync( SITE_FILE, `${JSON.stringify({ ...site, accent: "violet", _e2ePristine: pristine }, null, 2)}\n`, ); await page.addInitScript((key) => { try { localStorage.setItem(key, "brass"); } catch {} }, ACCENT_KEY); await page.emulateMedia({ colorScheme: "light" }); await page.goto("/"); await expect.poll(() => state(page)).toMatchObject({ accent: "violet", base: "light", stored: "brass", brand: ACCENTS.violet.onLight, }); await onBase(page, "dark"); await expect.poll(() => state(page)).toMatchObject({ accent: "violet", base: "dark", stored: "brass", brand: ACCENTS.violet.onDark, }); }); });