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:
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);
});