// The settings' design, held to its catalogue (contracts/settings-design.md). // // Every row in the app is exactly one of ten control kinds, drawn by that // kind's one component at that kind's one width. The index carries each // row's kind (read from `kind="…"` on the Row in the source); this test holds // the index to the catalogue, and the SOURCE to the index — a row that says // `kind="chips"` and draws four toggles is the 3.38.0 dictionaries row, and // fails here by name. It also holds the pages to the design: their groups, // their length, and the "This device, said once" rule. import "../client/i18n/both.ts"; import assert from "node:assert/strict"; import { readdirSync, readFileSync } from "node:fs"; import { describe, it } from "node:test"; import { CONTROL_KINDS, SETTABLE_KINDS, type ControlKind } from "../client/components/settings/catalogue.ts"; import { SETTINGS_INDEX } from "../client/components/settings/settingsIndex.ts"; import { GROUPS, TABS, devicePage } from "../client/components/settings/tabs.ts"; import { setLang, t } from "../client/i18n.ts"; const DIR = new URL("../client/components/settings/", import.meta.url); const files = readdirSync(DIR).filter((f) => f.endsWith(".tsx")); /** What each kind's component looks like in the source. */ const DRAWN_BY: Record = { toggle: /> = { chips: /… and … in the settings sources, with * its kind and label key (a ternary label counts by its first key). */ function blocks(): Block[] { const out: Block[] = []; for (const file of files) { const src = readFileSync(new URL(file, DIR), "utf8"); for (const m of src.matchAll(/<(Row|Part)\b([^>]*?)>([\s\S]*?)<\/\1>/g)) { if (file === "Row.tsx") continue; const head = m[2]; out.push({ file, tag: m[1] as "Row" | "Part", kind: /\bkind="(\w+)"/.exec(head)?.[1] ?? "", label: /label=\{t\((?:[^"]*\? )?"(\w+)"/.exec(head)?.[1] ?? "", body: m[3], }); } } return out; } describe("the control catalogue", () => { it("has ten kinds, seven of them set by a reader", () => { assert.equal(CONTROL_KINDS.length, 10); assert.deepEqual([...SETTABLE_KINDS].sort(), ["chips", "path", "segmented", "select", "slider", "text", "toggle"]); }); it("names a kind for every row and part in the index", () => { for (const e of SETTINGS_INDEX) assert.ok((CONTROL_KINDS as readonly string[]).includes(e.kind), `${e.label} is kind "${e.kind}"`); }); it("draws every row with its kind's component, and never with the one its kind forbids", () => { const found = blocks(); assert.ok(found.length >= 100, `only ${found.length} Row/Part blocks read — the reader is broken`); for (const b of found) { assert.ok((CONTROL_KINDS as readonly string[]).includes(b.kind), `${b.file}: <${b.tag}> ${b.label} has no kind`); const kind = b.kind as ControlKind; assert.match(b.body, DRAWN_BY[kind], `${b.file}: ${b.label} says kind="${kind}" and draws no ${kind}`); const never = NEVER[kind]; if (never) assert.doesNotMatch(b.body, never, `${b.file}: ${b.label} (${kind}) draws a control the catalogue forbids for it`); } }); it("gives the dictionaries ONE chips control, not a switch per language", () => { const row = blocks().find((b) => b.label === "rowSpellDicts"); assert.ok(row); assert.equal(row.kind, "chips"); assert.doesNotMatch(row.body, / e.label === "rowSpellDicts")?.kind, "chips"); }); it("offers no segmented control more than four segments", () => { for (const b of blocks().filter((x) => x.kind === "segmented")) { const own = b.tag === "Row" ? b.body.replace(//g, "") : b.body; const segs = [...own.matchAll(/\{ value: /g)].length; assert.ok(segs <= 4, `${b.file}: ${b.label} has ${segs} segments — five or more is a Select`); } }); }); describe("the pages", () => { it("sit in four groups, every group holding at least two pages", () => { for (const g of GROUPS) assert.ok(TABS.filter((s) => s.group === g.id).length >= 2, `${g.id} holds fewer than two pages`); for (const s of TABS) assert.ok(GROUPS.some((g) => g.id === s.group), `${s.id} is in no group`); }); it("carry each row's group in the index", () => { for (const e of SETTINGS_INDEX) assert.equal(e.group, TABS.find((s) => s.id === e.tab)?.group, `${e.label}`); }); it("show at most ten rows before their Advanced line", () => { for (const s of TABS) { const inSight = SETTINGS_INDEX.filter((e) => e.tab === s.id && e.row === undefined && e.adv !== true && e.mode !== "pocket"); assert.ok(inSight.length <= 10, `${s.id} shows ${inSight.length} rows`); } }); it("say 'This device' once on a page that is all this device's, and mark only the exceptions elsewhere", () => { for (const s of TABS) { const rows = SETTINGS_INDEX.filter((e) => e.tab === s.id && e.row === undefined); if (devicePage(s.id)) assert.ok(rows.every((e) => e.device === true), `${s.id} says all its rows are this device's`); else if (rows.length > 0) assert.ok(rows.some((e) => e.device !== true), `${s.id} is all this device's and does not say so once`); } // The 3.38.0 screenshots had a mark on nearly every row of Appearance. const marked = SETTINGS_INDEX.filter((e) => e.device === true && e.row === undefined && !devicePage(e.tab)); assert.deepEqual( marked.map((e) => e.label).sort(), ["rowEditorLanguage", "rowFrenchAutocorrect", "rowHeadingNumbers", "rowPrefsSync", "rowSpellDicts", "selToolbarLabel"], ); }); it("name every page and group in both languages", () => { for (const lang of ["en", "ar"] as const) { setLang(lang); for (const s of TABS) assert.ok(t(s.key).trim() !== "" && t(s.intro).trim() !== "", `${lang}: ${s.id}`); for (const g of GROUPS) assert.ok(t(g.key).trim() !== "", `${lang}: ${g.id}`); } setLang("en"); }); });