import { ThemeManager, THEME_KEYS, THEMES, ThemeKey, ThemeItem } from '../src/core/theme/theme.manager'; import * as fs from 'fs'; import * as path from 'path'; /** * 辅助函数:计算 Hex 颜色的相对亮度 (Relative Luminance) * 遵循 W3C WCAG 2.1 规范算法 */ function getLuminance(hex: string): number { const cleanHex = hex.replace('#', ''); const r = parseInt(cleanHex.substring(0, 2), 16) / 255; const g = parseInt(cleanHex.substring(2, 4), 16) / 255; const b = parseInt(cleanHex.substring(4, 6), 16) / 255; const a = [r, g, b].map(v => { return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }); return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; } /** * 辅助函数:计算两组 Hex 颜色的对比度 (Contrast Ratio) */ function getContrastRatio(hex1: string, hex2: string): number { const lum1 = getLuminance(hex1); const lum2 = getLuminance(hex2); const brightest = Math.max(lum1, lum2); const darkest = Math.min(lum1, lum2); return (brightest + 0.05) / (darkest + 0.05); } describe('ThemeManager (TDD 测试套件: 暖色系与双主题设计系统)', () => { let themeManager: ThemeManager; beforeEach(() => { themeManager = new ThemeManager(); }); describe('1. 主题 Design Tokens 完备性与格式规范', () => { it('应完整支持 cool (冷光暗夜) 与 warm (暖阳赤焰) 双套主题', () => { const themes = themeManager.getAvailableThemes(); const keys = themes.map((t: ThemeItem) => t.key); expect(keys).toContain(THEME_KEYS.COOL); expect(keys).toContain(THEME_KEYS.WARM); expect(themes.length).toBe(2); }); it('暖色系 (warm) 必须包含全部核心色彩语义 Tokens', () => { const warmTheme = THEMES[THEME_KEYS.WARM]; expect(warmTheme).toBeDefined(); const requiredTokens = [ 'bgPrimary', 'bgSurface', 'bgSurfaceElevated', 'bgSurfaceGlass', 'colorBrand', 'colorBrandLight', 'colorBrandGlow', 'borderSubtle', 'textPrimary', 'textSecondary', 'textMuted' ]; for (const token of requiredTokens) { expect(warmTheme.tokens[token as keyof typeof warmTheme.tokens]).toBeDefined(); } }); it('暖色系关键颜色必须符合合法的 Hex 与 rgba 语法格式', () => { const tokens = THEMES[THEME_KEYS.WARM].tokens; const hexRegex = /^#([0-9A-Fa-f]{6})$/; const rgbaRegex = /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)$/; expect(hexRegex.test(tokens.bgPrimary)).toBe(true); expect(hexRegex.test(tokens.bgSurface)).toBe(true); expect(hexRegex.test(tokens.colorBrand)).toBe(true); expect(hexRegex.test(tokens.textPrimary)).toBe(true); expect(rgbaRegex.test(tokens.colorBrandGlow)).toBe(true); }); }); describe('2. WCAG AA 级无障碍色彩对比度验证', () => { it('暖色系一级主文本 (textPrimary) 对底色与卡片底色对比度必须 >= 7:1 (超越 AA 级 4.5:1)', () => { const tokens = THEMES[THEME_KEYS.WARM].tokens; const ratioOnBg = getContrastRatio(tokens.textPrimary, tokens.bgPrimary); const ratioOnSurface = getContrastRatio(tokens.textPrimary, tokens.bgSurface); expect(ratioOnBg).toBeGreaterThanOrEqual(7.0); expect(ratioOnSurface).toBeGreaterThanOrEqual(7.0); }); it('暖色系二级辅助文本 (textSecondary) 对底色对比度必须 >= 4.5:1 (符合 WCAG AA 标准)', () => { const tokens = THEMES[THEME_KEYS.WARM].tokens; const ratio = getContrastRatio(tokens.textSecondary, tokens.bgPrimary); expect(ratio).toBeGreaterThanOrEqual(4.5); }); }); describe('3. 主题切换状态机与防御性控制', () => { it('默认初始主题应为 cool (冷光暗夜)', () => { expect(themeManager.getCurrentThemeKey()).toBe(THEME_KEYS.COOL); expect(themeManager.getCurrentTheme().name).toBe('冷光暗夜'); }); it('成功切换到 warm (暖阳赤焰) 主题', () => { const current = themeManager.switchTheme(THEME_KEYS.WARM); expect(current.key).toBe(THEME_KEYS.WARM); expect(current.name).toBe('暖阳赤焰'); expect(themeManager.getCurrentThemeKey()).toBe(THEME_KEYS.WARM); expect(themeManager.getCurrentTokens().colorBrand).toBe('#F97316'); }); it('传入非法主题标识时,应平滑回退到默认 cool 主题,确保系统不崩溃', () => { const fallback = themeManager.switchTheme('non_existent_theme' as ThemeKey); expect(fallback.key).toBe(THEME_KEYS.COOL); }); }); describe('4. 前端代码纯净度与 Emoji 门禁校验', () => { it('主题配置文件中严禁包含任何系统 Emoji', () => { const rootDir = path.resolve(__dirname, '../../'); const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; const filesToCheck = [ path.join(rootDir, 'server', 'src', 'core', 'theme', 'theme.manager.ts'), path.join(rootDir, 'pwuniapp', 'common', 'theme.js') ]; for (const file of filesToCheck) { if (fs.existsSync(file)) { const content = fs.readFileSync(file, 'utf-8'); const lines = content.split('\n'); lines.forEach((line, idx) => { expect({ file: path.relative(rootDir, file), line: idx + 1, hasEmoji: emojiRegex.test(line) }).toEqual({ file: path.relative(rootDir, file), line: idx + 1, hasEmoji: false }); }); } } }); }); });