Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 0c7e4393bbeca4e43cfd3a5e7feb4387f6886541
parent f8259263659c84074bc5f03de179f101dd2d9642
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 25 Sep 2026 21:50:41 -0400

homepage: theme.spec — dark + Signal by default; REQUIRED_TOKENS complete on all three bases (brand S2)

The project site opens on the dark base in Signal even under a light OS; the
toggle to light persists across a `commit` reload. The palette test imports
REQUIRED_TOKENS from themeConfig and reads every token (plus --radius) off the
computed style of each base; the grounds are BASE_GROUNDS and the three bases'
backgrounds, foregrounds and chart-1 all differ.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mhomepage/e2e/theme.spec.ts | 196++++++++++++++++++++++++++++++++-----------------------------------------------
1 file changed, 80 insertions(+), 116 deletions(-)

diff --git a/homepage/e2e/theme.spec.ts b/homepage/e2e/theme.spec.ts @@ -1,145 +1,109 @@ import { test, expect, type Page } from "@playwright/test"; +import { REQUIRED_TOKENS } from "../../common/components/themeConfig"; +import { ACCENTS, BASE_GROUNDS } from "../../common/lib/brand"; // The shared theme system (common/styles/tokens.css + ThemeScript + -// ThemeProvider + ThemeToggle) as this site uses it. The project site commits to -// the "archilyzer" family — the instrument face — and defaults to dark. The mode -// toggle must persist across reloads and be applied before hydration (no flash -// of the wrong theme). -// -// This used to assert the "archive" family: the site wore the same warm-brass -// costume as the archives it builds, back when it was a shelf of them. +// ThemeProvider + ThemeToggle/ThemeMenu) as the project's own site uses it: it +// opens on the DARK base in Signal, the family's accent. A reader's base +// persists across reloads and is applied before hydration (no flash of the +// wrong theme). + +const BASE_KEY = "ytdlp-tb:base"; async function htmlState(page: Page) { - return page.evaluate(() => ({ - dark: document.documentElement.classList.contains("dark"), - theme: document.documentElement.getAttribute("data-theme"), - mode: localStorage.getItem("ytdlp-tb:mode"), - })); + return page.evaluate((key) => { + const d = document.documentElement; + return { + base: d.getAttribute("data-base"), + dark: d.classList.contains("dark"), + accent: d.getAttribute("data-accent"), + stored: localStorage.getItem(key), + brand: getComputedStyle(d).getPropertyValue("--brand").trim(), + }; + }, BASE_KEY); } -test("archilyzer dark default; mode toggle persists with no FOUC", async ({ +test("dark + Signal by default; the base toggle persists with no FOUC", async ({ page, }) => { + // The OS says light: the default is still dark (it is the site's, not the + // system's). + await page.emulateMedia({ colorScheme: "light" }); await page.goto("/"); - // Default: the instrument family, dark. - let s = await htmlState(page); - expect(s.theme).toBe("archilyzer"); - expect(s.dark).toBe(true); + await expect.poll(() => htmlState(page)).toEqual({ + base: "dark", + dark: true, + accent: "signal", + stored: null, + brand: ACCENTS.signal.onDark, + }); const toggle = page.getByRole("button", { name: /switch to/i }); await expect(toggle).toBeVisible(); - // Cycle to an explicit light mode (dark → system → light) — deterministic - // regardless of the runner's OS color-scheme preference. - for (let i = 0; i < 3; i++) { - s = await htmlState(page); - if (s.mode === "light") break; - await toggle.click(); - } - s = await htmlState(page); - expect(s.mode).toBe("light"); + // Cycle to an explicit light base (dark → system → light). A click before + // hydration is lost, so click until it is stored; a click React took commits + // synchronously, so the check never races it into a second one. + await expect(async () => { + if ((await htmlState(page)).stored !== "light") await toggle.click(); + expect((await htmlState(page)).stored).toBe("light"); + }).toPass({ timeout: 10_000 }); + let s = await htmlState(page); + expect(s.base).toBe("light"); expect(s.dark).toBe(false); - expect(s.theme).toBe("archilyzer"); + expect(s.brand).toBe(ACCENTS.signal.onLight); - // Reload: the pre-paint inline script must re-apply the persisted mode on the - // very first commit, before React hydrates. + // Reload: the pre-paint inline script must re-apply the persisted base on + // the very first commit, before React hydrates. await page.reload({ waitUntil: "commit" }); - const onCommit = await page.evaluate(() => ({ - dark: document.documentElement.classList.contains("dark"), - mode: localStorage.getItem("ytdlp-tb:mode"), - })); - expect(onCommit.mode).toBe("light"); - expect(onCommit.dark).toBe(false); + await page.waitForFunction( + () => document.documentElement.dataset.themeReady === "1", + ); + s = await htmlState(page); + expect(s.stored).toBe("light"); + expect(s.base).toBe("light"); + expect(s.dark).toBe(false); }); -test("the family declares a complete palette in BOTH modes", async ({ +test("every base declares a complete palette, and the grounds differ", async ({ page, }) => { - // The failure this guards is silent: a family that omits a token inherits the - // BASE family's value, so a half-declared palette looks merely "a bit off" - // rather than broken — and only in the mode nobody checked. Light mode ships - // here because the theme toggle does. - const TOKENS = [ - "--background", - "--foreground", - "--card", - "--card-foreground", - "--popover", - "--popover-foreground", - "--primary", - "--primary-foreground", - "--secondary", - "--secondary-foreground", - "--muted", - "--muted-foreground", - "--accent", - "--accent-foreground", - "--destructive", - "--destructive-foreground", - "--destructive-soft", - "--border", - "--border-strong", - "--input", - "--ring", - "--surface", - "--faint", - "--panel", - "--panel-2", - "--success", - "--success-foreground", - "--success-soft", - "--warning", - "--warning-foreground", - "--warning-soft", - "--info", - "--info-foreground", - "--info-soft", - "--brand", - "--brand-strong", - "--brand-soft", - "--brand-ink", - "--state-gone", - "--state-gone-soft", - "--chart-1", - "--chart-2", - "--chart-3", - "--chart-4", - "--chart-5", - "--chart-surface", - "--chart-grid", - "--chart-axis", - "--chart-tooltip-bg", - "--radius", - ]; + // The failure this guards is silent: a base that omits a token inherits the + // LIGHT block's value (`:root` always matches), so a half-declared palette + // looks merely "a bit off" rather than broken — and only on the base nobody + // checked. themeTokens.test.ts checks the sheet; this checks the browser. + const TOKENS = [...REQUIRED_TOKENS, "--radius"]; await page.goto("/"); - const readAll = (names: string[]) => - page.evaluate((tokens) => { - const cs = getComputedStyle(document.documentElement); - return Object.fromEntries( - tokens.map((t) => [t, cs.getPropertyValue(t).trim()]), - ); - }, names); - - const setMode = (dark: boolean) => - page.evaluate((d) => { - localStorage.setItem("ytdlp-tb:mode", d ? "dark" : "light"); - document.documentElement.classList.toggle("dark", d); - }, dark); + const readOn = (base: "light" | "sepia" | "dark") => + page.evaluate( + ({ base, tokens }) => { + const d = document.documentElement; + d.setAttribute("data-base", base); + d.classList.toggle("dark", base === "dark"); + const cs = getComputedStyle(d); + return Object.fromEntries( + tokens.map((t) => [t, cs.getPropertyValue(t).trim()]), + ); + }, + { base, tokens: TOKENS }, + ); - await setMode(true); - const dark = await readAll(TOKENS); - await setMode(false); - const light = await readAll(TOKENS); + const bases = ["light", "sepia", "dark"] as const; + const palettes = {} as Record<(typeof bases)[number], Record<string, string>>; + for (const b of bases) palettes[b] = await readOn(b); - for (const t of TOKENS) { - expect(dark[t], `${t} must be set in dark mode`).not.toBe(""); - expect(light[t], `${t} must be set in light mode`).not.toBe(""); + for (const b of bases) { + for (const t of TOKENS) { + expect(palettes[b][t], `${t} must be set on the ${b} base`).not.toBe(""); + } + expect(palettes[b]["--background"]).toBe(BASE_GROUNDS[b]); } - // …and the two modes must actually differ, or "light mode" is a label on the - // dark palette. - expect(light["--background"]).not.toBe(dark["--background"]); - expect(light["--foreground"]).not.toBe(dark["--foreground"]); + // …and the three must actually differ, or a base is a label on another's + // palette. + expect(new Set(bases.map((b) => palettes[b]["--background"])).size).toBe(3); + expect(new Set(bases.map((b) => palettes[b]["--foreground"])).size).toBe(3); + expect(new Set(bases.map((b) => palettes[b]["--chart-1"])).size).toBe(3); });