Files
AstrBot/dashboard/tests/subsetMdiFont.test.mjs
LIghtJUNction bc01532e59 fix(provider): filter abort_signal from payloads to avoid JSON serialize error
`abort_signal` (asyncio.Event) is passed via **kwargs into payloads during
tool_call streaming, causing "Object of type Event is not JSON serializable"
when the OpenAI client tries to serialize the request body.

Regression test added: test_prepare_chat_payload_strips_non_json_serializable_kwargs
2026-03-28 01:15:21 +08:00

292 lines
9.4 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import { mkdirSync, writeFileSync, rmSync } from "fs";
import { join } from "path";
import { tmpdir } from "os";
import {
collectFiles,
scanUsedIcons,
parseIconCodepoints,
resolveUsedIcons,
extractUtilityCss,
ICON_CLASS_PATTERN,
REQUIRED_ICONS,
} 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"),
"<v-icon>mdi-home</v-icon><v-icon>mdi-close</v-icon>",
);
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"), "<div>Hello</div>");
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"),
`<v-icon>${requiredIcon}</v-icon><v-icon>mdi-home</v-icon>`,
);
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);
});