1. 无障碍测试为什么值得投入自动化
先说一个我自己的观察。做了多年测试和质量保障,国内团队对"无障碍"这件事的态度,多数停留在"知道但不动"的状态。直到有几次我做海外项目,客户明确要求产品必须符合 WCAG 2.1 AA 标准,否则不予验收,团队才被迫正视这个问题。
无障碍测试,英文叫 Accessibility Testing,圈内常简写为 a11y,核心是验证产品对残障用户是否可用:视障用户能靠屏幕阅读器读页面吗?色盲用户能看清状态提示吗?只靠键盘能完成整个购物流程吗?
这类测试本质上是规则校验,WCAG 标准把问题拆成了非常明确的可检查项,比如文字和背景的对比度不低于 4.5:1、所有图片必须有 alt 文本、所有交互元素必须能通过键盘聚焦。这些规则一旦落到代码层面,就是可枚举、可断言、可自动化的校验点。
所以我的结论是:无障碍测试是整个测试体系里最适合交给自动化的类型之一。它不像 UI 视觉测试那样需要大量人工判断,也不像探索性测试那样依赖人的创造力,它就是一套明确规则下的扫描和验证。借助自动化工具做常态化扫描,能保证每次版本迭代都不会带上旧的合规缺口,也能在早期就拦截新增的无障碍问题。
这篇内容适合谁看?测试工程师、前端开发、质量团队负责人,以及所有需要应付海外合规验收或希望把产品体验真正做扎实的团队。我会从方案设计讲起,再到工具选型、完整实操,最后是真正的坑和排查经验,尽量让对方看完就能上手。
2. 整体设计方案:自动化在合规验证里的位置
2.1 合规性验证到底涵盖哪些维度
在搭自动化之前,先要把"无障碍合规"拆开看,不然容易做成只有对比度检查的假合规,看起来报告好看,实则没什么用。
按我实际项目的经验,无障碍合规通常落在三个维度:
- 视觉层:颜色对比度、非文本对比度、焦点指示的可见性、文字缩放后布局是否可用。
- 操作层:仅用键盘能否走完所有流程、Tab 焦点顺序是否符合阅读逻辑、焦点是否会被困在弹窗里。
- 语义层:按钮有名字吗?图片有替代文字吗?表单字段有没有正确的 label 关联?ARIA 属性使用是否正确?屏幕阅读器能正确朗读内容结构吗?
自动化能高效覆盖视觉层和语义层,因为这两类规则高度明确,工具可以直接计算对比度、检查 HTML 属性。操作层里的一部分也能做,比如焦点陷阱、Tab 顺序、点击区域大小,都是可脚本化的。
但有一个现实我必须强调:WCAG 的检查项里,真正能被自动化工具完全覆盖的比例通常在 30% 到 40% 之间。剩下的部分,比如屏幕阅读器的实际听感、语音输入用户的操作流畅度、复杂交互组件的可用性判断,目前仍需要人工介入。
所以方案设计的第一原则就是:不要企图用一套自动扫描替代所有合规验证,而是用自动化做"巡检防线",把规则明确的部分全部自动化,把人工精力集中在机器做不了的地方。这套思路和我们做功能测试时"UI 自动化 + 探索性测试"的打法完全一样。
2.2 用"扫描金字塔"思路设计测试层级
功能测试界有个著名的测试金字塔,无障碍测试可以借用同样思路,分层设计,从底层到顶层分别覆盖不同类型的检查。
我的分层方式是:
第一层:静态代码与依赖层。在代码层面检查,比如 lint 规则里开启 jsx-a11y 或类似插件,开发阶段就能发现 alt 缺失、ARIA 乱用这类问题。速度最快,成本最低,问题在提交前就被拦截。
第二层:组件级渲染扫描层。把每个独立组件或页面渲染出来,用工具做自动化扫描。这个层级主要跑批量检查,适合做全量回归,比如用 Lighthouse 的 accessibility 分数做页面级体检,或者用 axe-core 直接扫组件。
第三层:端到端流程层。在真实用户流程中注入扫描,比如在 Selenium 或 Playwright 脚本里完成"登录、添加购物车、结算"的完整操作后,在关键节点调用无障碍扫描器做检查。这一层能发现动态交互产生的无障障问题,比如弹窗打开后焦点是否被正确管理、数据加载后对比度是否变差。
第四层:人工辅助层。用屏幕阅读器(NVDA、VoiceOver)+ 键盘走查关键流程。这一层不做全量,只做核心路径和复杂组件的人工验证。
自动化的重点放在前三层。我见过一些团队只跑第三层,忽略组件级扫描,导致每个页面都要走完整流程才能发现基础问题,效率极低。正确的做法是三层并行:组件级扫描做全量覆盖,E2E 扫描做动态交互保障,两者互补。
3. 工具选型:哪些方案经得起实际项目检验
3.1 主流无障碍自动化工具横向对比
工具选型我踩过不少坑,这里直接给一份实际对比。目前圈内主流的方案有 axe-core、pa11y、Lighthouse、WAVE 这么几类,另外还有具备商业背书的工具如 Deque 的 axe DevTools,但核心引擎还是 axe。
axe-core 是目前事实上的标准引擎。它由 Deque 系统开发,谷歌、微软等大厂以及很多开源组件库(如 Angular Material、Adobe Spectrum)都在用。它的优势是规则丰富、误报率低、社区活跃,而且版本更新紧跟 WCAG 2.1/2.2。最重要的,它能以 npm 包形式嵌入任意测试框架,灵活性极高。
pa11y 是另一个受认可的选择,底层虽然也用了 axe-core 一部分规则,但它自带命令行和 HTML 报告能力,适合不想写复杂代码、只想要定期扫描的团队。它把"扫描、生成报告"做成了开箱即用,这在快速试点阶段很有价值。
Lighthouse 更适合做整体的性能和无障碍体检,它的 accessibility 分数非常直观,给管理层的汇报效果好,但它的规则深度不如 axe-core,定位是"体检仪"而非"合规检查器"。我做项目时通常用 Lighthouse 做宏观体检,用 axe 做合规门禁。
WAVE 是老牌工具,浏览器插件方式使用方便,适合教研或快速看到页面上问题的可视化标注,但要自动化跑批就比较麻烦,不适合接入 CI。
这里我想特别说明选型逻辑:这是一套工具链,不是单选题。我们最终落地时选了 axe-core 做核心引擎,配 Playwright 做流程注入,Lighthouse CI 做整体评分,三样搭配使用。
3.2 为什么把 axe-core 作为主引擎
我推荐所有团队优先考虑 axe-core,理由有三点。
第一,规则覆盖面最广且权威。axe-core 的规则来自 WCAG 2.1、2.2 以及 WAI-ARIA 的实践规范,并且它区分了规则等级:serious、critical、moderate、minor。它还会把违反的 WCAG 成功标准(比如 "4.1.2 Name, Role, Value")直接标注出来,这对生成合规性审计报告极为重要,因为在人行审查时需要追溯每一条问题对应的标准条款。
第二,多框架支持。axe-core 既可以独立跑(Node 环境),也可以嵌入 Selenium、Playwright、Cypress、WebdriverIO。这意味着只要团队现有的自动化框架不是特别偏门,都能直接接入,不需要额外维护一套独立测试平台。
第三,误报率相对低。我自己用下来的体感,对比度类检查在复杂渐变背景上偶尔会有误报,但大部分规则,如 ARIA 属性使用错误、按钮名称空、图片 alt 缺失,报出来就是板上钉钉的问题。误报少,团队才愿意把它接进门禁流程,否则三天两头被无效失败打断,自动化很快就会被废弃。
3.3 工具链之外的思路:AI 辅助能做什么
最近不少人聊 AI 自动化测试,无障碍测试领域 AI 也有切入空间,但要认清边界。现有的开源 AI 或视觉模型能做的事情主要有两类:一是利用计算机视觉识别页面元素,自动补充 OCR 文案校验,但这对无障碍的语义规则帮助不大;二是利用 LLM 分析报告文本,把 axe 的原始 JSON 结果自动翻译成给开发团队的自然语言修复建议,这一点在我们团队已经用起来了,把结果丢给大模型,它能直接给出修复代码建议,开发修复效率提升明显。
但要警惕一个误区:AI 目前还不能替代标准引擎做规则判定。真正的合规判定逻辑是确定性的,对比度是数学计算,ARIA 属性合法性是枚举校验,这些场景没必要用大模型,精确无误的工具引擎更可靠。AI 适合做辅助解释、建议生成、报告汇总这类自然语言工作,而不是替代确定性逻辑。
4. 实操全流程:从零搭建无障碍自动化合规测试
4.1 环境准备与基础依赖
这一节给出一套可以直接复制的完整流程。我以 Node.js + Playwright + @axe-core/playwright 为主路线演示,这是目前最稳的组合,再附上 Python + Selenium 的路线做参考。
前提条件:Node.js 16 以上版本,npm 可用;Python 路线需要 Python 3.8 以上和 Chrome 浏览器。
初始化项目并安装依赖:
mkdir accessibility-compliance cd accessibility-compliance npm init -y npm install --save-dev playwright @axe-core/playwright axe-core npx playwright install chromium如果走 Python 路线:
pip install selenium # 同时需要下载 axe-core,将 axe.min.js 放到项目目录 # 官方源:https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.7.2/axe.min.js安装过程中最容易出问题的点是 npx playwright install chromium 需要联网下载浏览器内核,在部分内网环境会失败,需要配置镜像或手动指定浏览器路径。这个细节很多第一次用 Playwright 的同事会卡住,提前知道可以省时间。
4.2 编写端到端扫描脚本:语义合规检查
先演示 Playwright 的用法,写一个最简单的页面扫描脚本:
import { chromium } from 'playwright'; import AxeBuilder from '@axe-core/playwright'; (async () => { const browser = await chromium.launch(); const page = await browser.newPage(); await page.goto('https://your-site.com/login'); const results = await new AxeBuilder({ page }).analyze(); console.log(`违规数量: ${results.violations.length}`); results.violations.forEach(violation => { console.log(`[${violation.impact}] ${violation.id} - ${violation.description}`); violation.nodes.forEach(node => { console.log(` -> ${node.target.join(' ')}`); console.log(` ${node.failureSummary}`); }); }); await browser.close(); })();这段脚本做的事是:打开页面,注入 axe 扫描引擎,把页面上所有违反无障碍规范的内容打印出来。对初学者来说,这个脚本就是最小可用原型。你先跑通这个,再谈集成 CI。
Python + Selenium 路线,核心是通过 execute_script 注入 axe.min.js,再执行扫描:
from selenium import webdriver import json driver = webdriver.Chrome() driver.get("https://your-site.com/login") with open("axe.min.js", "r", encoding="utf-8") as f: axe_script = f.read() driver.execute_script(axe_script) result = driver.execute_script("return axe.run().then(r => r);") violations = json.loads(json.dumps(result["violations"])) print(f"违规数量: {len(violations)}") for violation in violations: print(f"[{violation['impact']}] {violation['id']}") for node in violation["nodes"]: print(f" -> {node['target']}") driver.quit()Selenium 注入方式的原理是:axe.run() 在浏览器上下文里执行 DOM 扫描,所有规则判断都在前端完成,不需要后端服务,所以只要能在页面里跑 JS,就能用 axe。这意味着你现有测试框架如果已经基于 Selenium,根本不需要推倒重来,加一个注入步骤就行。
4.3 在真实用户流程中做动态交互检查
静态页面扫描只是基础。实际业务系统里,很多无障碍问题是在交互发生后暴露的。比如点击按钮弹出 Dialog 后,焦点没有移入弹窗;表单校验失败的错误提示没有通过 aria-live 广播给屏幕阅读器;异步加载内容后新元素的对比度不足。
所以在 E2E 流程里嵌入扫描是必经之路。下面是用 Playwright 在登录流程后做合规检查的示例:
import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; test('登录流程无严重无障碍违规', async ({ page }) => { await page.goto('/login'); await page.getByLabel('用户名').fill('tester'); await page.getByLabel('密码').fill('password123'); await page.getByRole('button', { name: '登录' }).click(); await page.waitForURL('/dashboard'); const results = await new AxeBuilder({ page }) .withRules(['color-contrast', 'aria-valid-attr', 'button-name']) .analyze(); const seriousViolations = results.violations.filter( v => v.impact === 'serious' || v.impact === 'critical' ); expect(seriousViolations).toEqual([]); });这段代码里有个关键选择:用 withRules 限定只检查 color-contrast、aria-valid-attr、button-name 三类高频规则。为什么强调这个?
因为全量规则的扫描在复杂页面上可能耗时数秒,而且大量规则全部开启会导致一次性暴露几十个问题,团队根本无从下手。实际项目里的做法是分层推进:第一轮只设"必须通过"的核心规则(严重等级过滤),其余规则作为报告参考;等团队把严重问题清零后,再逐步加严。这和控制功能测试范围的思路完全一致,先保证核心场景,再逐步扩大覆盖。
4.4 定义合规门禁:怎样的结果算通过
扫描出违规不等于要全部修复,要根据影响等级和业务场景设定容忍度。这里透露我常用的标准:
- critical 和 serious 等级的违规,设定为门禁必须为零,否则 CI 失败。
- moderate 和 minor 等级的违规,允许存在但要在报告里记录,并排期修复。
- 对海外合规验收项目,最终目标是对照 WCAG AA 标准,所有违反项为零。
对应到 Playwright 断言里就是 expect 只过滤 critical 和 serious:
const criticalOrSerious = results.violations.filter( v => v.impact === 'critical' || v.impact === 'serious' ); expect(criticalOrSerious).toEqual([]);如果要更精细,可以在 include 里直接指定 WCAG 级别:
const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) .analyze();withTags 这个方法非常实用,它只保留对应 WCAG 版本和级别的规则。比如客户只要求符合 WCAG 2.1 AA,那就指定 wcag21aa,不要让它报一堆符合 AAA 的问题,不然开发团队会一直处理无关任务,最后把整个检查流程玩坏。
4.5 接入 CI:每次提交自动跑合规扫描
自动化测试不接入 CI,价值折损一半。合规性测试尤其如此,因为它的价值在于持续回归,而不是偶尔手动跑一次。
给出一个 GitLab CI 的最小示例:
accessibility-test: stage: test image: mcr.microsoft.com/playwright:v1.40.0-focal script: - npm ci - npx playwright install --with-deps chromium - npm run test:accessibility artifacts: when: always paths: - test-results/ expire_in: 14 days核心逻辑并不复杂:拉取包含 Playwright 的镜像,安装依赖,跑测试脚本,然后把报告作为构建产物保留下来。如果门禁失败,MR 就无法合并;如果门禁通过,报告也能让任何人在流水线记录里回溯。
这里有个实践教训:全量页面扫描全部串行跑的话,一个中型系统的耗时可能超过十分钟。建议做三件事:一是用 Playwright 的 shard 功能做并行分片;二是只对核心页面做全量规则扫描,非核心页面做抽样;三是在 nightly 任务里跑全量,merge 前的校验跑核心路径即可。这样既保证反馈速度,又确保覆盖深度。
5. 报告生成与合规审计追溯
5.1 从 JSON 到可读报告
axe 扫描返回的是 JSON 结构,里面每个 violation 包含 id、impact、description、helpUrl、nodes 数组以及 nodes 里的 target 和 failureSummary。原始数据信息很全,但不直观,给开发看还好,给项目干系人看就会一头雾水。
我们团队的做法有三类输出:控制台精简输出(开发自测时用)、HTML 报告(附到 MR 里给团队看)、审计归档报告(给客户和合规验收用)。
Playwright 生态里可以直接把结果渲染成 HTML。简单实现方式是把 JSON 结果转存后用模板渲染,我提供一个最小实现思路:
import { writeFileSync } from 'fs'; const htmlReport = ` <!DOCTYPE html> <html> <head><title>Accessibility Report</title></head> <body> <h1>无障碍合规扫描报告</h1> <p>违规总数: ${results.violations.length}</p> ${results.violations.map(v => ` <div style="border:1px solid #ddd; padding:12px; margin:8px 0;"> <h3>[${v.impact}] ${v.id}</h3> <p>${v.description}</p> <a href="${v.helpUrl}" target="_blank">规则说明</a> <ul> ${v.nodes.map(n => `<li>${n.target.join(' > ')}</li>`).join('')} </ul> </div> `).join('')} </body> </html>`; writeFileSync('test-results/accessibility-report.html', htmlReport);如果想省事,也可以用 pa11y 的 HTML 报告作为兜底。但我们实际用的时候发现 pa11y 报告对动态流程支持不如 Playwright 灵活,所以最终还是自渲染了简单模板。要花心思的是报告中给每个问题添加上下文信息,比如是哪个页面、哪个操作步骤后发现的,这样开发才能在定位时少走弯路。
5.2 用基线模式管理存量问题
存量系统接入无障碍合规检查时,第一次扫描往往会冒出一大堆历史违规。此时如果把门禁直接设为"零违规",团队大概率会造反,因为马上要干大量脏活。
推荐基线模式:第一次全量扫描后,把当前所有违规明细存成基线文件;后续 CI 里只对"新增违规"做门禁拦截。这样存量问题被显性记录但不在合并流程里强制阻断,团队可以排计划逐步消化。
基线对比逻辑不复杂:
const baseline = JSON.parse(readFileSync('accessibility-baseline.json', 'utf-8')); const baselineKeys = new Set( baseline.violations.flatMap(v => v.nodes.map(n => `${v.id}::${n.target.join(' ')}`) ) ); const newViolations = results.violations.filter(v => v.nodes.some(n => !baselineKeys.has(`${v.id}::${n.target.join(' ')}`)) ); expect(newViolations).toEqual([]);这种做法在测试领域叫做 "allowlist" 或 "baseline"。它的好处是让团队立刻获得可持续的 CI 门禁,而不是把所有问题都推给一次技术债清理。新功能引入问题会被拦截,存量问题也不会被遗忘,因为基线文件会在每次扫描时同步更新,违规数只能降不能升。
5.3 自动化结果如何驱动团队修复
报告有了,怎么让团队真的去修?我总结了一个行之有效的组合拳。
首先,在失败信息里写清楚规则的含义和修复代码示例。axe 返回的 helpUrl 直接指向官方文档,测试失败信息里要带上它,并尽量附加节点定位信息。开发打开失败日志就能定位页面元素,而不是反手把测试跳过。
其次,把合规修复纳入迭代排期。每周选 5 到 10 个违规项集中修,修完标记基线。我见过最有效的节奏是:每两周一次的自动化回归扫描 + 每次迭代明确"无障碍技术债消化"任务,三个月基本能把中等规模系统的严重问题清完。
最后,用趋势数据说话。把每次扫描的 critical/serious 数量画成趋势图,管理层看到数字在降,就会持续投入资源。这一点对争取团队时间非常有用,权重很高,别只在开发内部打转,要把合规性进展变成业务语言向上汇报。
6. 常见问题与排查技巧实录
6.1 动态内容和 SPA 页面导致扫描结果不一致
SPA 场景里,页面标题不变但 DOM 已经换过好几轮,扫描结果容易和期望不符。
处理技巧:在扫描前等待页面稳定,最简单的方式是等待某个标志性元素出现或网络请求完成。Playwright 的自动等待已经比较智能,但针对接口返回后渲染的模块,建议显式等待:
await page.waitForResponse(resp => resp.url().includes('/api/user/info') && resp.status() === 200 ); await page.waitForLoadState('networkidle');还有一种情况是弹窗、抽屉这类临时容器。如果要对弹窗单独扫描,直接用 Playwright 的 locator 定位弹窗根节点,再用 axe 的 include 参数指定范围:
const modal = page.locator('[role="dialog"]'); const results = await new AxeBuilder({ page }) .include(modal) .analyze();include 的本质是把扫描范围限定到指定元素及其子树。很多人不知道这个能力,导致要扫某个特定组件时总是连带扫出全页面的问题,噪音很大,用了 include 之后结果干净非常多。
6.2 Shadow DOM 和 iframe 里的问题扫不到
现代前端框架大量使用 Shadow DOM,sometimes iframe 嵌套也很深。axe-core 对常规 Shadow DOM 是支持的,但默认配置在 iframe 场景下不一定完整。
解决办法是开启 iframe 选项:
const results = await new AxeBuilder({ page }) .withOptions({ iframes: true }) .analyze();如果是跨域 iframe,scan 会受同源策略限制扫不进去。此时只能单独去访问那个 iframe 页面做独立扫描,或者在设置里加入跨域处理逻辑。
这里有个坑要提醒:axe 对 Shadow DOM 的支持限定了 open 模式,如果是 closed 模式(比如某些组件库内部封装),工具扫不到。遇到这种情况,排查思路不是硬刚扫描器,而是回到组件库层面做单测,或者要求组件库保留可访问性接口。这是工具边界的真实体现,项目落地前要和团队明确这些边界,不要等到上线前才发现有盲区。
6.3 对比度和焦点管理的自动化盲区
颜色对比度检查看起来简单,实际复杂场景很多。渐变背景、半透明遮罩、背景图片混排,都会导致 axe 计算与实际视觉呈现不一致。比如一个按钮在渐变背景中部,axe 取到的只是背景色采样,可能算出 4.6:1,但视觉上按钮下方背景恰好是低对比度区域,实际感知对比度并不过关。
对此我的补充建议是:自动化跑基础对比度检查,对高风险区域(如电商的价格标签、表单错误提示)单独做人工视觉验证,也可以引入 playwright 的视觉回归截图对比。重点区域宁可多花人工时间,也不要指望全自动工具能覆盖所有视觉场景。
焦点管理是另一个自动化覆盖不全的地方。axe 能判断某个元素是否可聚焦,但"按 Tab 后焦点是否按设计顺序移动"这类行为,需要手动模拟按键操作:
await page.keyboard.press('Tab'); await expect(page.locator(':focus')).toHaveAttribute('id', 'expected-element');这是一个很朴实但有效的焦点顺序验证方式。它确实需要在用例里写死预期的 Tab 顺序,维护成本偏高,所以我们通常只对核心用户路径做焦点顺序断言,其他路径交给人工抽查。
6.4 不要忽视真实屏幕阅读器验证
这是我最想强调的经验。无论自动化工具覆盖得多全面,都无法替代真实的屏幕阅读器体验。我在多个项目里发现,某些和实际产品体验相关的问题,自动化工具完全发现不了:比如一个按钮通过 ARIA 设置名称后,工具认为有可访问名,但 NVDA 实际朗读时的语境和语义组合不佳,用户听着很困惑。
建议每个迭代至少做一次屏幕阅读器人工走查,固定 3 到 5 个核心任务,比如"在首页完成一次搜索并读取结果列表"、"填写一个三字段表单并提交"、"弹出 Dialog 并成功关闭"。Windows 上推荐 NVDA,macOS 上直接用 VoiceOver。走查时要求测试人员记录实际听到的语音反馈,和预期文案做对照。
自动化合规测试 + 屏幕阅读器人工走查,两者结合,才是完整的无障碍质量保障体系。缺了任何一半,都有可能在关键验收点翻车。
7. 关于落地节奏的最后经验
回到具体项目的落地路径,我建议按四个阶段推进。
第一个阶段是摸底。选 10 个核心页面,用 axe 全量扫描一次,把违规数量、类型、分布统计出来。这个过程通常一天就能完成,目的是让所有人知道现状有多差、问题集中在哪里。
第二个阶段是架流水线。把核心页面的扫描脚本集成到 CI,设定基线模式,先做到"新增违规不流入",同时把报告自动发到团队群。这个阶段一到两周可以完成,团队要的是持续提醒而不是一次惊吓。
第三个阶段是清存量。按严重程度排序,每轮迭代消化 5 到 10 个问题。修完就更新基线,让数字往下降,瑕疵越来越少。这个阶段通常是两到三个月,取决于存量问题的多少和团队投入。
第四个阶段是扩覆盖。从核心页面扩展到所有业务页面,从纯扫描扩展到 E2E 流程注入扫描,从 UI 自动化扩展到包含触发性交互的复合扫描。Form 完流程之后,再加组件库层面的静态检查,形成从开发到验收的闭环。
我个人的体会是,无障碍自动化测试的落地难点从来不在技术本身。扫一下页面、写几个断言,这个门槛很低。真正难的是让团队相信这件事值得做,并且愿意把修无障碍问题当作正常开发任务来排期。合规验收的压力、真实用户反馈的推动、领导层的理解,都可能成为启动的契机。作为质量从业者,我们能做的就是把这个领域的专业方案准备好,随时可以把合规性测试体系搭起来。