Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
4.6 kB · 117 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118/** * Text has to grow when the reader asks it to. * * A browser's "default font size" setting lands on the root element, so a rule * in rem or em follows it and a rule in px does not. This stylesheet was * written in rem throughout — sixty font sizes, all relative — except for one * `16px` in the `font:` shorthand on body. * * That single absolute value pinned everything inheriting from body while * everything sized in rem carried on scaling, so a reader at 200% got a * masthead and a footer at twice the size around body copy, headings and FAQ * questions that had not moved. The chrome grew and the words they came for * did not. It is WCAG 1.4.4, and it fails in the worst way: not by refusing to * scale, but by scaling half the page. * * So the rule is the whole class of bug rather than that one declaration: no * absolute font size anywhere in the stylesheet. There is nothing to catch at * runtime — the page renders identically at the default size, which is the * size everyone testing it is using. * * 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 css = await readFile( fileURLToPath(new URL("../src/styles.css", import.meta.url)), "utf8",);
/** Lengths that ignore the reader's setting. */const ABSOLUTE = /\b\d*\.?\d+(px|pt|pc|in|cm|mm|Q)\b/;
/** The line a match sits on, for an error message worth reading. */function lineOf(index) { return css.slice(0, index).split("\n").length;}
test("no font-size is an absolute length", () => { const bad = []; for (const found of css.matchAll(/font-size:\s*([^;]+);/g)) { if (ABSOLUTE.test(found[1])) { bad.push(`line ${lineOf(found.index)}: font-size: ${found[1].trim()}`); } } assert.deepEqual( bad, [], `a reader who raises their browser's default text size is ignored here:\n ${bad.join("\n ")}`, );});
test("no font shorthand carries an absolute size", () => { const bad = []; // The shorthand's size sits before the family list, and the family list can // hold anything, so only the part up to the first comma is checked. for (const found of css.matchAll(/\bfont:\s*([^;,]+)/g)) { if (ABSOLUTE.test(found[1])) { bad.push(`line ${lineOf(found.index)}: font: ${found[1].trim()}`); } } assert.deepEqual( bad, [], `the font shorthand sets a size the reader cannot change:\n ${bad.join("\n ")}`, );});
test("body sizes itself from the root, so the setting reaches it", () => { const body = css.slice( css.indexOf("\nbody {"), css.indexOf("\n}", css.indexOf("\nbody {")), ); assert.notEqual(body.length, 0, "the body rule moved or was renamed"); // Any rem, not 1rem. The unit is the invariant — it is what carries the // reader's setting through — and the number is a typographic decision that // moved once already: --font-text is semi-condensed and sets a smaller // x-height than the system sans it replaced, so body went to 1.05rem to put // that back. Pinning the number made this test fail for the one reason it // does not care about. assert.match( body, /font:\s*\d*\.?\d+rem\//, "body no longer takes its size from the root element", );});
// The other half of the same failure: text that scales but cannot wrap. A word// with no break opportunity in it sets the width of whatever holds it, and at a// large text size that width is wider than a phone — so the whole document// scrolls sideways rather than the one line. Both rules below hold text that is// routinely one long token: an error names an origin or an id, and the hero's// headline is set in rem with a floor that keeps growing after the viewport has// stopped.//// `anywhere` and not `break-word`: only `anywhere` lets the break count towards// the intrinsic width, which is what stops the container being sized by the// unbroken word in the first place. `break-word` was tried on the headline and// changed nothing measurable.function ruleFor(selector) { const at = css.indexOf(`\n${selector} {`); assert.notEqual(at, -1, `${selector} moved or was renamed`); return css.slice(at, css.indexOf("\n}", at));}
test("text that is one long token may break, so the page does not", () => { for (const selector of [".error", ".hero-copy h1"]) { assert.match( ruleFor(selector), /overflow-wrap:\s*anywhere/, `${selector} holds text with no spaces in it and cannot break it, so a ` + `long token sizes the page rather than wrapping`, ); }});