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