import { test } from "node:test"; import assert from "node:assert/strict"; import { HEADER_SOCIAL_LINKS_MAX, headerSocialLinks, safeSocialSvg, scopeSvgIds, sizeSocialSvg, socialLinksForSave, } from "./socialLinks"; import { normalizeSocialSvg, parseSocialLinks, type SocialLink } from "./settingsSchema"; const SVG = ``; const link = (label: string, featured?: boolean): SocialLink => ({ label, url: `https://${label.toLowerCase()}.example`, svg: SVG, ...(featured ? { featured: true } : {}), }); const labels = (links: SocialLink[]) => links.map((l) => l.label); test("the header bound is four", () => { assert.equal(HEADER_SOCIAL_LINKS_MAX, 4); }); test("wide: four or fewer links → all of them, in order, marked or not", () => { assert.deepEqual(headerSocialLinks([], "wide"), []); assert.deepEqual(labels(headerSocialLinks([link("A")], "wide")), ["A"]); assert.deepEqual( labels(headerSocialLinks([link("A"), link("B", true), link("C"), link("D")], "wide")), ["A", "B", "C", "D"], ); }); test("wide: six links, none marked → the last four", () => { const six = ["A", "B", "C", "D", "E", "F"].map((l) => link(l)); assert.deepEqual(labels(headerSocialLinks(six, "wide")), ["C", "D", "E", "F"]); }); test("wide: six links, some marked → the marked kept first, the last of the rest after, in configured order", () => { const six = [link("A", true), link("B"), link("C"), link("D", true), link("E"), link("F")]; assert.deepEqual(labels(headerSocialLinks(six, "wide")), ["A", "D", "E", "F"]); const one = [link("A", true), link("B"), link("C"), link("D"), link("E"), link("F")]; assert.deepEqual(labels(headerSocialLinks(one, "wide")), ["A", "D", "E", "F"]); }); test("wide: more than four marked → the last four marked", () => { const six = ["A", "B", "C", "D", "E", "F"].map((l) => link(l, l !== "F")); assert.deepEqual(labels(headerSocialLinks(six, "wide")), ["B", "C", "D", "E"]); }); test("narrow: none marked → none; the name wins and the footer has them all", () => { assert.deepEqual(headerSocialLinks([], "narrow"), []); assert.deepEqual(headerSocialLinks([link("A"), link("B")], "narrow"), []); const six = ["A", "B", "C", "D", "E", "F"].map((l) => link(l)); assert.deepEqual(headerSocialLinks(six, "narrow"), []); }); test("narrow: the marked ones only, in order; more than four marked → the last four", () => { assert.deepEqual(labels(headerSocialLinks([link("A"), link("B"), link("C", true)], "narrow")), ["C"]); const six = [link("A"), link("B", true), link("C"), link("D", true), link("E"), link("F")]; assert.deepEqual(labels(headerSocialLinks(six, "narrow")), ["B", "D"]); const many = ["A", "B", "C", "D", "E", "F"].map((l) => link(l, l !== "F")); assert.deepEqual(labels(headerSocialLinks(many, "narrow")), ["B", "C", "D", "E"]); }); test("header: the input is not changed", () => { const six = ["A", "B", "C", "D", "E", "F"].map((l) => link(l, l === "B")); headerSocialLinks(six, "wide"); headerSocialLinks(six, "narrow"); assert.equal(six.length, 6); assert.deepEqual(labels(six), ["A", "B", "C", "D", "E", "F"]); }); test("parseSocialLinks: featured is kept only when exactly true", () => { const parsed = parseSocialLinks([ { label: "A", url: "https://a.example", svg: SVG, featured: true }, { label: "B", url: "https://b.example", svg: SVG, featured: false }, { label: "C", url: "https://c.example", svg: SVG, featured: "yes" }, { label: "D", url: "https://d.example", svg: SVG }, ]); assert.deepEqual(parsed[0], { label: "A", url: "https://a.example", svg: SVG, featured: true }); for (const l of parsed.slice(1)) assert.ok(!("featured" in l), l.label); // An old file — no link marked — parses exactly as it did before the key. const old = [{ label: "A", url: "https://a.example", svg: SVG }]; assert.deepEqual(parseSocialLinks(old), old); assert.deepEqual(JSON.parse(JSON.stringify(parseSocialLinks(old))), old); }); test("sizeSocialSvg: an intrinsic size on an unsized root, never a second one", () => { assert.ok(sizeSocialSvg(SVG).startsWith('`; assert.equal(sizeSocialSvg(sized), sized); assert.ok(sizeSocialSvg(SVG, 36).startsWith('` + `` + `` + `` + `` + ``; test("scopeSvgIds: every id and every reference to one gains the scope", () => { const out = scopeSvgIds(REFS, "s1-0"); for (const id of ["root", "g", "g2", "m"]) assert.ok(out.includes(`id="s1-0-${id}"`), id); assert.ok(out.includes(`id='s1-0-c'`)); assert.ok(out.includes(`fill="url(#s1-0-g)"`)); assert.ok(out.includes(`style="fill:url('#s1-0-g2')"`)); assert.ok(out.includes(`clip-path="url(#s1-0-c)"`)); assert.ok(out.includes(`mask="url(#s1-0-m)"`)); assert.ok(out.includes(`xlink:href="#s1-0-g"`)); assert.ok(out.includes(``)); // Not an id of this icon: left alone. So is a colour that looks like one. assert.ok(out.includes(``)); assert.ok(out.includes(`fill="url(#gx)"`)); assert.ok(out.includes(`stop-color:#D7DF23`)); }); test("scopeSvgIds: two scopes of one icon share no id; no ids → unchanged", () => { const ids = (svg: string) => [...svg.matchAll(/\sid=["']([^"']+)["']/g)].map((m) => m[1]); const a = ids(scopeSvgIds(REFS, "a")); const b = ids(scopeSvgIds(REFS, "b")); assert.equal(a.length, 5); assert.ok(a.every((id) => !b.includes(id))); assert.equal(scopeSvgIds(SVG, "a"), SVG); }); test("scopeSvgIds: entity-quoted and case-varied references are scoped with their id", () => { const svg = `` + ``; const out = scopeSvgIds(svg, "s"); assert.ok(out.includes(`ID="s-g"`)); assert.ok(out.includes(`mask="url("#s-g")"`)); assert.ok(out.includes(`fill="url('#s-g')"`)); assert.ok(out.includes(`HREF="#s-g"`)); }); test("scopeSvgIds: an id that is not a plain name is left alone, so no comment can close early", () => { for (const id of ["->", "-!>"]) { const svg = ``; assert.equal(scopeSvgIds(svg, "s"), svg, id); } }); // Every icon the checker accepts, scoped as a page renders it, still passes the // checker: the render never makes markup the save would have refused. test("a scoped and sized icon still passes the checker", () => { for (const raw of [ ``, ``, ]) { const stored = normalizeSocialSvg(raw); assert.ok(stored, raw.slice(0, 60)); const rendered = sizeSocialSvg(scopeSvgIds(stored, "sl_S_1_-0")); assert.equal(normalizeSocialSvg(rendered)?.includes("sl_S_1_-0-"), true); } }); test("safeSocialSvg: a stored icon that fails the check is not rendered", () => { assert.equal(safeSocialSvg(SVG), SVG); for (const bad of [ ``, ``, ``, "not an svg", 42, undefined, ]) { assert.equal(safeSocialSvg(bad), null, String(bad).slice(0, 40)); } }); test("socialLinksForSave: an unchanged stored icon is kept as it is; a new or edited one is checked", () => { const refusedByNow = ``; const stored: SocialLink[] = [{ label: "Old", url: "https://old.example", svg: refusedByNow }]; // Unchanged (a label or `featured` may change): kept byte-identical. const same = socialLinksForSave([{ ...stored[0], label: "Renamed", featured: true }], stored); assert.deepEqual(same, { links: [{ ...stored[0], label: "Renamed", featured: true }] }); // Edited to something refused: the label and the reason, never the markup. const bad = socialLinksForSave([{ ...stored[0], svg: `` }], stored); assert.ok("refused" in bad && bad.refused.label === "Old" && /event handler/.test(bad.refused.problem)); // New and good: normalized. const fresh = socialLinksForSave([{ label: "New", url: "https://new.example", svg: `` }], stored); assert.ok("links" in fresh && fresh.links[0].svg.startsWith('