import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); /** Declarations of the block that starts at `opener`, up to its closing brace. */ function blockTokens(opener: string): Map { const start = globalStyles.indexOf(opener); assert.notEqual(start, -1, `missing block: ${opener}`); const end = globalStyles.indexOf("\n}", start); assert.notEqual(end, -1, `unterminated block: ${opener}`); const body = globalStyles.slice(start, end); return new Map([...body.matchAll(/--([\w-]+)\s*:\s*([^;]+);/g)] .map((hit) => [hit[1], hit[2].trim().replace(/\s+/g, " ")])); } const light = blockTokens(":root {\n color-scheme: light;"); const preferred = blockTokens(':root:not([data-theme="light"]) {'); const pinned = blockTokens(':root[data-theme="dark"] {'); // A token whose value is the same in both themes on purpose. Everything else // must be redefined, or dark mode ships a light-theme value on a dark ground. const themeNeutral = new Set([ "font-display", "font-body", "font-mono", "ease-out", "composer-reserve", ]); test("the OS-preference and pinned dark blocks never drift apart", () => { // Plain CSS cannot share a declaration list, so the dark palette is written // twice. This is the guard that keeps the copies honest. assert.deepEqual([...pinned.keys()].sort(), [...preferred.keys()].sort()); for (const [token, value] of pinned) { assert.equal(preferred.get(token), value, `--${token} differs between the two dark blocks`); } }); test("every themeable token has a dark value", () => { const themeable = [...light.keys()].filter((token) => ( !themeNeutral.has(token) && (token.startsWith("color-") || token.startsWith("shadow-") || token.startsWith("report-") || token === "ring-hairline" || token === "sheen") )); const missing = themeable.filter((token) => !pinned.has(token)).sort(); assert.deepEqual(missing, [], `no dark value for: ${missing.join(", ")}`); assert.ok(themeable.length >= 30, `only ${themeable.length} themeable tokens found`); }); test("both themes declare color-scheme so form controls and scrollbars follow", () => { assert.equal(light.get("color-scheme") ?? "light", "light"); assert.match(globalStyles, /:root:not\(\[data-theme="light"\]\) \{\n\s*color-scheme: dark;/); assert.match(globalStyles, /:root\[data-theme="dark"\] \{\n\s*color-scheme: dark;/); }); test("dark ink reads lighter than dark canvas, and the action stays legible", () => { const luminance = (hex: string) => { const channel = (value: number) => { const c = value / 255; return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; }; const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)); return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(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); }; // Checking only the reading surface missed ink-tertiary and action on the // raised card (--color-canvas-muted). New text or surface tokens must join // this matrix; do not drop a pair or lower 4.5 to make it pass. const inks = [ "color-ink", "color-ink-strong", "color-ink-secondary", "color-ink-tertiary", "color-action", "color-danger", "color-success", "color-warning", ]; const surfaces = [ "color-canvas", "color-canvas-soft", "color-canvas-muted", "color-sidebar-solid", ]; const failures: string[] = []; for (const ink of inks) { const inkValue = pinned.get(ink)!; assert.match(inkValue, /^#[0-9a-f]{6}$/i, `--${ink} must be a hex so contrast is checkable`); for (const surface of surfaces) { const surfaceValue = pinned.get(surface)!; assert.match( surfaceValue, /^#[0-9a-f]{6}$/i, `--${surface} must be a hex so contrast is checkable`, ); const ratio = contrast(inkValue, surfaceValue); if (ratio < 4.5) { failures.push(`--${ink} (${inkValue}) on --${surface} (${surfaceValue}) is ${ratio.toFixed(2)}:1`); } } } assert.equal(failures.length, 0, failures.join("; ")); // Elevation reads through lightness on dark: floor is darkest, raised steps up. const steps = ["color-canvas-soft", "color-canvas", "color-canvas-muted", "color-canvas-strong"] .map((token) => luminance(pinned.get(token)!)); for (let i = 1; i < steps.length; i += 1) { assert.ok(steps[i] > steps[i - 1], "dark surfaces must get lighter as they rise"); } }); test("the previous dark action and tertiary hexes are gone from product sources", () => { const sources = [ globalStyles, readFileSync(new URL("../src/app/error.tsx", import.meta.url), "utf8"), readFileSync(new URL("../src/app/not-found.tsx", import.meta.url), "utf8"), readFileSync(new URL("../src/app/forbidden.tsx", import.meta.url), "utf8"), readFileSync(new URL("../src/app/global-error.tsx", import.meta.url), "utf8"), readFileSync(new URL("../DESIGN.md", import.meta.url), "utf8"), ]; for (const source of sources) { assert.doesNotMatch(source, /#d4785a/i); } assert.equal(pinned.get("color-action"), "#d78064"); assert.equal(pinned.get("color-focus"), "#d78064"); assert.equal(pinned.get("color-ink-tertiary"), "#9c988e"); assert.equal(preferred.get("color-action"), "#d78064"); assert.equal(preferred.get("color-ink-tertiary"), "#9c988e"); });