/** * What the editor's sliders say, as opposed to what they show. * * Every one of them drives a picture through a number that is not the number * on screen: Size is a 0..1 fraction shown as a percentage, Angle is a * fraction of a turn shown in degrees, Zoom is a multiple, and the two framing * controls run -1..1 and are shown signed. `format` is what closes that gap * for a reader looking at the page, and `aria-valuetext` is the only thing * that closes it for one who is not — without it a screen reader lands on * Angle and says "0.25" where the page says 90°. * * Structural, because there is nothing to catch at runtime. A slider with no * aria-valuetext still works, still renders, still drags; it is only wrong out * loud. * * Run with `npm test`. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; const web = fileURLToPath(new URL("../", import.meta.url)); const source = await readFile(join(web, "src/camo/editor.ts"), "utf8"); /** The body of the slider builder, which is where all of this has to be. */ const builder = (() => { const start = source.indexOf(" function slider("); assert.notEqual(start, -1, "the slider builder moved or was renamed"); const end = source.indexOf("\n }", start); assert.notEqual(end, -1, "the slider builder has no end"); return source.slice(start, end); })(); test("a slider says its formatted value, not its raw one", () => { assert.match( builder, /aria-valuetext/, "the slider announces the number it is implemented with rather than the " + "one it is showing", ); }); test("the readout and the announcement are the same string", () => { // Two calls to format() that could drift apart is the whole failure mode // this replaced: the readout said one thing and the control said another. const formats = [...builder.matchAll(/format\(/g)]; assert.equal( formats.length, 1, `format() is called ${formats.length} times in the slider builder; it has ` + "to be called once and the result used for both the readout and " + "aria-valuetext, or the two can disagree", ); }); test("the value is set once at build and again on every input", () => { // A slider built with the right announcement and never updated is worse than // one with none: it is confidently wrong for the whole of a drag. The shape // that holds is one setter called from both places. const setter = /const show = \(v: number\): void =>/.test(builder); assert.ok(setter, "the slider no longer has one setter for its value"); const calls = [...builder.matchAll(/\bshow\(/g)]; assert.ok( calls.length >= 2, "the setter is called once, so either the initial value or the drag is " + "not covered", ); }); test("the readout does not announce itself on top of the slider", () => { // is a live region. Left in the tree it announced every step of a // drag, over the value the slider announces anyway. assert.match( builder, /readout\.setAttribute\("aria-hidden", "true"\)/, "the readout is a live region again, so a drag is announced twice", ); });