/** * The unit grid must not push the page sideways on a phone. * * `.camo-fleet` is three columns of actual-size sprites, and actual size is the * point: it is where a player finds out whether their camo survives being drawn * small. That makes its width a hard floor rather than something the layout can * squeeze, and a grid with a floor inside a flex row does not wrap โ€” it overran * the document. At 320px the whole page scrolled sideways by 14px, and every * 360px phone scrolled too. * * WCAG 1.4.10 puts the line at 320px, so the rule is: at 320px, three columns * either fit or there are not three columns. * * The arithmetic is here rather than a remembered breakpoint, because the * number moves the moment anyone touches the sprite size or a padding. If this * fails, it prints what the grid now needs and what the viewport has left. * * It counts sprites and chrome, not the unit names under them, so its numbers * come out a little under what a browser measures โ€” "Medium Mek ยท quad" is * wider than the sprite it labels and widens that column. That makes this a * floor: passing here is necessary and a browser is still the thing that says * the page does not scroll. * * Run with `npm test`. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; const src = fileURLToPath(new URL("../src/", import.meta.url)); const css = await readFile(`${src}styles.css`, "utf8"); const mech = await readFile(`${src}camo/mech.ts`, "utf8"); /** WCAG 1.4.10 reflow: no horizontal scrolling at this width. */ const REFLOW = 320; const ROOT_FONT = 16; function number(source, pattern, what) { const found = pattern.exec(source); assert.ok(found, `${what} moved or was renamed`); return Number(found[1]); } /** A CSS length in px. Only px and rem appear in these rules. */ function length(raw, what) { const found = /^([\d.]+)(px|rem)$/.exec(raw.trim()); assert.ok(found, `${what} is ${raw}, which this test cannot measure`); return Number(found[1]) * (found[2] === "rem" ? ROOT_FONT : 1); } /** The body of a rule, by its selector. */ function rule(selector) { const start = css.indexOf(`\n${selector} {`); assert.notEqual(start, -1, `${selector} moved or was renamed`); const end = css.indexOf("\n}", start); return css.slice(start, end); } function shorthandInline(body, property, what) { const found = new RegExp(`${property}:\\s*([^;]+);`).exec(body); assert.ok(found, `${what} moved or was renamed`); const parts = found[1].trim().split(/\s+/); // One value applies to both sides; two are block then inline. return length(parts.length > 1 ? parts[1] : parts[0], what); } test("three columns of units fit at 320px, or there are not three", () => { const sprite = number(mech, /SPRITE_W = (\d+)/, "SPRITE_W"); const button = rule(".camo-unit-btn"); const buttonPad = shorthandInline( button, "padding", ".camo-unit-btn padding", ); const buttonBorder = length( /border:\s*([\d.]+(?:px|rem))/.exec(button)[1], ".camo-unit-btn border", ); const gap = length( /gap:\s*([^;]+);/.exec(rule(".camo-fleet"))[1], ".camo-fleet gap", ); // Everything the page and the card spend either side of the grid. const around = 2 * shorthandInline(rule("#app"), "padding", "#app padding") + 2 * shorthandInline(rule(".card"), "padding", ".card padding"); const column = sprite + 2 * buttonPad + 2 * buttonBorder; const threeWide = 3 * column + 2 * gap; const available = REFLOW - around; if (threeWide <= available) return; // three still fit; nothing to reflow // They do not fit, so a narrow-width rule has to take a column away, and it // has to fire before the grid stops fitting rather than at some width // somebody liked the look of. const narrow = /@media \(max-width: (\d+)px\) \{\s*\.camo-fleet \{\s*grid-template-columns: repeat\((\d)/.exec( css, ); assert.ok( narrow, `three columns need ${threeWide}px and a ${REFLOW}px viewport leaves ${available}px, ` + `but no @media rule reduces .camo-fleet's columns`, ); const [, threshold, columns] = narrow; assert.ok( Number(columns) < 3, `the narrow rule still asks for ${columns} columns`, ); // The width at which three columns stop fitting. The breakpoint has to be at // least this wide or there is a band of phones still scrolling. const breaksAt = threeWide + around; assert.ok( Number(threshold) >= breaksAt, `.camo-fleet drops to ${columns} columns at ${threshold}px, but three stop ` + `fitting at ${breaksAt}px โ€” viewports between those two scroll sideways`, ); const fewer = Number(columns) * column + (Number(columns) - 1) * gap; assert.ok( fewer <= available, `${columns} columns need ${fewer}px and ${REFLOW}px leaves ${available}px`, ); });