mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 01:40:15 +08:00
267 lines
9.1 KiB
JavaScript
267 lines
9.1 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,
|
|
} 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'));
|
|
assert.ok(icons.has('mdi-radiobox-blank'));
|
|
assert.ok(icons.has('mdi-radiobox-marked'));
|
|
assert.equal(icons.size, 4); // source icons + required radio icons
|
|
|
|
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 required radio 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']));
|
|
assert.ok(icons.has('mdi-radiobox-blank'));
|
|
assert.ok(icons.has('mdi-radiobox-marked'));
|
|
assert.equal(icons.size, 2);
|
|
|
|
rmSync(tmp, { recursive: true });
|
|
});
|
|
|
|
test('scanUsedIcons deduplicates required radio icons when source already references them', () => {
|
|
const tmp = makeTmpDir();
|
|
writeFileSync(join(tmp, 'A.vue'), '<v-icon>mdi-radiobox-marked</v-icon><v-icon>mdi-home</v-icon>');
|
|
|
|
const icons = [...scanUsedIcons(collectFiles(tmp, ['.vue']))];
|
|
assert.equal(icons.filter(icon => icon === 'mdi-radiobox-marked').length, 1);
|
|
assert.ok(icons.includes('mdi-radiobox-blank'));
|
|
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);
|
|
});
|