feat(theme): 主题三态偏好模型,默认跟随系统自动切换暗黑模式

- 新增 src/lib/theme.ts:三态偏好单一真源
  (system/light/dark,无存储值默认跟随系统)
- ThemeToggle 改三态循环(Monitor→Sun→Moon):
  实时监听 prefers-color-scheme(仅 system 档生效)
  + storage 跨标签页同步;尺寸位置不变
- layout.tsx FOUC 内联脚本支持 system 档,
  首帧前解析避免闪烁
- e2e 视觉回归 L2 改用 test.use({ colorScheme }) 驱动
  (事后 setAttribute 存在被挂载效果覆盖的竞态)
- header.test.tsx lucide 白名单补 Monitor
- CLAUDE.md 同步三态语义与 variant 踩坑
- 新增 scripts/audit/theme-system-verify.mjs
  (55 断言:首帧无闪烁/循环/实时跟随/旧值兼容/
  污染回退/6 路由冒烟)

验证:单测 131 套件 1662 通过 0 失败;
tsc 0 错;eslint 0 错;next build 通过;
运行时 55/55 PASS(dogfood-output/theme-system-verify/)
This commit is contained in:
2026-09-20 11:50:58 +08:00
parent 5159862e96
commit 0bd1e9a7e2
9 changed files with 962 additions and 71 deletions
+21 -17
View File
@@ -140,30 +140,34 @@ test.describe('L2: 组件视觉状态测试', () => {
test.describe('L2: 主题切换视觉测试', () => {
test.setTimeout(45000);
test('浅色主题 - 首页视觉', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await waitForPageStable(page);
// 主题由「系统偏好」驱动(默认跟随系统,见 src/lib/theme.ts),因此用 Playwright 的
// colorScheme 上下文选项模拟 OS 偏好,而不是事后 setAttribute
// 挂载后的 ThemeToggle 会把 html[data-theme] 重置为解析结果,事后改属性存在竞态
// hydration 晚于 setAttribute 时会被覆盖,深色快照拍成浅色)。
test.describe('系统偏好浅色', () => {
test.use({ colorScheme: 'light' });
const htmlEl = page.locator('html');
const currentTheme = await htmlEl.getAttribute('data-theme') || 'light';
test('浅色主题 - 首页视觉', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await waitForPageStable(page);
if (currentTheme !== 'light') {
await htmlEl.evaluate(el => el.setAttribute('data-theme', 'light'));
await page.waitForTimeout(300);
}
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
await expect(page.locator('main').first()).toHaveScreenshot('theme-light-main.png');
await expect(page.locator('main').first()).toHaveScreenshot('theme-light-main.png');
});
});
test('深色主题 - 首页视觉', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await waitForPageStable(page);
test.describe('系统偏好深色', () => {
test.use({ colorScheme: 'dark' });
const htmlEl = page.locator('html');
await htmlEl.evaluate(el => el.setAttribute('data-theme', 'dark'));
await page.waitForTimeout(500);
test('深色主题 - 首页视觉', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await waitForPageStable(page);
await expect(page.locator('main').first()).toHaveScreenshot('theme-dark-main.png');
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
await expect(page.locator('main').first()).toHaveScreenshot('theme-dark-main.png');
});
});
});