// The settings index (client/components/settings/settingsIndex.ts) and the // search over it. // // The index is GENERATED from the panel's source and gated by // `npm run check-settings`, so what is worth testing here is not that it // matches — the gate proves that — but that a reader searching for a thing // they can see actually finds it, in both languages. // Both dictionaries, installed: under Node there is no chunk to fetch // (client/i18n.ts loads one language at a time in a page). import "../client/i18n/both.ts"; import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { SETTINGS_INDEX } from "../client/components/settings/settingsIndex.ts"; import { DESKTOP_ONLY_ROWS, searchSettings } from "../client/components/settings/searchSettings.ts"; import { setLang, t } from "../client/i18n.ts"; import { TABS } from "../client/components/settings/tabs.ts"; describe("the settings index", () => { it("covers every page the panel has but About, which holds facts and no rows", () => { const tabs = new Set(SETTINGS_INDEX.map((e) => e.tab)); for (const id of TABS.map((s) => s.id).filter((id) => id !== "about")) { assert.ok(tabs.has(id), `no rows indexed for the ${id} page`); } }); it("names no key twice in the whole index — a row lives in exactly one section", () => { const seen = new Set(); for (const e of SETTINGS_INDEX) { assert.ok(!seen.has(e.label), `${e.label} is indexed twice`); seen.add(e.label); } }); it("names no key twice within a tab", () => { const seen = new Set(); for (const e of SETTINGS_INDEX) { const key = `${e.tab}/${e.label}`; assert.ok(!seen.has(key), `duplicate row ${key}`); seen.add(key); } }); it("keeps every page short — ten rows in sight at most, and none a lone row", () => { // The 3.15 re-cut exists because Publishing ran to twenty-one rows while // Identity and Typography held five each. A section nobody scrolls to the // end of is a section whose rows may as well not exist; this pins the shape. // // Counted as ONE DEVICE sees it: parts (a control folded into its host, // `row` set) are not rows; the desktop app's own rows are drawn on the // desktop only; and a pocket's Backup & sync rows never share a screen // with an instance's — so a section carries the larger of each pair. const DESKTOP_APP = new Set(["rowAppName", "rowAppIcon", "rowAppLauncher", "rowAppLauncherWin", "rowUpdates"]); const tabs = new Set(SETTINGS_INDEX.map((e) => e.tab)); for (const tab of tabs) { const rows = SETTINGS_INDEX.filter((e) => e.tab === tab && e.row === undefined); const view = (pocket: boolean, desktopApp: boolean) => rows.filter((e) => (e.mode === undefined || e.mode === (pocket ? "pocket" : "instance")) && (desktopApp || !DESKTOP_APP.has(e.label))).length; const n = Math.max(view(false, true), view(true, true)); const inSight = rows.filter((e) => e.adv !== true && (e.mode === undefined || e.mode === "instance") && !DESKTOP_APP.has(e.label)).length; assert.ok(inSight <= 10, `${tab} shows ${inSight} rows before its Advanced line`); assert.ok(n <= 18, `${tab} carries ${n} rows`); // About is facts first; its rows are the app's own (one in a browser). assert.ok(view(false, false) >= 2, `${tab} carries only ${view(false, false)} rows`); } }); it("points every part at a row of its own section, and keeps parts out of Advanced unless their host is in it", () => { for (const e of SETTINGS_INDEX) { if (e.row === undefined) continue; const host = SETTINGS_INDEX.find((h) => h.label === e.row && h.row === undefined); assert.ok(host, `${e.label} names ${e.row}, which is not a row`); assert.equal(host.tab, e.tab, `${e.label} is in ${e.tab} but its host is in ${host.tab}`); assert.equal(e.adv === true, host.adv === true, `${e.label} and its host disagree about Advanced`); } }); it("finds a row by the words behind its ⓘ, and lands a part on itself", () => { setLang("en"); const labels = (q: string): string[] => searchSettings(q, true).map((h) => h.entry.label); // "Mastodon" is in the fediverse row's hint; "bookmarklet" only in the // clipper's ⓘ paragraph (moreClipper) — reference text a reader remembers. assert.ok(labels("Mastodon").includes("rowFediverse")); // Every row with an ⓘ paragraph is found by that paragraph's longest word. const longest = (text: string): string => text.split(/[^\p{L}\p{N}]+/u).reduce((a, w) => (w.length > a.length ? w : a), ""); const ref = SETTINGS_INDEX.find((e) => e.more?.length && !labels(longest(t(e.more[0]))).includes(e.label)); assert.equal(ref, undefined, `a row's ⓘ text does not find it: ${ref?.label}`); const part = searchSettings("Fediverse name", true)[0]; assert.equal(part.entry.label, "rowFediverseHandle"); assert.equal(part.entry.row, "rowFediverse"); }); it("puts the desktop's update switch where a search for it lands — on the desktop", () => { const row = SETTINGS_INDEX.find((e) => e.label === "rowUpdates"); assert.ok(row, "the Software updates row is not indexed"); assert.equal(row.tab, "device"); setLang("en"); assert.ok(searchSettings("software updates", true).some((h) => h.entry.label === "rowUpdates")); assert.ok(searchSettings("installed", true).some((h) => h.entry.label === "rowUpdates"), "the hint's promise is not searchable"); setLang("ar"); assert.ok(searchSettings("تحديثات", true).some((h) => h.entry.label === "rowUpdates")); setLang("en"); }); it("keeps desktop-only rows out of a browser's results, and names only real rows", () => { setLang("en"); assert.ok(!searchSettings("software updates", false).some((h) => h.entry.label === "rowUpdates"), "a browser search landed on a row it cannot draw"); assert.ok(!searchSettings("this app's name", false).some((h) => h.entry.label === "rowAppName")); for (const label of DESKTOP_ONLY_ROWS) { assert.ok(SETTINGS_INDEX.some((e) => e.label === label), `${label} is not an index entry`); } }); it("carries the environment variables an operator would search for", () => { const envs = new Set(SETTINGS_INDEX.map((e) => e.env).filter(Boolean)); for (const name of ["SITE_NAME", "SITE_LANG", "PUBLIC_LAYOUT"]) { assert.ok(envs.has(name), `${name} is not reachable from the settings search`); } }); }); // The probes below search AS THE DESKTOP (`true`): they pick their row out // of the whole index, and the first rows in it are the desktop-only ones a // browser's search deliberately leaves out. describe("searching the settings", () => { it("finds a row by its own label", () => { setLang("en"); const label = t(SETTINGS_INDEX[0].label); const hits = searchSettings(label, true); assert.ok(hits.some((h) => h.entry.label === SETTINGS_INDEX[0].label), `"${label}" found nothing`); }); it("finds a row by its ENVIRONMENT VARIABLE — the operator's half", () => { setLang("en"); const hits = searchSettings("SITE_LANG"); assert.ok(hits.length > 0, "SITE_LANG found nothing"); assert.equal(hits[0].entry.env, "SITE_LANG"); }); it("is case-insensitive, so a variable typed in lower case still lands", () => { setLang("en"); assert.ok(searchSettings("site_lang").length > 0); }); it("returns nothing for an empty query rather than everything", () => { assert.deepEqual(searchSettings(""), []); assert.deepEqual(searchSettings(" "), []); }); it("ranks a LABEL match above a help-text match", () => { setLang("en"); // A word common enough to appear in both places; the row that is NAMED for // it must come first, because that is the one the reader meant. const hits = searchSettings("theme"); assert.ok(hits.length > 1, "expected several theme rows"); assert.ok(/theme/i.test(t(hits[0].entry.label)), `first hit was "${t(hits[0].entry.label)}"`); }); it("searches ARABIC labels on an Arabic instance, from the same index", () => { setLang("ar"); const entry = SETTINGS_INDEX.find((e) => /[؀-ۿ]/.test(t(e.label))); assert.ok(entry, "no Arabic label found — the dictionary is not translated"); const hits = searchSettings(t(entry.label), true); assert.ok(hits.some((h) => h.entry.label === entry.label), "an Arabic label found nothing"); setLang("en"); }); it("matches at a WORD START, so 'graph' no longer answers with para-graph", () => { setLang("en"); const labels = (q: string) => searchSettings(q, true).map((h) => t(h.entry.label)); assert.ok(!labels("graph").includes(t("rowTextDirection")), "'graph' matched inside 'paragraph'"); assert.ok(!labels("date").includes(t("rowWhatsNew")), "'date' matched inside 'update'"); // …while a word that begins a label or a hint still lands. assert.ok(labels("date").includes(t("rowDateCalendar"))); }); it("finds the rows a reader searches for by the words they use", () => { setLang("en"); const labels = (q: string): string[] => searchSettings(q, true).map((h) => h.entry.label); for (const q of ["font", "fonts"]) { for (const row of ["rowFontProse", "rowFontMono", "rowFontArabic"]) { assert.ok(labels(q).includes(row), `"${q}" did not find ${row}`); } } assert.ok(labels("typeface").includes("rowFontUi"), "'typeface' found nothing"); assert.ok(labels("width").includes("rowEditorWidth"), "'width' found nothing"); assert.ok(labels("dark mode").includes("rowYourTheme"), "'dark mode' found nothing"); assert.ok(labels("history").includes("rowNoteVersions"), "'history' found nothing"); assert.ok(labels("rtl").includes("rowTextDirection"), "'rtl' found nothing"); assert.ok(labels("designer").includes("rowOpenDesigner"), "'designer' found nothing"); assert.ok(labels("SITE_LANG").includes("rowLanguage"), "an env name lost its word-start match on the underscore"); }); it("folds the alef family — and the fold provably DOES something", () => { setLang("ar"); // The first version of this test compared the two spellings and passed // while both returned [] — two empty lists agreeing that the fold was // broken. So first find a label that really contains a hamza-alef, then // assert the BARE spelling finds it: non-empty, or the test says nothing. const hamza = SETTINGS_INDEX.find((e) => /[\u0622\u0623\u0625]/.test(t(e.label))); assert.ok(hamza, "no hamza-alef label in the index — pick another probe"); const bare = t(hamza.label).replace(/[\u0622\u0623\u0625]/g, "\u0627"); const hits = searchSettings(bare, true); assert.ok( hits.some((h) => h.entry.label === hamza.label), `bare-alef "${bare}" did not find the hamza-spelled label "${t(hamza.label)}"`, ); setLang("en"); }); });