/** * 强调色可见性验收:真实渲染 + 取实际计算色 + 算 WCAG 对比度。 * * 结构性断言(test/theme.test.mjs)只能保证变量存在、档位达标; * 「按钮到底看得见吗」必须在真浏览器里量 —— 上一次那个 bug 正是 * 所有静态检查都过、只有肉眼能发现。 */ import { openApp, WIDE } from './narrow-probe-helper.mjs'; const srgb = c => { c /= 255; return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; }; const L = ([r, g, b]) => 0.2126 * srgb(r) + 0.7152 * srgb(g) + 0.0722 * srgb(b); const ratio = (a, b) => { const l1 = L(a), l2 = L(b); const [h, o] = l1 > l2 ? [l1, l2] : [l2, l1]; return (h + 0.05) / (o + 0.05); }; const parse = s => (s.match(/\d+/g) || []).slice(0, 3).map(Number); // 把一批强调色类名注入页面,量它们的实际计算值 // hover 档(bg-red-700 / bg-blue-700)不在这里:源码里它们只以 `hover:` 变体 // 出现,Tailwind 因此不生成基础类 —— 探它会得到透明背景,那是探针的假阳性 // 而不是真 bug。它们由 theme.test.mjs 的档位断言覆盖(--s-* 两模式同值)。 const PROBES = [ // [类名组合, 说明, 期望最低对比度] ['bg-red-600 text-white', '危险实心按钮(确认归档 / 删除)', 3.0], ['bg-red-500 text-white', '未读徽标', 3.0], ['bg-green-600 text-white', '同意按钮', 3.0], ['bg-orange-700 text-white', '待决策徽标', 4.5], ['bg-blue-600 text-white', '主按钮', 4.5], ['bg-red-50 text-red-600', '错误提示条', 4.5], ['bg-red-50 text-red-700', '危险区块文字', 4.5], ['bg-green-50 text-green-700', '成功提示条', 4.5], ['bg-orange-100 text-orange-700', '橙色 chip', 4.5], ['bg-amber-100 text-amber-700', '警告 chip', 4.5], ['bg-amber-50 text-amber-800', '警告条', 4.5], ['bg-yellow-100 text-yellow-700', '黄色 chip', 4.5], ['bg-blue-50 text-blue-700', '信息条', 4.5], ['bg-red-100 text-red-700', '红色 chip', 4.5], ['bg-orange-200 text-orange-800', '深橙 chip', 4.5], ['bg-white text-gray-900', '卡片正文', 4.5], ['bg-white text-gray-500', '卡片次要文字', 4.5], ]; const { browser, page } = await openApp(WIDE); let fail = 0; for (const mode of ['light', 'dark']) { await page.evaluate(m => { document.documentElement.classList.toggle('dark', m === 'dark'); let host = document.getElementById('__probe'); if (host) host.remove(); host = document.createElement('div'); host.id = '__probe'; host.style.position = 'fixed'; host.style.top = '0'; host.style.left = '0'; host.style.zIndex = '99999'; document.body.appendChild(host); }, mode); console.log(`\n─── ${mode === 'dark' ? '深色' : '浅色'} ───`); for (const [cls, label, min] of PROBES) { const got = await page.evaluate(c => { const host = document.getElementById('__probe'); host.innerHTML = `测试`; const el = document.getElementById('__p'); const s = getComputedStyle(el); return { bg: s.backgroundColor, fg: s.color }; }, cls); // 透明背景 = 声明失效(正是上次那个 bug 的指纹) const transparent = /rgba\(0,\s*0,\s*0,\s*0\)|transparent/.test(got.bg); if (transparent) { console.log(` 失败 ${label} — 背景透明(${cls} 的 background-color 声明失效)`); fail++; continue; } const r = ratio(parse(got.fg), parse(got.bg)); const ok = r >= min; if (!ok) fail++; console.log(` ${ok ? '通过' : '失败'} ${label.padEnd(22)} ${r.toFixed(2)}:1 (需 ${min}) ${got.bg} / ${got.fg}`); } } await page.evaluate(() => document.getElementById('__probe')?.remove()); await browser.close(); console.log(fail ? `\n!! ${fail} 项不达标` : '\n全部达标'); process.exit(fail ? 1 : 0);