import { test, expect, type Locator, type Page } from "@playwright/test"; import { ICON_PALETTES, childIconPalette, markSvg } from "../../common/lib/brand"; import { resolveAccent } from "../../common/lib/accent"; import { installRoutes } from "./helpers"; // The family brand on an archive site (plans/brand-and-themes.md, slice S1): // the Found-line mark and the two-weight wordmark in the header, the parent // mark beside the footer credit, and the icon set rendered per build from the // site's accent. The fixture site (e2e/fixtures/sites/testsite/site.json) is // headerTitle "Fixture Header", wordmarkLead "Fixture", accent #cc3366. // The computed fill of each part of a BrandMark, plus what the site's accent // resolves to — so the lit line can be checked against the token it follows // without pinning a colour the themes slice will change. async function markFills(mark: Locator) { return mark.evaluate((svg) => { const fill = (tone: string) => getComputedStyle(svg.querySelector(`[data-tone="${tone}"]`) as Element).fill; const probe = document.createElement("span"); probe.style.color = "var(--brand-mark, var(--brand))"; document.body.append(probe); const accent = getComputedStyle(probe).color; probe.remove(); return { ground: fill("ground"), dim: fill("dim"), lit: fill("lit"), accent }; }); } test("the header link is the mark + the split wordmark, named by the header title", async ({ page, }) => { await installRoutes(page); await page.goto("/"); const home = page .getByRole("banner") .getByRole("link", { name: "Fixture Header", exact: true }); await expect(home).toBeVisible(); await expect(home).toHaveAttribute("href", "/"); // Two adjacent spans, no whitespace node between them: the lead is the // configured prefix, the suffix is the rest (its leading space included). const wordmark = home.locator("[data-wordmark]"); const parts = await wordmark.evaluate((w) => [...w.childNodes].map((n) => ({ tag: n.nodeName, text: n.textContent, weight: n instanceof Element ? getComputedStyle(n).fontWeight : null, display: n instanceof Element ? getComputedStyle(n).display : null, })), ); expect(parts).toEqual([ { tag: "SPAN", text: "Fixture", weight: "720", display: "inline" }, { tag: "SPAN", text: " Header", weight: "380", display: "inline" }, ]); const mark = home.locator("svg[data-brand-mark]"); await expect(mark).toHaveCount(1); await expect(mark).toHaveAttribute("aria-hidden", "true"); await expect(mark).toHaveAttribute("focusable", "false"); await expect(mark).toBeVisible(); }); test("the header mark is the ink tile, its lit line following the site's accent", async ({ page, }) => { await installRoutes(page); await page.goto("/"); const mark = page.getByRole("banner").locator("svg[data-brand-mark]"); const f = await markFills(mark); expect(f.ground).toBe("rgb(12, 10, 8)"); // ICON_PALETTES.child.ground expect(f.dim).toBe("rgb(107, 93, 71)"); // ICON_PALETTES.child.dim, #6b5d47 expect(f.lit).toBe(f.accent); expect(f.lit).not.toBe(f.dim); }); // The ring on dark (release 10, slice MR): on the dark base a site's ink tile // IS the page, so every mark's tile gets a 1px ring outside it in its own // palette's dim, following the ground's corner (rx 112 of 512). A box-shadow, // so the mark's box is the same on every base; light draws none. const BASE_KEY = "ytdlp-tb:base"; 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"); expect(await page.evaluate(() => document.documentElement.classList.contains("dark"))).toBe( base === "dark", ); } // The mark's visible box-shadow layers (Tailwind's composite lists four // transparent ones before the ring), its corner, and its laid-out size. async function markRing(mark: Locator) { return mark.evaluate((svg) => { const cs = getComputedStyle(svg); const r = svg.getBoundingClientRect(); const layers = cs.boxShadow === "none" ? [] : cs.boxShadow.split(/,(?![^(]*\))/); return { ring: layers.map((l) => l.trim()).filter((l) => !l.startsWith("rgba(0, 0, 0, 0)")), radius: cs.borderTopLeftRadius, size: [r.width, r.height], }; }); } test("on dark the header and footer marks' tiles have a 1px ring in their dim; on light none", async ({ page, }) => { await installRoutes(page); await page.goto("/"); const header = page.getByRole("banner").locator("svg[data-brand-mark]"); const footer = page.locator("footer svg[data-brand-mark]"); await onBase(page, "dark"); expect(await markRing(header)).toEqual({ ring: ["rgb(107, 93, 71) 0px 0px 0px 1px"], // ICON_PALETTES.child.dim, #6b5d47 radius: "21.875%", size: [28, 28], }); expect(await markRing(footer)).toEqual({ ring: ["rgb(88, 105, 119) 0px 0px 0px 1px"], // ICON_PALETTES.archilyzer.dim, #586977 radius: "21.875%", size: [16, 16], }); for (const base of ["light"] as const) { await onBase(page, base); expect(await markRing(header), base).toEqual({ ring: [], radius: "21.875%", size: [28, 28] }); expect(await markRing(footer), base).toEqual({ ring: [], radius: "21.875%", size: [16, 16] }); } }); test("the footer carries the parent mark before the credit, outside the link", async ({ page, }) => { await installRoutes(page); await page.goto("/"); const footer = page.locator("footer"); const credit = footer.getByRole("link", { name: "Archilyzer", exact: true }); await expect(credit).toBeVisible(); await expect(credit.locator("svg")).toHaveCount(0); const mark = footer.locator("svg[data-brand-mark]"); await expect(mark).toHaveCount(1); await expect(mark).toHaveAttribute("aria-hidden", "true"); // The mark's next sibling holds "Built with" and the credit. await expect( footer.locator("svg[data-brand-mark] + span").getByRole("link", { name: "Archilyzer", exact: true, }), ).toHaveCount(1); const f = await markFills(mark); expect(f.ground).toBe("rgb(21, 27, 32)"); // #151b20 expect(f.lit).toBe("rgb(231, 237, 241)"); // #e7edf1, bone: the parent mark }); test(" links the rendered icon set, SVG first", async ({ page }) => { await installRoutes(page); await page.goto("/"); const icons = await page .locator('head link[rel="icon"]') .evaluateAll((ls) => ls.map((l) => [l.getAttribute("href"), l.getAttribute("type"), l.getAttribute("sizes")]), ); expect(icons).toEqual([ ["/icons/icon.svg", "image/svg+xml", null], ["/icons/icon-32.png", "image/png", "32x32"], ["/icons/icon-192.png", "image/png", "192x192"], ["/icons/icon-512.png", "image/png", "512x512"], ]); await expect(page.locator('head link[rel="apple-touch-icon"]')).toHaveAttribute( "href", "/icons/apple-touch-icon.png", ); }); test("the site's icons are lit with its own accent, fitted to the ink tile", async ({ request, }) => { const lit = resolveAccent("#cc3366").dark; const res = await request.get("/icons/icon.svg"); expect(res.ok()).toBeTruthy(); expect(await res.text()).toBe(markSvg(childIconPalette(lit))); const maskable = await request.get("/icons/maskable.svg"); expect(await maskable.text()).toBe( markSvg(childIconPalette(lit), { variant: "maskable" }), ); // Not the parent mark: that is the hub's, the homepage's and the editor's. expect(await res.text()).not.toContain(ICON_PALETTES.archilyzer.ground); });