问题:Tailwind 原生 bg-white 是硬编码值,深色主题下不翻转, 导致整页/整 section 恒为纯白,与转深色的页头页脚割裂。 (globals.css:--color-bg-primary-rgb 浅色 255 255 255 → 深色 10 14 20) 改动:10 文件 67 处 bg-white → 项目 token,按语义二分类 - bg-bg-primary (33 处):页面/大块骨架(min-h-screen 容器、<section>、<main>) - bg-bg-elevated (34 处):浮起层(带 border 的卡片、表单输入、图标盒、grid 单元格) --color-bg-elevated 浅色 #FFFFFF → 深色 #151B23,同样翻转。 浅色主题下 bg-white / bg-bg-primary / bg-bg-elevated 三者同值(纯白), 故本次改动在浅色主题下零像素差异,只修复深色主题。 新增审计工具: - scripts/audit/hardcoded-color-audit.mjs:硬编码颜色分级盘点 (含 alpha 误报修正:bg-white/NN 半透明叠加两主题均成立,判 INFO 非 P0) - scripts/audit/darkmode-bg-verify.mjs:直接验证背景随主题翻转 (浅色应纯白、深色应 rgb(10,14,20) 且无残留纯白块) 审计结果:marketing P0 67 → 0;全站 P0 97 → 30 (剩余 components 16 / other 8 / layout 3 / ui-kit 3 为第二批;admin 本就为 0) 门禁: - tsc 0 error - eslint 0 error(50 warnings 全为既有,改动文件未新增) - jest 129/130 套件通过,1548 通过 / 2 跳过 / **0 断言失败** 唯一失败 src/lib/admin-api.test.ts 为**沙箱环境限制非代码问题**: CODEBUDDY_BROKER_DENY 拦截 readFileSync,单独复验同样失败, 与本次改动零交集(改动仅限 src/app/(marketing)/ 下 10 个页面组件) - 暗黑模式背景验证 18/18 PASS(9 路由 × 浅深双版)
103 lines
4.2 KiB
JavaScript
103 lines
4.2 KiB
JavaScript
// 暗黑模式背景翻转验证(Phase 2 token 化专用)
|
|
//
|
|
// 用途:直接验证「背景随主题翻转」这一改动目标,而不只是验证「页面没报错」。
|
|
//
|
|
// 机理(globals.css):
|
|
// --color-bg-primary-rgb 浅色 255 255 255 (L70) → 深色 10 14 20 (L409)
|
|
// --color-bg-elevated 浅色 #FFFFFF (L69) → 深色 #151B23 (L415)
|
|
// 因此浅色主题下 bg-bg-primary / bg-bg-elevated 都渲染为纯白 —— 与改动前的
|
|
// bg-white **像素一致**(零回归);深色主题下才显现差异。
|
|
//
|
|
// 断言:
|
|
// light : 根节点背景 == rgb(255, 255, 255)(与改动前一致)
|
|
// dark : 根节点背景 != rgb(255, 255, 255) 且亮度低(已翻转为深色)
|
|
// dark : 页面内不得残留大面积纯白块(抽样主要 section / 卡片)
|
|
//
|
|
// 环境约束(2026-09-02 踩坑固化):
|
|
// - 必须 localhost(非 127.0.0.1):Next dev allowedDevOrigins 白名单不含 127.0.0.1
|
|
// - 必须 --no-proxy-server:本机系统代理会拦 403
|
|
//
|
|
// CLI: node scripts/audit/darkmode-bg-verify.mjs [--routes /,/contact] [--out <dir>]
|
|
|
|
import { chromium } from 'playwright';
|
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
|
|
const args = process.argv.slice(2);
|
|
const getList = (flag, def) => {
|
|
const i = args.indexOf(flag);
|
|
return i >= 0 && args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1].split(',') : def;
|
|
};
|
|
const ROUTES = getList('--routes', ['/', '/contact', '/products', '/solutions', '/team', '/cases', '/news', '/methodology', '/products/erp-upgrade']);
|
|
const BASE = 'http://localhost:3000';
|
|
const OUT = 'dogfood-color-audit';
|
|
mkdirSync(OUT, { recursive: true });
|
|
|
|
const browser = await chromium.launch({ args: ['--no-proxy-server'] });
|
|
|
|
/** 亮度:0(黑) ~ 255(白) */
|
|
function luma(rgb) {
|
|
const m = rgb.match(/\d+/g);
|
|
if (!m) return null;
|
|
const [r, g, b] = m.map(Number);
|
|
return Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b);
|
|
}
|
|
|
|
async function probe(path, theme) {
|
|
const ctx = await browser.newContext();
|
|
const page = await ctx.newPage();
|
|
await page.addInitScript((t) => {
|
|
try { localStorage.setItem('novalon-theme', t); } catch {}
|
|
}, theme);
|
|
|
|
const errors = [];
|
|
page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });
|
|
page.on('pageerror', (e) => errors.push('PAGEERROR: ' + e.message));
|
|
|
|
await page.goto(BASE + path, { waitUntil: 'networkidle', timeout: 45000 });
|
|
|
|
const info = await page.evaluate(() => {
|
|
const pick = (el) => (el ? getComputedStyle(el).backgroundColor : null);
|
|
const body = pick(document.body);
|
|
// 抽样:主内容区的前若干 section / 大块容器
|
|
const blocks = Array.from(document.querySelectorAll('section, main, [class*="min-h-screen"]'))
|
|
.slice(0, 12)
|
|
.map((el) => getComputedStyle(el).backgroundColor);
|
|
return { body, blocks, html: getComputedStyle(document.documentElement).backgroundColor };
|
|
});
|
|
|
|
await ctx.close();
|
|
|
|
const bodyLuma = luma(info.body);
|
|
// 页面里仍然纯白的块(排除透明)
|
|
const whiteBlocks = info.blocks.filter((c) => luma(c) !== null && luma(c) >= 250);
|
|
|
|
let ok;
|
|
let note;
|
|
if (theme === 'light') {
|
|
ok = bodyLuma !== null && bodyLuma >= 250;
|
|
note = `body=${info.body} (luma=${bodyLuma}),浅色应为纯白(与改动前一致)`;
|
|
} else {
|
|
ok = bodyLuma !== null && bodyLuma < 60 && whiteBlocks.length === 0;
|
|
note = `body=${info.body} (luma=${bodyLuma}),残留纯白块 ${whiteBlocks.length} 个`;
|
|
}
|
|
|
|
return { path, theme, ok, bodyLuma, whiteBlocks: whiteBlocks.length, errors, note };
|
|
}
|
|
|
|
const results = [];
|
|
for (const r of ROUTES) {
|
|
results.push(await probe(r, 'light'));
|
|
results.push(await probe(r, 'dark'));
|
|
}
|
|
await browser.close();
|
|
|
|
const failed = results.filter((r) => !r.ok || r.errors.length > 0);
|
|
writeFileSync(`${OUT}/darkmode-bg-verify.json`, JSON.stringify({ generatedAt: new Date().toISOString(), routes: ROUTES, results, failedCount: failed.length }, null, 2));
|
|
|
|
for (const r of results) {
|
|
console.log(`${r.ok && r.errors.length === 0 ? 'PASS' : 'FAIL'} ${r.theme.padEnd(5)} ${r.path.padEnd(24)} ${r.note}`);
|
|
r.errors.slice(0, 2).forEach((e) => console.log(` └ ${e.slice(0, 140)}`));
|
|
}
|
|
console.log(`\n${results.length - failed.length}/${results.length} PASS`);
|
|
process.exit(failed.length > 0 ? 1 : 0);
|