// 暗黑模式背景翻转验证(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
]
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);