import assert from "node:assert/strict"; import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { collectFiles, extractUtilityCss, ICON_CLASS_PATTERN, parseIconCodepoints, REQUIRED_ICONS, resolveUsedIcons, scanUsedIcons, } from "../scripts/subset-mdi-font.mjs"; // ── Helper: create a temporary directory tree for file-system tests ───────── function makeTmpDir() { const base = join(tmpdir(), `mdi-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); mkdirSync(base, { recursive: true }); return base; } // ── collectFiles ──────────────────────────────────────────────────────────── test("collectFiles yields files matching given extensions", () => { const tmp = makeTmpDir(); writeFileSync(join(tmp, "a.vue"), ""); writeFileSync(join(tmp, "b.ts"), ""); writeFileSync(join(tmp, "c.txt"), ""); const files = [...collectFiles(tmp, [".vue", ".ts"])]; assert.equal(files.length, 2); assert.ok(files.some((f) => f.endsWith("a.vue"))); assert.ok(files.some((f) => f.endsWith("b.ts"))); rmSync(tmp, { recursive: true }); }); test("collectFiles recurses into subdirectories", () => { const tmp = makeTmpDir(); const sub = join(tmp, "sub"); mkdirSync(sub); writeFileSync(join(sub, "deep.vue"), ""); const files = [...collectFiles(tmp, [".vue"])]; assert.equal(files.length, 1); assert.ok(files[0].endsWith("deep.vue")); rmSync(tmp, { recursive: true }); }); test("collectFiles skips node_modules directories", () => { const tmp = makeTmpDir(); const nm = join(tmp, "node_modules"); mkdirSync(nm); writeFileSync(join(nm, "pkg.vue"), ""); writeFileSync(join(tmp, "app.vue"), ""); const files = [...collectFiles(tmp, [".vue"])]; assert.equal(files.length, 1); assert.ok(files[0].endsWith("app.vue")); rmSync(tmp, { recursive: true }); }); test("collectFiles yields nothing for empty directory", () => { const tmp = makeTmpDir(); const files = [...collectFiles(tmp, [".vue"])]; assert.equal(files.length, 0); rmSync(tmp, { recursive: true }); }); // ── scanUsedIcons ─────────────────────────────────────────────────────────── test("scanUsedIcons extracts mdi-* icon names from files", () => { const tmp = makeTmpDir(); writeFileSync(join(tmp, "A.vue"), "mdi-homemdi-close"); writeFileSync(join(tmp, "B.vue"), 'icon="mdi-home"'); const icons = scanUsedIcons(collectFiles(tmp, [".vue"])); assert.ok(icons instanceof Set); assert.ok(icons.has("mdi-home")); assert.ok(icons.has("mdi-close")); for (const requiredIcon of REQUIRED_ICONS) { assert.ok(icons.has(requiredIcon)); } const expectedIcons = new Set([...REQUIRED_ICONS, "mdi-home", "mdi-close"]); assert.deepEqual(icons, expectedIcons); rmSync(tmp, { recursive: true }); }); test("scanUsedIcons excludes utility classes", () => { const tmp = makeTmpDir(); writeFileSync(join(tmp, "A.vue"), "mdi-spin mdi-rotate-90 mdi-flip-h mdi-home"); const icons = scanUsedIcons(collectFiles(tmp, [".vue"])); assert.ok(icons.has("mdi-home")); assert.ok(!icons.has("mdi-spin")); assert.ok(!icons.has("mdi-rotate-90")); assert.ok(!icons.has("mdi-flip-h")); rmSync(tmp, { recursive: true }); }); test("scanUsedIcons includes all required icons even when no mdi-* icons are found in source", () => { const tmp = makeTmpDir(); writeFileSync(join(tmp, "A.vue"), "
Hello
"); const icons = scanUsedIcons(collectFiles(tmp, [".vue"])); for (const requiredIcon of REQUIRED_ICONS) { assert.ok(icons.has(requiredIcon)); } assert.equal(icons.size, REQUIRED_ICONS.size); rmSync(tmp, { recursive: true }); }); test("scanUsedIcons deduplicates required icons when source already references them", () => { const tmp = makeTmpDir(); const requiredIcon = [...REQUIRED_ICONS][0]; writeFileSync(join(tmp, "A.vue"), `${requiredIcon}mdi-home`); const icons = [...scanUsedIcons(collectFiles(tmp, [".vue"]))]; assert.equal(icons.filter((icon) => icon === requiredIcon).length, 1); for (const builtInRequiredIcon of REQUIRED_ICONS) { assert.ok(icons.includes(builtInRequiredIcon)); } assert.ok(icons.includes("mdi-home")); rmSync(tmp, { recursive: true }); }); // ── parseIconCodepoints ───────────────────────────────────────────────────── test("parseIconCodepoints parses icon definitions from CSS", () => { const css = ` .mdi-home::before { content: "\\F02DC"; } .mdi-close::before { content: "\\F0156"; } `; const map = parseIconCodepoints(css); assert.equal(map.size, 2); assert.equal(map.get("mdi-home"), "F02DC"); assert.equal(map.get("mdi-close"), "F0156"); }); test("parseIconCodepoints handles CSS with semicolons inside braces", () => { const css = `.mdi-check::before { content: "\\F012C"; }`; const map = parseIconCodepoints(css); assert.equal(map.get("mdi-check"), "F012C"); }); test("parseIconCodepoints returns empty map for non-matching CSS", () => { const css = `.some-other-class { color: red; }`; const map = parseIconCodepoints(css); assert.equal(map.size, 0); }); // ── resolveUsedIcons ──────────────────────────────────────────────────────── test("resolveUsedIcons separates resolved and missing icons", () => { const usedIcons = new Set(["mdi-home", "mdi-close", "mdi-nonexistent"]); const iconMap = new Map([ ["mdi-home", "F02DC"], ["mdi-close", "F0156"], ]); const { resolvedIcons, missingIcons, subsetChars } = resolveUsedIcons(usedIcons, iconMap); assert.ok(resolvedIcons.includes("mdi-home")); assert.ok(resolvedIcons.includes("mdi-close")); assert.equal(resolvedIcons.length, 2); assert.deepEqual(missingIcons, ["mdi-nonexistent"]); // Verify subsetChars contains correct Unicode characters assert.equal(subsetChars.length, 2); assert.equal(subsetChars[0], String.fromCodePoint(0xf02dc)); assert.equal(subsetChars[1], String.fromCodePoint(0xf0156)); }); test("resolveUsedIcons returns all missing when iconMap is empty", () => { const usedIcons = new Set(["mdi-home"]); const iconMap = new Map(); const { resolvedIcons, missingIcons, subsetChars } = resolveUsedIcons(usedIcons, iconMap); assert.equal(resolvedIcons.length, 0); assert.deepEqual(missingIcons, ["mdi-home"]); assert.equal(subsetChars.length, 0); }); // ── extractUtilityCss ─────────────────────────────────────────────────────── test("extractUtilityCss removes icon definitions and keeps utility rules", () => { const css = ` @font-face { font-family: "Material Design Icons"; src: url("../fonts/materialdesignicons-webfont.woff2") format("woff2"); } .mdi:before, .mdi-set { display: inline-block; font: normal normal normal 24px/1 "Material Design Icons"; } .mdi-home::before { content: "\\F02DC"; } .mdi-close::before { content: "\\F0156"; } .mdi-spin:before { animation: mdi-spin 2s infinite linear; } .mdi-18px.mdi-set, .mdi-18px.mdi:before { font-size: 18px; } /*# sourceMappingURL=materialdesignicons.css.map */ `; const result = extractUtilityCss(css, ICON_CLASS_PATTERN); // Should NOT contain icon definitions assert.ok(!result.includes("mdi-home")); assert.ok(!result.includes("mdi-close")); // Should NOT contain @font-face assert.ok(!result.includes("@font-face")); // Should NOT contain base .mdi rules assert.ok(!result.includes("display: inline-block")); // Should NOT contain source map assert.ok(!result.includes("sourceMappingURL")); // SHOULD contain utility classes assert.ok(result.includes("mdi-spin")); assert.ok(result.includes("mdi-18px")); }); test("extractUtilityCss returns empty string when only icon defs exist", () => { const css = ` @font-face { font-family: "MDI"; src: url("font.woff2"); } .mdi:before, .mdi-set { display: inline-block; } .mdi-home::before { content: "\\F02DC"; } `; const result = extractUtilityCss(css, ICON_CLASS_PATTERN); assert.equal(result, ""); }); test("extractUtilityCss handles empty CSS input", () => { const result = extractUtilityCss("", ICON_CLASS_PATTERN); assert.equal(result, ""); }); // ── ICON_CLASS_PATTERN ────────────────────────────────────────────────────── test("ICON_CLASS_PATTERN matches standard MDI icon definitions", () => { const css = `.mdi-home::before { content: "\\F02DC"; }`; const matches = [...css.matchAll(ICON_CLASS_PATTERN)]; assert.equal(matches.length, 1); assert.equal(matches[0][1], "mdi-home"); assert.equal(matches[0][2], "F02DC"); }); test("ICON_CLASS_PATTERN does not match non-icon classes", () => { const css = `.some-class::before { content: "hello"; }`; const matches = [...css.matchAll(ICON_CLASS_PATTERN)]; assert.equal(matches.length, 0); });