Lighthouse 不适合作为组件可访问性体检主工具,因其是报告型而非断言型工具:默认不中断CI构建、需额外配置assertions、规则粒度粗、错误定位模糊(如仅报“button缺少name”而无具体selector)、不支持细粒度规则开关(如仅启用wcag2aa)、资源开销大且难以与JSDOM/Playwright等测试环境对齐。

为什么不能只靠 Lighthouse 做组件可访问性体检
Lighthouse 在 CI 里默认不因可访问性问题失败,它生成报告但不抛错——你得手动翻页、人工判断、再找人修。组件库迭代快,等不到人看报告。lhci autorun 也不自动中断构建,除非你额外配 collect.assertions,但规则粒度粗、定位模糊,比如只报“button 元素缺少 name”,不告诉你具体是哪个 button[data-testid="close"]。
真正能卡住 PR 的,是断言型工具:跑完立刻告诉你是哪一行 HTML、哪个 selector 违反了哪条 WCAG 规则。
- axe-core 返回的
results.violations[0].nodes[0].target是 CSS 选择器路径(如["#modal-123 > .header button"]),直接对应源码位置 - 支持只开
"wcag2aa"规则集,避免误报“color-contrast”这类渲染依赖项 - 能在 JSDOM(Vitest)、Playwright page、真实浏览器里复用同一套校验逻辑
怎么在组件测试中注入 axe-core 并精准扫描
别在 <script> 标签里直接 axe.run()——组件还没挂载、DOM 是空的,document.querySelector() 拿不到任何东西,结果全是 []。
以 Playwright + React 组件为例,关键三步必须串起来:
立即学习“前端免费学习笔记(深入)”;
- 等 hydration 完成:
await page.waitForFunction(() => document.querySelector('#root')?.children.length > 0) - 注入 axe:
await page.addScriptTag({ path: require.resolve('axe-core') }) - 执行并断言:
const results = await page.evaluate(() => (window as any).axe.run()); expect(results.violations).toHaveLength(0)
如果组件用了动态字体或 lazy image,还得加 await page.waitForFunction(() => getComputedStyle(document.body).fontSize !== '0px'),否则 color-contrast 规则会因颜色未计算而误报。
如何让 axe-core 覆盖组件库的全生命周期场景
一个按钮组件,静态 HTML 没问题,但展开菜单后焦点没管理、禁用态仍可键盘聚焦、ARIA 状态没同步更新——这些都得在交互后扫。
实操要点:
- 每个交互动作后都重新 run:
await page.click('button[aria-expanded="false"]'); await page.waitForTimeout(100); const postClick = await page.evaluate(() => axe.run()) - 对模态框类组件,必须验证
aria-modal="true"+inert或aria-hidden="true"是否生效,否则屏幕阅读器会读背景内容 - 表单组件要测
label和input的for/id匹配、错误消息是否通过aria-describedby关联 - 图标按钮必须有
aria-label或包裹span[aria-hidden="true"]+span[role="img"],纯<svg>不会被读
CI 中 axe-core 扫描失真怎么办
本地绿、CI 红,90% 是环境差异导致 axe 看到的 DOM 和你预期不一样。最常踩的坑不是代码,是上下文。
三个关键点必须检查:
-
font-family加载延迟:CI 容器里没缓存字体,getComputedStyle(el).color可能返回'rgba(0, 0, 0, 0)',触发一堆color-contrast报错;加await page.waitForFunction(() => window.getComputedStyle(document.body).fontFamily.includes('Inter')) - JS 错误阻断渲染:CI 日志里搜
console.error,哪怕只是Cannot read property 'map' of undefined,也会让组件没挂载,axe 扫到空容器 - 服务端渲染(SSR)与客户端 hydration 不一致:比如 SSR 输出了
<div aria-live="polite"></div>,但 JS 初始化时把它删了又重建,axe 在 hydration 前扫就抓不到 live region
真正难的是那些无法被 axe 捕获的逻辑缺陷:焦点陷阱、动态内容无通知、键盘操作跳过必要步骤。这些必须靠手动走查+屏幕阅读器验证,自动化只能守住底线,不能替代人眼和耳朵。



















