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"); 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); }; function blockTokens(opener: string): Map { const start = globalStyles.indexOf(opener); assert.notEqual(start, -1, `missing block: ${opener}`); const end = globalStyles.indexOf("\n}", start); const body = globalStyles.slice(start, end); return new Map([...body.matchAll(/--([\w-]+)\s*:\s*([^;]+);/g)] .map((hit) => [hit[1], hit[2].trim()])); } test("the report palette is declared on :root, never on the reader element", () => { // A custom property declared on .personal-report-reader wins over :root no // matter what a media query says. Keeping it local made the dark override // dead: the paper stayed light while --color-ink went light with the theme, // which is white text on white paper. const reader = globalStyles.slice(globalStyles.indexOf(".personal-report-reader {")); const readerBody = reader.slice(0, reader.indexOf("\n}")); assert.doesNotMatch(readerBody, /--report-(paper|rule|accent)\s*:/); for (const token of ["report-paper", "report-rule", "report-accent"]) { assert.ok(blockTokens(":root {\n color-scheme: light;").has(token), `--${token} must live on :root`); } }); test("report ink clears AA on report paper in both themes", () => { const light = blockTokens(":root {\n color-scheme: light;"); const dark = blockTokens(':root[data-theme="dark"] {'); for (const [name, palette] of [["light", light], ["dark", dark]] as const) { const paper = palette.get("report-paper")!; assert.match(paper, /^#[0-9a-f]{6}$/i); for (const ink of ["color-ink", "color-ink-strong", "color-ink-secondary", "color-ink-tertiary", "report-accent"]) { const value = palette.get(ink)!; const ratio = contrast(value, paper); assert.ok( ratio >= 4.5, `${name}: --${ink} (${value}) on --report-paper (${paper}) is ${ratio.toFixed(2)}:1`, ); } } }); test("print pins the palette back to light so paper is always paper", () => { // Printing while the screen is dark forced white backgrounds but left every // ink light, which comes out of the printer blank. const print = globalStyles.slice(globalStyles.indexOf("@media print {")); assert.match(print, /:root \{\s*\n\s*color-scheme: light;/); const printRoot = blockTokens("@media print {\n /* Paper is always paper"); for (const token of ["color-ink", "color-ink-strong", "report-paper", "report-accent"]) { assert.ok(printRoot.has(token), `@media print must reset --${token}`); } assert.equal(printRoot.get("color-ink"), "#1d1d1f"); assert.equal(printRoot.get("report-paper"), "#f8f5ee"); });