/** * 图标解析(@/utils/iconResolver):字符串 → 组件。 * * 重点是三条边界,它们各自对应一类真实故障: * 1. 名称归一化:`chevron-down` / `chevronDown` / `CHEVRON_DOWN` 都要能解析, * 否则历史数据里换过写法的图标会**静默丢失**; * 2. SVG 源码:lucide 覆盖不到的行业专有图标靠它兜底,但必须校验合法性, * 不能把随便一段文本当 SVG 塞进 innerHTML(那是 XSS / 布局炸掉的口子); * 3. 与侧栏共用同一解析器:菜单管理里能预览出来的图标,侧栏必须也渲染得出来。 */ import { describe, expect, it } from 'vitest' import { getIcon, getIconNames, isValidIconValue, looksLikeSvg, normalizeIconName } from '@/utils/iconResolver' import { getMenuIcon } from '@/layouts/icons' describe('图标名归一化', () => { it('kebab / snake / camel / 大写 都归一成 PascalCase', () => { expect(normalizeIconName('chevron-down')).toBe('ChevronDown') expect(normalizeIconName('chevron_down')).toBe('ChevronDown') expect(normalizeIconName('chevronDown')).toBe('ChevronDown') expect(normalizeIconName('CHEVRON DOWN')).toBe('ChevronDown') }) it('单个词只做首字母大写', () => { expect(normalizeIconName('ship')).toBe('Ship') expect(normalizeIconName('Ship')).toBe('Ship') }) it('空值返回空串(不抛错)', () => { expect(normalizeIconName('')).toBe('') expect(normalizeIconName(' ')).toBe('') }) }) describe('getIcon:解析图标名', () => { it('kebab 写法可解析(与 s_menu 存量数据一致)', () => { expect(getIcon('circle-check')).toBeTruthy() expect(getIcon('file-text')).toBeTruthy() }) it('PascalCase 与 camelCase 同样可解析(历史数据兼容)', () => { expect(getIcon('CircleCheck')).toBeTruthy() expect(getIcon('circleCheck')).toBeTruthy() }) it('lucide: 前缀显式指定也能解析', () => { expect(getIcon('lucide:chevron-down')).toBeTruthy() expect(getIcon('lucide:ship')).toBeTruthy() }) it('白名单时代的 15 个图标全部仍可解析(升级不能弄丢现有菜单的图标)', () => { const legacy = [ 'layout-dashboard', 'file-text', 'database', 'table', 'list-checks', 'circle-check', 'users', 'building-2', 'ship', 'plane', 'container', 'package', 'wallet', 'blocks', 'settings', ] for (const name of legacy) { expect(getIcon(name), `${name} 应能解析`).toBeTruthy() } }) it('白名单外的合法 lucide 图标现在也能解析(本次改造的核心)', () => { expect(getIcon('truck')).toBeTruthy() expect(getIcon('calendar')).toBeTruthy() expect(getIcon('receipt')).toBeTruthy() }) it('空值与无效名返回 null(不抛错)', () => { expect(getIcon('')).toBeNull() expect(getIcon(null)).toBeNull() expect(getIcon(undefined)).toBeNull() expect(getIcon(' ')).toBeNull() expect(getIcon('这根本不是图标名')).toBeNull() }) it('lucide: 前缀匹配不到时返回 null,不退回当 SVG 试', () => { expect(getIcon('lucide:not-a-real-icon-xyz')).toBeNull() }) }) describe('getIcon:SVG 源码', () => { const SVG = '' it('合法 SVG 标记可渲染', () => { expect(getIcon(SVG)).toBeTruthy() }) it('同一段 SVG 返回同一组件实例(否则每次渲染都会卸载重建、图标闪烁)', () => { expect(getIcon(SVG)).toBe(getIcon(SVG)) }) it('非 SVG 的普通文本不渲染(不能把任意文本塞进 innerHTML)', () => { expect(getIcon('