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:
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user