/** * The camo editor has to survive light mode, and the way it stops doing so is * always the same: a rule pairs a theme colour with a literal one. * * That is how the three roll buttons came to be `color: var(--ink)` on a fixed * near-black. In dark mode --ink is pale and the button reads; in light mode * --ink is near-black too and the button vanishes into its own background. * Nothing errors, no test that renders the page fails, and the control is * simply not there. * * So the rule this checks is structural rather than visual: inside the * editor's section of the stylesheet, a colour is a custom property. The * literals live in one token block at the top, each one a light-dark() pair, * and every rule below reads them. Anything else is the bug above waiting to * happen again. * * Run with `npm test`. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; const web = join(new URL(".", import.meta.url).pathname, ".."); const css = await readFile(join(web, "src/styles.css"), "utf8"); const START = "/* --- camo editor"; const start = css.indexOf(START); assert.notEqual(start, -1, "the camo editor section moved or was renamed"); // The section runs to the sign-in dialog, which is shared with the masthead // and is not the editor's to theme. const end = css.indexOf("/* --- the sign-in dialog", start); assert.notEqual(end, -1, "the section after the camo editor moved"); const section = css.slice(start, end); // The one place literals are allowed: the token block, which is the whole // point of having them. const tokensEnd = section.indexOf(".camo-editor canvas"); assert.notEqual(tokensEnd, -1, "the editor token block moved"); const rules = section.slice(tokensEnd); test("the editor defines its surfaces in both schemes", () => { const tokens = section.slice(0, tokensEnd); const declarations = [...tokens.matchAll(/(--camo-[a-z-]+):\s*([^;]+);/g)]; assert.ok(declarations.length >= 5, "expected a block of --camo-* tokens"); for (const [, name, value] of declarations) { // --camo-chip-ink is deliberately one value in both schemes: it forwards // to --ink, which the site already flips. if (name === "--camo-chip-ink") continue; assert.match( value.trim(), /^light-dark\(.+,.+\)$/, `${name} is not a light-dark() pair, so it keeps one value in both schemes`, ); } }); test("no rule in the editor hardcodes a colour", () => { const literals = [ ...rules.matchAll(/#[0-9a-fA-F]{3,8}\b/g), ...rules.matchAll(/\b(?:rgba?|hsla?)\(/g), ].map((m) => m[0]); assert.deepEqual( literals, [], "use a --camo-* token (or a site token) so light mode follows", ); });