Files
Literature-Management/tests/unit/renderer/styles/theme-presets-token-coverage.test.ts
2026-10-21 14:42:59 +08:00

141 lines
5.2 KiB
TypeScript

// tests/unit/renderer/styles/theme-presets-token-coverage.test.ts
//
// 守 r247「主题预设扩到 15 档」的**配方完整性**契约:
//
// 每档预设(除 default 外)在 globals.css 里都必须有一块自己的配方,
// 且该块必须把 **全部 28 个颜色 token** 都定义齐 —— 少一个,那个 token
// 就会**继承**自 :root(基础暗色 / 浅色配方),于是「选 Nord 却看到一截
// 基础暗色的灰」这种半吊子配色,而且极难被发现(只有切到那一档才看得见)。
//
// 为什么这是行为面而不是实现细节:
// 预设清单的真相源是 shared/types.ts 的 THEME_PRESETS;CSS 配方是它的
// **另一半**。加了一档预设却忘了写 CSS ⇒ 用户在设置页点得到那一档,
// 主题「切了但没变」(或只变了一半)—— 这正是 r125 / r229 在字体族、
// 主题枚举上修过的同款漂移。
//
// 守卫点:
// 1. THEME_PRESETS 里每个非 default 预设都有对应的 CSS 块;
// 2. 每块(以及两块基础配方)都定义了全部必需 token;
// 3. 每块都声明了 color-scheme(浏览器原生控件 / 滚动条 / 表单跟随);
// 4. 没有「孤儿块」:CSS 里有、THEME_PRESETS 里没有的预设(删预设漏删 CSS)。
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,
} from './theme-css';
const ROOT = process.cwd();
const CSS = readFileSync(join(ROOT, 'src/renderer/src/styles/globals.css'), 'utf8');
/**
* 每一档配方必须定义齐的颜色 token。
*
* 分组与 globals.css 的设计 token 注释一一对应:
* 背景 6 档 + 边框 4 档 + 文字 4 档 + 强调 3 档
* + 状态 6 个(色 / 实心底 各 3)+ 模块语义 2 + 遮罩 1 + 滚动条 1
*/
const REQUIRED_TOKENS = [
// 背景层级
'bg-rgb',
'bg-canvas-rgb',
'bg-surface-rgb',
'bg-raised-rgb',
'bg-hover-rgb',
'bg-active-rgb',
'bg-selected-rgb',
// 边框
'border-rgb',
'border-subtle-rgb',
'border-strong-rgb',
'border-focus-rgb',
// 文字四级灰阶
'fg-rgb',
'fg-secondary-rgb',
'fg-tertiary-rgb',
'fg-muted-rgb',
// 强调
'accent-rgb',
'accent-hover-rgb',
'accent-dim-rgb',
// 状态色(色 / 实心底 两槽)
'success-rgb',
'success-strong-rgb',
'warning-rgb',
'warning-strong-rgb',
'danger-rgb',
'danger-strong-rgb',
// 模块语义色 / 滚动条
'module-library-rgb',
'module-notes-rgb',
'scrollbar-thumb-rgb',
] as const;
/**
* 刻意**不**要求每档预设都定义的 token:
* overlay-scrim-rgb —— 遮罩层永远是纯黑(浅色主题上也是黑遮罩,这是
* 桌面端通用做法)。它随 :root 继承即可,逐档重复写没有意义。
*/
const blocks = parseTopLevelBlocks(CSS);
describe('主题配方 token 完整性(r247)', () => {
it('两块基础配方(暗 / 浅)定义了全部必需 token', () => {
for (const polarity of ['dark', 'light'] as const) {
const block = findPreviewBlock(blocks, polarity);
expect(block, `globals.css 里找不到 [data-theme-preview='${polarity}'] 基础配方块`).toBeDefined();
const missing = REQUIRED_TOKENS.filter((t) => !block!.tokens.has(t));
expect(missing, `基础${polarity}配方缺 token: ${missing.join(', ')}`).toEqual([]);
}
});
it('每档预设都有对应的 CSS 配方块', () => {
const missing = THEME_PRESETS.filter((p) => p !== 'default' && !findPresetBlock(blocks, p));
expect(missing, `这些预设没有 CSS 配方块(切过去会继承基础配方): ${missing.join(', ')}`).toEqual(
[],
);
});
it('每档预设都定义了全部必需 token', () => {
const problems: string[] = [];
for (const preset of THEME_PRESETS) {
if (preset === 'default') continue;
const block = findPresetBlock(blocks, preset);
if (!block) continue; // 上一条已报
const missing = REQUIRED_TOKENS.filter((t) => !block.tokens.has(t));
if (missing.length) problems.push(`${preset} 缺 ${missing.join(', ')}`);
}
expect(problems, `预设配方不完整:\n${problems.join('\n')}`).toEqual([]);
});
it('每档预设都声明了 color-scheme(原生控件 / 滚动条跟随)', () => {
const problems: string[] = [];
for (const preset of THEME_PRESETS) {
if (preset === 'default') continue;
const block = findPresetBlock(blocks, preset);
if (!block) continue;
const scheme = block.decls.get('color-scheme');
if (scheme !== 'light' && scheme !== 'dark') {
problems.push(`${preset} → color-scheme=${String(scheme)}`);
}
}
expect(problems, `这些预设没声明 color-scheme:\n${problems.join('\n')}`).toEqual([]);
});
it('没有孤儿预设块(CSS 里有、THEME_PRESETS 里没有的预设)', () => {
const orphans: string[] = [];
for (const block of blocks) {
const m = block.selector.match(/\[data-theme-preset='([^']+)'\]/);
if (!m) continue;
if (!(THEME_PRESETS as readonly string[]).includes(m[1])) orphans.push(m[1]);
}
expect(orphans, `这些 CSS 预设块已不在 THEME_PRESETS 里(删预设漏删 CSS): ${orphans.join(', ')}`).toEqual(
[],
);
});
});