import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; const here = dirname(fileURLToPath(import.meta.url)); const css = readFileSync(join(here, "../src/index.css"), "utf8"); type Rgb = readonly [number, number, number]; /** A `--color-…` token from the theme as RGB, with its alpha if it has one. */ function token(name: string): { rgb: Rgb; alpha: number } { const value = new RegExp(`--color-${name}: ([^;]+);`).exec(css)?.[1]; if (!value) throw new Error(`no token --color-${name}`); if (value.startsWith("#")) { const n = parseInt(value.slice(1), 16); return { rgb: [(n >> 16) & 255, (n >> 8) & 255, n & 255], alpha: 1 }; } const [r, g, b, a] = value.match(/[\d.]+/g)!.map(Number); return { rgb: [r!, g!, b!], alpha: a ?? 1 }; } const over = (top: Rgb, alpha: number, under: Rgb): Rgb => [0, 1, 2].map((i) => top[i]! * alpha + under[i]! * (1 - alpha)) as unknown as Rgb; function luminance(rgb: Rgb): number { const [r, g, b] = rgb.map((v) => { const c = v / 255; return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; }); return 0.2126 * r! + 0.7152 * g! + 0.0722 * b!; } function contrast(a: Rgb, b: Rgb): number { const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x); return (hi! + 0.05) / (lo! + 0.05); } const TEXT = [ "ink-50", "ink-100", "ink-200", "ink-300", "ink-400", "ink-500", "action", "flight", "earned", "problem", ]; describe("theme (CR-029)", () => { it("self-hosts the title and body fonts", () => { expect(css).toMatch(/@import "@fontsource\/michroma\//); expect(css).toMatch(/@import "@fontsource\/instrument-sans\//); expect(css).toMatch(/--font-title: "Michroma"/); expect(css).toMatch(/--font-sans: "Instrument Sans"/); }); it("keeps the wide title face off labels and numbers", () => { expect(css).toMatch(/--font-display: "Instrument Sans"/); }); it("uses the plain ground behind screens, not a backdrop image", () => { expect(css).not.toContain("bg.webp"); expect(css).toContain("background-color: var(--color-space-950)"); }); it("turns motion off under prefers-reduced-motion", () => { expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)/); }); // A panel can sit over the Sun or a bright star, so white is the worst case. it.each(TEXT)("%s reads at 4.5:1 on the glass over dark space and over white", (name) => { const glass = token("glass"); const text = token(name).rgb; for (const behind of [token("space-950").rgb, [255, 255, 255] as Rgb]) { expect(contrast(text, over(glass.rgb, glass.alpha, behind))).toBeGreaterThanOrEqual(4.5); } }); it.each([ ["on-action", "action"], ["on-problem", "problem"], ])("%s reads at 4.5:1 on %s", (text, fill) => { expect(contrast(token(text).rgb, token(fill).rgb)).toBeGreaterThanOrEqual(4.5); }); });