// The stylesheet has to keep picking from the scales. // // Before the token block went in, holdem.css used 42 different spacing values, 31 font // sizes, 14 border-radii and 139 colours, and defined three custom properties. None of // that was anybody's decision — each number was chosen at the moment it was needed, and // the result read as crowded even where nothing overlapped, because a 0.42rem gap next // to a 0.45rem gap next to a 0.5rem gap gives the eye no rhythm to lock onto. // // A one-off cleanup would last until the next `padding: 0.35rem`. So the scales are // checked here instead: a new value has to be a step on one of them, or an exception // with a reason written next to it. // // This reads the stylesheet as text rather than through a CSS parser, on purpose — the // rest of this suite has no dependencies and runs in a second, and a regex over // declarations is enough to catch the thing that actually happens, which is somebody // (me) typing a number instead of reaching for a token. const test = require("node:test"); const { describe } = require("node:test"); const assert = require("node:assert"); const fs = require("fs"); const path = require("path"); const CSS_PATH = path.join(__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "css", "casino", "holdem.css"); const css = fs.readFileSync(CSS_PATH, "utf8"); // Comments carry measurements and rationale — "measured at 342px tall on a phone" — // and none of that is a declaration. Strip them before looking for values. const declarations = css.replace(/\/\*[\s\S]*?\*\//g, ""); // Everything after the token block. The :root block is where the raw values are // supposed to be. const body = declarations.slice(declarations.indexOf("#potBadgeAmount,")); const valuesIn = (properties, source = body) => { const found = []; const re = new RegExp(`\\b(${properties.join("|")})\\s*:([^;}]*)`, "g"); let m; while ((m = re.exec(source)) !== null) { const [, prop, value] = m; // calc() and env() do their own arithmetic on tokens; the tokens inside them // are still checked, because the regex below looks at bare numbers only. (value.match(/(? found.push({ prop, value: v })); } return found; }; describe("the spacing scale", () => { // Hairlines and structural clearances, each one deliberate: // 0px/1px/3px/4px/22px — borders, hairline offsets, and the 22px of room a seat // puck's name gives the dealer button beside it. // 5rem/9rem — how far the page has to scroll past the fixed action // bar. Not rhythm; clearance for a thing of a known size. const ALLOWED = new Set(["0px", "1px", "2px", "3px", "4px", "22px", "5rem", "9rem"]); test("every gap, margin and padding comes off the scale", () => { const offenders = valuesIn( ["padding", "padding-top", "padding-right", "padding-bottom", "padding-left", "margin", "margin-top", "margin-right", "margin-bottom", "margin-left", "gap", "row-gap", "column-gap"]) .filter(v => !ALLOWED.has(v.value)); assert.deepEqual(offenders, [], "use var(--sp-0 … --sp-6), or add the value to ALLOWED with a reason"); }); test("and the scale is the one the tokens define", () => { ["--sp-0", "--sp-1", "--sp-2", "--sp-3", "--sp-4", "--sp-5", "--sp-6"] .forEach(token => assert.match(css, new RegExp(`\\${token}:`), `${token} is not defined`)); }); }); describe("the type scale", () => { // 16px — the size below which iOS zooms the page when a field is focused, so // every text input states it. Not a typographic choice. // 1.05em, 0.44rem — relative bumps inside a component, and the shrink-to-fit for // a six-figure stack on a 58px seat puck. const ALLOWED = new Set(["16px", "1.05em", "0.44rem"]); test("every font-size comes off the scale", () => { const offenders = valuesIn(["font-size"]).filter(v => !ALLOWED.has(v.value)); assert.deepEqual(offenders, [], "use var(--fs-2xs … --fs-2xl), or add the value to ALLOWED with a reason"); }); test("and the scale is the one the tokens define", () => { ["--fs-2xs", "--fs-xs", "--fs-sm", "--fs-base", "--fs-md", "--fs-lg", "--fs-xl", "--fs-2xl"] .forEach(token => assert.match(css, new RegExp(`\\${token}:`), `${token} is not defined`)); }); }); describe("the corner scale", () => { test("every border-radius comes off it", () => { const offenders = valuesIn(["border-radius"]).filter(v => v.value !== "2px"); assert.deepEqual(offenders, [], "use var(--r-sm | --r-md | --r-lg | --r-pill)"); }); }); describe("the tokens themselves", () => { test("every var() the stylesheet reaches for exists", () => { const used = new Set((css.match(/var\((--[a-zA-Z0-9-]+)/g) || []).map(s => s.slice(4))); // Published from JS at runtime rather than declared here — see actionBar.js. const fromScript = new Set(["--actionBarHeight", "--feltRoomPx"]); // Defined by the site's own stylesheet, not this one. const fromSite = new Set(["--font-mono"]); const missing = [...used].filter(t => !fromScript.has(t) && !fromSite.has(t) && !new RegExp(`\\${t}\\s*:`).test(css)); assert.deepEqual(missing, []); }); test("and the dark surfaces stay off pure black", () => { // Shadows are invisible on #000, so depth has to come from a lighter surface — // and white text on true black is the pairing that smears on an OLED phone, // which is what this tool is used on. const surfaces = [...css.matchAll(/--surface-\d+:\s*(#[0-9a-fA-F]{3,8})/g)].map(m => m[1].toLowerCase()); assert.ok(surfaces.length >= 4, "the surface ramp should have four steps"); surfaces.forEach(hex => { assert.notEqual(hex, "#000"); assert.notEqual(hex, "#000000"); }); }); }); describe("numbers that change while the dealer watches", () => { test("are set in tabular figures", () => { // A "1" is narrower than an "8" in a proportional face, so a pot ticking from // $18 to $21 shuffles its digits sideways and the eye reads movement where // there is only arithmetic. assert.match(css, /font-variant-numeric:\s*tabular-nums/); ["#potBadgeAmount", ".seatPuckStack", ".actionPot", ".standingsRow"] .forEach(sel => assert.ok(css.includes(sel), `${sel} should be in the tabular-figures list`)); }); }); describe("controls that must not resize under a thumb", () => { test("the undo pill has a fixed width, not a content width", () => { // It names the last action, so its label changes on every tap. Measured across // one hand before this: 148, 110, 109, 108, 109, 110, 120. const rule = css.match(/\.actionUndo\s*\{[^}]*\}/); assert.ok(rule, ".actionUndo rule not found"); assert.match(rule[0], /width:\s*[0-9.]+rem/); }); test("and its label is the part that ellipsizes", () => { const rule = css.match(/\.actionUndoLabel\s*\{[^}]*\}/); assert.ok(rule, ".actionUndoLabel rule not found"); assert.match(rule[0], /text-overflow:\s*ellipsis/); }); });