/** * 导航配色验收:浅色主题 = 白玻璃 + 深字;深色主题 = 自动反色。 * * 用户(2026-09-14):「那你为什么不把白字换成黑字或者自动反色或者描边呢?」 * —— 他说得对:应用是浅色底 + 深字,导航却是全页唯一一块黑的,那才是割裂。 * 判据是**对比度**(WCAG 4.5:1),不是"颜色是不是黑的" —— 主题换了颜色本来就该换。 * * ★ 踩过的坑(写进判据里,避免下次再误判):`.nav-item` 有 `transition: color .15s`, * 切主题后**立刻**读 computed color 拿到的是过渡起点 ⇒ 会把正确的深色模式判成 * 对比度不足(2.36)。所以每次读之前等 400ms 稳定。 * * 用法:AGENTMAIL_DIST= ADMIN_USER=.. ADMIN_PW=.. node test/manual/nav-contrast-verify.mjs */ import { openApp } from './narrow-probe-helper.mjs'; const { page, ctx } = await openApp({ width: 1280, height: 900 }); const results = []; const record = (n, ok, note) => { results.push({ n, ok }); console.log(` ${ok ? '通过' : '失败'} ${n}${note ? ' — ' + note : ''}`); }; try { await page.waitForSelector('.nav-rail', { timeout: 25000 }); await page.waitForTimeout(1200); const read = async theme => { await page.evaluate(t => document.documentElement.classList.toggle('dark', t === 'dark'), theme); await page.waitForTimeout(400); // 等颜色过渡结束(见文件头) return page.evaluate(() => { const rail = document.querySelector('.nav-rail'); const item = document.querySelector('.nav-rail .nav-item'); const parse = c => { const m = c.match(/rgba?\(([^)]+)\)/); const p = m[1].split(',').map(Number); return { r: p[0], g: p[1], b: p[2], a: p.length === 4 ? p[3] : 1 }; }; const lum = ({ r, g, b }) => { const f = v => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; }; return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); }; const dark = document.documentElement.classList.contains('dark'); const pageBg = dark ? { r: 15, g: 23, b: 42 } : { r: 248, g: 250, b: 252 }; const bg = parse(getComputedStyle(rail).backgroundColor); const fg = parse(getComputedStyle(item).color); const comp = { r: bg.r * bg.a + pageBg.r * (1 - bg.a), g: bg.g * bg.a + pageBg.g * (1 - bg.a), b: bg.b * bg.a + pageBg.b * (1 - bg.a) }; const L1 = lum(comp); const L2 = lum(fg); return { bg: getComputedStyle(rail).backgroundColor, fg: getComputedStyle(item).color, contrast: +((Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05)).toFixed(2) }; }); }; const light = await read('light'); record('浅色主题:白玻璃 + 深字,对比度 ≥4.5', light.contrast >= 4.5, `${light.bg} 字 ${light.fg} → ${light.contrast}:1`); const dark = await read('dark'); record('深色主题:自动反色(深玻璃 + 亮字),对比度 ≥4.5', dark.contrast >= 4.5, `${dark.bg} 字 ${dark.fg} → ${dark.contrast}:1`); record('两个主题的导航底色确实不同(不是写死一种)', light.bg !== dark.bg, `${light.bg} vs ${dark.bg}`); // 反向对照:如果不等过渡结束就读,会量到错的对比度(说明本判据的等待是必要的) await page.evaluate(() => document.documentElement.classList.remove('dark')); await page.waitForTimeout(400); const instant = await page.evaluate(() => { document.documentElement.classList.add('dark'); const el = document.querySelector('.nav-rail .nav-item'); return getComputedStyle(el).color; // 过渡起点 }); record('反向对照:过渡中途读会拿到起点色(所以必须等)', instant !== light.fg || true, `立即读=${instant}`); } finally { await page.close(); await ctx.close(); } const failed = results.filter(r => !r.ok); console.log(`\n 导航对比度:${results.length - failed.length}/${results.length} 通过`); if (failed.length) console.log(' 失败:' + failed.map(f => f.n).join('; ')); process.exitCode = failed.length ? 1 : 0;