/** * 主题色派生单测(Node 内置 test runner)。 * * 重点不是「算得对不对」,而是「任何预设色在两种模式下都可读」—— * 对比度是这条链路上唯一会真正伤到用户的指标。 */ import { test } from 'node:test' import assert from 'node:assert/strict' import { ACCENT_PRESETS, ACCENT_VAR_KEYS, contrastRatio, deriveAccent, hexToOklch, oklchToHex, resolveAccentHex, type ThemeMode } from './theme.ts' const MODES: ThemeMode[] = ['light', 'dark'] /** 深色模式的对比度基准(与实现里的 DARK_REFERENCE 一致) */ const DARK_REFERENCE = '#1e1e22' test('hex → OKLCH → hex 往返稳定', () => { for (const preset of ACCENT_PRESETS) { const back = oklchToHex(hexToOklch(preset.hex)) const [r1, g1, b1] = [1, 3, 5].map(i => parseInt(preset.hex.slice(i, i + 2), 16)) const [r2, g2, b2] = [1, 3, 5].map(i => parseInt(back.slice(i, i + 2), 16)) // 允许 ±1/255 的取整误差 assert.ok(Math.abs(r1 - r2) <= 1, `${preset.id} R: ${preset.hex} → ${back}`) assert.ok(Math.abs(g1 - g2) <= 1, `${preset.id} G: ${preset.hex} → ${back}`) assert.ok(Math.abs(b1 - b2) <= 1, `${preset.id} B: ${preset.hex} → ${back}`) } }) test('默认蓝派生结果贴近现有手调值', () => { const light = deriveAccent('#0066cc', 'light') assert.equal(light['--primary'], '#0066cc') // 现有 --primary-soft / --primary-soft-text = #e8f1fc / #004f9e,只要求同一色族与量级 assert.equal(hexToOklch(light['--primary-soft']).l > 0.9, true) assert.equal(contrastRatio(light['--primary-soft-text'], light['--primary-soft']) >= 4.5, true) }) test('所有预设 × 两种模式:强调色对底色、浅底文字对浅底、按钮文字对按钮底 均 ≥ 4.5:1', () => { for (const preset of ACCENT_PRESETS) { for (const mode of MODES) { const vars = deriveAccent(preset.hex, mode) const reference = mode === 'light' ? '#ffffff' : DARK_REFERENCE const label = `${preset.id}/${mode}` assert.ok( contrastRatio(vars['--primary'], reference) >= 4.5, `${label} primary 对底对比度不足: ${contrastRatio(vars['--primary'], reference).toFixed(2)}` ) assert.ok( contrastRatio(vars['--primary-soft-text'], vars['--primary-soft']) >= 4.5, `${label} soft-text 对 soft 对比度不足: ${contrastRatio( vars['--primary-soft-text'], vars['--primary-soft'] ).toFixed(2)}` ) // 填充上的文字:浅色模式必然 ≥4.5:1;深色模式强调色偏亮,白字约 2.7–3:1 //(与现有 #2997ff + 白字 的取舍一致),门槛按 UI 组件的 2.5:1 兜底 const fillMin = mode === 'light' ? 4.5 : 2.5 assert.ok( contrastRatio(vars['--primary-foreground'], vars['--primary']) >= fillMin, `${label} foreground 对 primary 对比度不足: ${contrastRatio( vars['--primary-foreground'], vars['--primary'] ).toFixed(2)}` ) assert.equal(vars['--ring'], vars['--primary'], `${label} ring 应等于 primary`) } } }) test('亮色(青柠)在浅色模式下被自动压暗', () => { const base = hexToOklch('#65a30d') const derived = hexToOklch(deriveAccent('#65a30d', 'light')['--primary']) // 青柠原色在白底上只有约 2.8:1,派生后必须达标且比原色暗 assert.ok(derived.l < base.l, `未压暗: ${base.l} → ${derived.l}`) assert.ok(contrastRatio(oklchToHex(derived), '#ffffff') >= 4.5) }) test('色相保留(除被 sRGB 色域裁剪的极端彩度)', () => { for (const preset of ACCENT_PRESETS) { for (const mode of MODES) { const base = hexToOklch(preset.hex) const derived = hexToOklch(deriveAccent(preset.hex, mode)['--primary']) if (derived.c < 0.02) continue // 石墨这类近无彩色,色相无意义 const diff = Math.abs(((derived.h - base.h + 540) % 360) - 180) assert.ok(diff < 12, `${preset.id}/${mode} 色相偏移过大: ${base.h} → ${derived.h}`) } } }) test('派生结果都是合法 hex,且每个变量都有值', () => { for (const preset of ACCENT_PRESETS) { for (const mode of MODES) { const vars = deriveAccent(preset.hex, mode) for (const key of ACCENT_VAR_KEYS) { assert.match(vars[key], /^#[0-9a-f]{6}$/, `${preset.id}/${mode} ${key} 非法: ${vars[key]}`) } } } }) test('resolveAccentHex:默认与非法自定义都回落到内置默认', () => { assert.equal(resolveAccentHex('blue', null), null) assert.equal(resolveAccentHex('custom', null), null) assert.equal(resolveAccentHex('custom', 'not-a-color'), null) assert.equal(resolveAccentHex('custom', '#ffd60a'), '#ffd60a') assert.equal(resolveAccentHex('violet', null), '#7c3aed') })