115 lines
4.7 KiB
TypeScript
115 lines
4.7 KiB
TypeScript
/**
|
|
* 主题色派生单测(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')
|
|
})
|