update
This commit is contained in:
165
tests/unit/renderer/styles/theme-presets-contrast.test.ts
Normal file
165
tests/unit/renderer/styles/theme-presets-contrast.test.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
// tests/unit/renderer/styles/theme-presets-contrast.test.ts
|
||||
//
|
||||
// 守 r247「15 档预设都按 WCAG 校准过」的**对比度**契约。
|
||||
//
|
||||
// 为什么要有这个守卫:
|
||||
// 配色方案的「好不好看」很难自动判,但「看不看得清」可以。本守卫把每一档
|
||||
// 配方的关键色对量成 WCAG 2.x 对比度,低于阈值就红 —— 于是
|
||||
// 「新加一档配色,顺手把某个灰阶调暗了两档」这种事在 CI 就被拦住,
|
||||
// 不用等用户来吐槽「Nord 下正文发灰看不清」。
|
||||
//
|
||||
// 阈值出处(WCAG 2.1):
|
||||
// - 正文级文字 4.5:1 (SC 1.4.3 AA)
|
||||
// - 大号文字 / 图形控件 / 焦点环 3:1 (SC 1.4.11)
|
||||
// - 状态色的「实心底 + 白字」= 正文级 ⇒ 4.5:1
|
||||
//
|
||||
// 语义分工(见 globals.css「状态色两槽」注释):
|
||||
// --success-rgb 等是「色」(图标 / 文字 / 描边);
|
||||
// --success-strong-rgb 等是「底」(实心按钮 / toast 底 + 白字)——
|
||||
// Dracula 的原色 #50FA7B 压白字只有 1.37:1,所以底必须走 strong 档。
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { THEME_PRESETS } from '../../../../src/shared/types';
|
||||
import {
|
||||
parseTopLevelBlocks,
|
||||
findPresetBlock,
|
||||
findPreviewBlock,
|
||||
contrastRatio,
|
||||
token,
|
||||
WHITE,
|
||||
type ThemeBlock,
|
||||
} from './theme-css';
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const CSS = readFileSync(join(ROOT, 'src/renderer/src/styles/globals.css'), 'utf8');
|
||||
const blocks = parseTopLevelBlocks(CSS);
|
||||
|
||||
/** 每一档都要查的「块」:两块基础配方 + 14 档具体预设 */
|
||||
function allThemeBlocks(): Array<{ name: string; block: ThemeBlock }> {
|
||||
const out: Array<{ name: string; block: ThemeBlock }> = [];
|
||||
for (const polarity of ['dark', 'light'] as const) {
|
||||
const b = findPreviewBlock(blocks, polarity);
|
||||
if (b) out.push({ name: `基础${polarity}`, block: b });
|
||||
}
|
||||
for (const preset of THEME_PRESETS) {
|
||||
if (preset === 'default') continue;
|
||||
const b = findPresetBlock(blocks, preset);
|
||||
if (b) out.push({ name: preset, block: b });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const THEMES = allThemeBlocks();
|
||||
|
||||
/** 收集不达标的项,统一报错(比逐条 expect 好读) */
|
||||
function collect(
|
||||
themes: Array<{ name: string; block: ThemeBlock }>,
|
||||
check: (block: ThemeBlock) => { label: string; ratio: number; min: number },
|
||||
): string[] {
|
||||
const bad: string[] = [];
|
||||
for (const { name, block } of themes) {
|
||||
const { label, ratio, min } = check(block);
|
||||
if (ratio < min) bad.push(`${name}: ${label} = ${ratio.toFixed(2)}:1 (< ${min}:1)`);
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
|
||||
describe('主题预设的 WCAG 对比度(r247)', () => {
|
||||
it('量到了全部 16 个配方块(2 基础 + 14 预设)', () => {
|
||||
expect(THEMES.length).toBe(16);
|
||||
});
|
||||
|
||||
it('正文级文字 ≥ 4.5:1(fg / fg-secondary 压在主区 bg 上)', () => {
|
||||
const bad = collect(THEMES, (b) => ({
|
||||
label: 'fg vs bg',
|
||||
ratio: contrastRatio(token(b, 'fg-rgb'), token(b, 'bg-rgb')),
|
||||
min: 4.5,
|
||||
}));
|
||||
expect(bad, `正文对比度不足:\n${bad.join('\n')}`).toEqual([]);
|
||||
|
||||
const bad2 = collect(THEMES, (b) => ({
|
||||
label: 'fg-secondary vs bg',
|
||||
ratio: contrastRatio(token(b, 'fg-secondary-rgb'), token(b, 'bg-rgb')),
|
||||
min: 4.5,
|
||||
}));
|
||||
expect(bad2, `次级文字对比度不足:\n${bad2.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
it('三级文字 ≥ 4.5:1(fg-tertiary 压在 header / tabs 的 bg-raised 上)', () => {
|
||||
const bad = collect(THEMES, (b) => ({
|
||||
label: 'fg-tertiary vs bg-raised',
|
||||
ratio: contrastRatio(token(b, 'fg-tertiary-rgb'), token(b, 'bg-raised-rgb')),
|
||||
min: 4.5,
|
||||
}));
|
||||
expect(bad, `三级文字对比度不足:\n${bad.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
it('弱化文字 ≥ 3:1(fg-muted 压在面板 bg-surface 上)', () => {
|
||||
const bad = collect(THEMES, (b) => ({
|
||||
label: 'fg-muted vs bg-surface',
|
||||
ratio: contrastRatio(token(b, 'fg-muted-rgb'), token(b, 'bg-surface-rgb')),
|
||||
min: 3,
|
||||
}));
|
||||
expect(bad, `弱化文字对比度不足:\n${bad.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
it('强调色 ≥ 3:1(accent 作为图标 / 焦点环 / 链接底)', () => {
|
||||
const bad = collect(THEMES, (b) => ({
|
||||
label: 'accent vs bg',
|
||||
ratio: contrastRatio(token(b, 'accent-rgb'), token(b, 'bg-rgb')),
|
||||
min: 3,
|
||||
}));
|
||||
expect(bad, `强调色对比度不足:\n${bad.join('\n')}`).toEqual([]);
|
||||
|
||||
const bad2 = collect(THEMES, (b) => ({
|
||||
label: 'accent vs bg-raised',
|
||||
ratio: contrastRatio(token(b, 'accent-rgb'), token(b, 'bg-raised-rgb')),
|
||||
min: 3,
|
||||
}));
|
||||
expect(bad2, `强调色在 header 上对比度不足:\n${bad2.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
it('状态色 ≥ 3:1(作为文字 / 图标色时)', () => {
|
||||
for (const name of ['success', 'warning', 'danger']) {
|
||||
const bad = collect(THEMES, (b) => ({
|
||||
label: `${name} vs bg`,
|
||||
ratio: contrastRatio(token(b, `${name}-rgb`), token(b, 'bg-rgb')),
|
||||
min: 3,
|
||||
}));
|
||||
expect(bad, `${name} 对比度不足:\n${bad.join('\n')}`).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it('模块语义色 ≥ 3:1(侧栏模块图标 / 徽标)', () => {
|
||||
for (const name of ['module-library', 'module-notes']) {
|
||||
const bad = collect(THEMES, (b) => ({
|
||||
label: `${name} vs bg`,
|
||||
ratio: contrastRatio(token(b, `${name}-rgb`), token(b, 'bg-rgb')),
|
||||
min: 3,
|
||||
}));
|
||||
expect(bad, `${name} 对比度不足:\n${bad.join('\n')}`).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it('实心底 + 白字 ≥ 4.5:1(success-strong / warning-strong / danger-strong)', () => {
|
||||
for (const name of ['success-strong', 'warning-strong', 'danger-strong']) {
|
||||
const bad = collect(THEMES, (b) => ({
|
||||
label: `white vs ${name}`,
|
||||
ratio: contrastRatio(WHITE, token(b, `${name}-rgb`)),
|
||||
min: 4.5,
|
||||
}));
|
||||
expect(bad, `${name} 压白字对比度不足:\n${bad.join('\n')}`).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it('滚动条 thumb ≥ 3:1(压在面板 bg-surface 上)', () => {
|
||||
const bad = collect(THEMES, (b) => ({
|
||||
label: 'scrollbar-thumb vs bg-surface',
|
||||
ratio: contrastRatio(token(b, 'scrollbar-thumb-rgb'), token(b, 'bg-surface-rgb')),
|
||||
min: 3,
|
||||
}));
|
||||
expect(bad, `滚动条对比度不足:\n${bad.join('\n')}`).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user