83 lines
4.0 KiB
JavaScript
83 lines
4.0 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import test from 'node:test';
|
|
import { parse, compileTemplate } from 'vue/compiler-sfc';
|
|
import ts from 'typescript';
|
|
|
|
const root = fileURLToPath(new URL('../src/', import.meta.url));
|
|
const source = readFileSync(join(root, 'utils/icon-mask.ts'), 'utf8');
|
|
const javascript = ts.transpileModule(source, {
|
|
compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.ES2020 },
|
|
}).outputText;
|
|
const { ICON_PATHS, createIconMask } = await import(`data:text/javascript;base64,${Buffer.from(javascript).toString('base64')}`);
|
|
const component = parse(readFileSync(join(root, 'components/AppIcon.vue'), 'utf8')).descriptor;
|
|
|
|
function nodes(node) {
|
|
return [node, ...(node.children ?? []).flatMap(nodes)];
|
|
}
|
|
function vueFiles(dir) {
|
|
return readdirSync(dir, { withFileTypes: true }).flatMap((item) => {
|
|
const file = join(dir, item.name);
|
|
return item.isDirectory() ? vueFiles(file) : file.endsWith('.vue') ? [file] : [];
|
|
});
|
|
}
|
|
function decodeMask(mask) {
|
|
assert.ok(mask.startsWith('url("data:image/svg+xml;charset=utf-8,'));
|
|
assert.ok(mask.endsWith('")'));
|
|
return decodeURIComponent(mask.slice(mask.indexOf(',') + 1, -2));
|
|
}
|
|
|
|
test('AppIcon renders a uni-app view, never unsupported raw svg/path elements', () => {
|
|
const elements = nodes(component.template.ast).filter((node) => node.type === 1);
|
|
assert.deepEqual(elements.map((node) => node.tag), ['view']);
|
|
const result = compileTemplate({ source: component.template.content, filename: 'AppIcon.vue', id: 'app-icon' });
|
|
assert.deepEqual(result.errors, []);
|
|
assert.doesNotMatch(result.code, /resolveComponent\(["'](?:svg|path)["']\)/);
|
|
assert.match(component.scriptSetup.content, /'-webkit-mask-image': mask/);
|
|
assert.match(component.scriptSetup.content, /'mask-image': mask/);
|
|
assert.match(component.styles[0].content, /background-color:\s*currentColor/);
|
|
assert.match(component.styles[0].content, /-webkit-mask-size:\s*100% 100%/);
|
|
});
|
|
|
|
test('every icon is self-contained with explicit SVG image styling', () => {
|
|
assert.ok(Object.keys(ICON_PATHS).length >= 35);
|
|
for (const [name, paths] of Object.entries(ICON_PATHS)) {
|
|
const svg = decodeMask(createIconMask(name));
|
|
assert.match(svg, /xmlns="http:\/\/www.w3.org\/2000\/svg"/);
|
|
assert.match(svg, /viewBox="0 0 24 24"/);
|
|
assert.match(svg, /fill="none" stroke="#000" stroke-width="1.8"/);
|
|
assert.equal((svg.match(/<path /g) || []).length, paths.length, name);
|
|
for (const path of paths) assert.ok(svg.includes(`d="${path}"`));
|
|
assert.doesNotMatch(svg, /(?:href|src)=|<script|currentColor/, name);
|
|
}
|
|
});
|
|
|
|
test('filled state, custom stroke widths, and unknown names stay usable', () => {
|
|
assert.match(decodeMask(createIconMask('heart', true, 2.4)), /fill="#000" stroke="#000" stroke-width="2.4"/);
|
|
assert.notEqual(createIconMask('heart'), createIconMask('heart', true));
|
|
assert.match(decodeMask(createIconMask('add', false, NaN)), /stroke-width="1.8"/);
|
|
assert.match(decodeMask(createIconMask('add', false, 999)), /stroke-width="8"/);
|
|
for (const name of ['unknown', '__proto__', 'constructor']) {
|
|
assert.equal(createIconMask(name), createIconMask('sparkles'));
|
|
}
|
|
});
|
|
|
|
test('all static AppIcon usages and bottom navigation names have real paths', () => {
|
|
let checked = 0;
|
|
for (const file of vueFiles(root)) {
|
|
const template = parse(readFileSync(file, 'utf8')).descriptor.template;
|
|
if (!template) continue;
|
|
for (const node of nodes(template.ast)) {
|
|
if (node.type !== 1 || node.tag !== 'AppIcon') continue;
|
|
const name = node.props.find((prop) => prop.type === 6 && prop.name === 'name')?.value?.content;
|
|
if (name) { assert.ok(Object.hasOwn(ICON_PATHS, name), `${file}: ${name}`); checked++; }
|
|
}
|
|
}
|
|
assert.ok(checked > 25);
|
|
for (const name of ['home', 'message', 'add', 'compass', 'user', 'search', 'pin', 'heart', 'back', 'more', 'smile', 'voice']) {
|
|
assert.ok(ICON_PATHS[name]?.length, name);
|
|
}
|
|
});
|