Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
4.9 kB · 129 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130/** * 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`, );});