// The sums the contrast check runs on, tested away from a browser. // // The check itself needs a page to look at, but the arithmetic underneath it doesn't — // and the arithmetic is the part that decides whether a real fault is reported or // waved through. Faded text used to be filed as "not checked" rather than measured; // this is the compositing that replaced that. const test = require("node:test"); const { describe } = require("node:test"); const assert = require("node:assert"); const LayoutAudit = require("./uiAudit"); const WHITE = { r: 255, g: 255, b: 255, a: 1 }; const BLACK = { r: 0, g: 0, b: 0, a: 1 }; const PAGE = { r: 28, g: 28, b: 28, a: 1 }; // the felt page behind everything const PUCK = { r: 44, g: 44, b: 44, a: 1 }; // a seat's own background describe("the ratio itself", () => { test("black on white is the full 21:1, either way round", () => { assert.ok(Math.abs(LayoutAudit.contrastRatio(BLACK, WHITE) - 21) < 0.01); assert.ok(Math.abs(LayoutAudit.contrastRatio(WHITE, BLACK) - 21) < 0.01); }); test("a colour against itself is 1:1", () => { assert.ok(Math.abs(LayoutAudit.contrastRatio(PUCK, PUCK) - 1) < 0.001); }); }); describe("text on a box that is being faded", () => { // opacity on a box fades the box and its text together onto whatever is behind // it — so a dimmed seat is not grey text on a dark puck, it is a faded puck and // faded text both landing on the page. const fade = alpha => ({ alpha, behind: PAGE }); test("at full strength nothing is composited", () => { const painted = LayoutAudit.asPainted(WHITE, PUCK, { alpha: 1, behind: PAGE }); assert.deepEqual(painted.fg, WHITE); assert.deepEqual(painted.bg, PUCK); }); test("fading moves both the text and its background, not just the text", () => { const painted = LayoutAudit.asPainted(WHITE, PUCK, fade(0.7)); assert.ok(painted.fg.r < WHITE.r, "the text dims"); assert.ok(painted.bg.r < PUCK.r, "and so does what it sits on"); assert.ok(painted.bg.r > PAGE.r, "but not all the way to the page"); }); test("which is what stops a fade being reported as a fault it isn't", () => { // Dimming light text on a dark puck over a dark page costs contrast, but far // less than measuring the faded text against the *unfaded* puck would claim. const honest = LayoutAudit.asPainted(WHITE, PUCK, fade(0.7)); const wrong = { fg: LayoutAudit.over({ ...WHITE, a: 0.7 }, PUCK), bg: PUCK }; assert.ok(LayoutAudit.contrastRatio(honest.fg, honest.bg) > LayoutAudit.contrastRatio(wrong.fg, wrong.bg)); }); test("a fade all the way out lands everything on the page behind", () => { const painted = LayoutAudit.asPainted(WHITE, PUCK, fade(0)); assert.deepEqual(painted.fg, PAGE); assert.deepEqual(painted.bg, PAGE); }); test("with nothing known to be behind it, the fade is left alone", () => { // Better to measure the unfaded pair than to invent a backdrop. const painted = LayoutAudit.asPainted(WHITE, PUCK, { alpha: 0.5, behind: null }); assert.deepEqual(painted.fg, WHITE); assert.deepEqual(painted.bg, PUCK); }); }); describe("a fade makes small grey text worse, which is the fault it went looking for", () => { // Deliberately not asserting a particular CSS colour here: the page's own greys are // measured in the browser, against the real gradients, where they live. What this // pins is the direction — that dimming a box costs its text contrast, which is why // filing faded text as "not checked" hid faults rather than avoiding false ones. test("the same grey scores lower once its box is dimmed", () => { const grey = LayoutAudit.parseColour("rgb(138, 138, 138)"); const solid = LayoutAudit.asPainted(grey, PUCK, { alpha: 1, behind: PAGE }); const dimmed = LayoutAudit.asPainted(grey, PUCK, { alpha: 0.7, behind: PAGE }); assert.ok(LayoutAudit.contrastRatio(dimmed.fg, dimmed.bg) < LayoutAudit.contrastRatio(solid.fg, solid.bg)); }); test("and a lighter grey scores higher at the same fade", () => { const at = colour => { const painted = LayoutAudit.asPainted(LayoutAudit.parseColour(colour), PUCK, { alpha: 0.7, behind: PAGE }); return LayoutAudit.contrastRatio(painted.fg, painted.bg); }; assert.ok(at("rgb(174, 174, 174)") > at("rgb(138, 138, 138)")); }); }); describe("reading a colour off the page", () => { test("rgb and rgba both come back as numbers", () => { assert.deepEqual(LayoutAudit.parseColour("rgb(1, 2, 3)"), { r: 1, g: 2, b: 3, a: 1 }); assert.deepEqual(LayoutAudit.parseColour("rgba(1, 2, 3, 0.5)"), { r: 1, g: 2, b: 3, a: 0.5 }); }); test("a gradient gives up every stop, because text could be over any of them", () => { const stops = LayoutAudit.gradientStops("linear-gradient(rgb(203, 170, 240), rgb(169, 127, 214))"); assert.equal(stops.length, 2); assert.deepEqual(stops[0], { r: 203, g: 170, b: 240, a: 1 }); assert.deepEqual(stops[1], { r: 169, g: 127, b: 214, a: 1 }); }); test("no background image, no stops", () => { assert.deepEqual(LayoutAudit.gradientStops("none"), []); }); });