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(/ { 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); } });