无障碍自动化测试实战:基于axe-core的WCAG合规性扫描与CI集成
2026/9/8 6:47:13 网站建设 项目流程

1. 无障碍合规性到底在测什么:先把游戏规则搞清楚

先说个我自己的经历。之前接了一个改造项目,客户官网明明已经做了好几轮“无障碍优化”,结果拿去走合规审计的时候,自动化扫描一跑,色块对比度大面积飘红,一堆图片缺alt文本,表单控件连label都没绑。客户当时非常不理解:“我们设计稿里颜色挺好看的啊,怎么就合规不通过了呢?”

这个场景在国内特别典型。大家一听“无障碍自动化测试”,第一反应是“这不就是给视障用户用的吗”,然后下意识觉得“等产品做得差不多了再补”。但在真正落地之后你会发现,无障碍合规性根本不是“做好事”,它是一套有明确验收标准的工程规范。标准就是 W3C 推出的 WCAG(Web Content Accessibility Guidelines,网页内容无障碍指南),目前全球主流的合规审计基本都锚定在WCAG 2.1 AA 级别

为什么是 AA 而不是 AAA?因为 AAA 级别很多要求在实际产品中几乎不可能完全达到,比如“所有视频都要有完整手语翻译”“所有文本的对比度要达到 7:1”,这在商业项目里成本太高。而 AA 级别覆盖了绝大多数常见障碍场景,也是各国法律法规引用得最多的标准。你做无障碍自动化测试,说到底就是把 WCAG 2.1 AA 里那些可以用代码自动判断的规则变成一条条可执行的测试断言,塞进现有的自动化测试体系里。

先把这个观念纠正过来:无障碍测试不是辅助功能测试,它是合规性测试。你的代码不满足无障碍规则,就像页面有一个功能性 bug 一样,是质量缺陷。理解了这一点,后续整套技术方案的设计逻辑才顺得下去。

WCAG 2.1 总共有 78 个成功标准(Success Criteria),但别被这个数字吓到。真正能通过自动化工具检查的,大概只有 30%-40% 左右,其余的必须靠人工测试。所以自动化测试的目标定位要清晰:它能帮你挡掉大量低级的、重复的、肉眼容易漏掉的问题,但永远不可能替代完整的人工审计。这个边界从一开始就要跟团队说清楚,否则后面一定会有“既然自动化都过了,为什么人工审计还报了问题”的扯皮。

领域内那种“上了个无障碍扫描工具就宣称自己合规”的团队我见过太多,落地结果基本都是自欺欺人。接下来这篇,我就以axe-core这个事实标准工具为主线,把从标准到工具、从搭建到接入 CI 的完整路径拆开讲清楚。

2. 自动化工具选型:为什么是 axe-core 而不是其他方案

无障碍自动化测试的工具其实不少,常见的有axe-core、Google 的Lighthouse(内置了部分无障碍审计)、pa11yWAVETenon等等。我直接把结论放在前面:如果你要在 CI/CD 管道里做无障碍合规性检查,首选axe-core,没有之一。

下面这张表是我在实际落地过程中对比过的几个方案,重点看适用场景和接入成本:

工具规则覆盖误报率CI 集成难度适用场景
axe-core规则最全,紧跟 WCAG 2.x极低低,支持多种测试框架主推方案,适合几乎所有 Web 项目
Lighthouse基础规则较高中,适合性能审计顺带扫一眼性能优化时顺带看一眼无障碍,不建议作为合规依据
pa11y基于 axe 核心,自带 HTML 报告小项目快速出报告,但可定制性差
WAVE浏览器插件为主较高差,基本靠人工点击给不了解无障碍的人做个初步体验
TenonAPI 接口方式平台化改造成本高企业级平台集成,但收费且规则闭源

看到这里你应该能发现,axe-core 的核心优势在于规则引擎是最完整且社区维护最活跃的。Deque Systems 这个团队本身就是 WCAG 标准制定的深度参与者,所以 axe-core 的很多规则不仅检测“有没有”,还会区分“严重级别”:

  • critical(严重):会影响用户正常操作,比如表单没有关联 label、按钮没有可访问名称
  • serious(较重):影响信息获取,比如图片缺少替代文本、iframe 缺少 title
  • moderate(中等):影响部分用户的体验,比如某些 ARIA 属性使用不当
  • minor(轻微):比如某些 HTML 语义标签嵌套不规范

这个严重级别很关键,它是你后续决定“哪些违规必须阻断发布、哪些可以暂存为技术债”的依据。

还有一个在选型时容易忽略的细节:axe-core 是纯 JavaScript 库,可以在浏览器、Node.js、Selenium、Playwright、Cypress 等任意环境下跑。这种“一次引入,处处可跑”的特性,决定了它能很自然地嵌到现有测试体系里,而不是成为一套必须单独维护的旁路工具。

对比一下pa11y你就会发现问题。pa11y 虽然也是基于 axe-core 的,但它封装了一层独立的命令行工具和报告生成逻辑。你要是用 pa11y,一般就是独立于主测试框架跑一遍拿到报告,但很难跟你的已有用例断言做联动。比如你想“登录后打开设置页再跑无障碍检查”,pa11y 做起来就比较别扭,而用 axe-core 直接在 Selenium 脚本里注入,你想在哪一步检查就在哪一步检查,灵活性完全不是一个量级。

这里补充一点,大家常问的“用 Lighthouse 跑出来的无障碍分数 90+,是不是就合规了”。答案是:不可信。Lighthouse 的审计规则是简化过的,而且很多规则因为实现方式问题,识别不了动态渲染的内容。它的分数只能作为大致参考,不能作为合规验收凭据。在我接手过的好几个项目里,Lighthouse 跑 95 分的页面,用 axe-core 扫还能查出七八个 serious 级别的问题。这差距不是一点点。

3. 从零搭建一套无障碍自动扫描工具的完整流程

接下来是实操环节。我以 Node.js + Selenium WebDriver 这套最通用的技术栈为例,给你演示从项目初始化到产出扫描报告的全过程。这套流程我基本上在不下五个项目里原样复用过,稳定可靠。

3.1 初始化项目并安装依赖

mkdir a11y-automation cd a11y-automation npm init -y npm install --save-dev @axe-core/webdriverio selenium-webdriver chromedriver

这里跟你解释一下为什么用@axe-core/webdriverio而不是直接用axe-coreaxe-core本身是个注入到页面里的脚本,你需要自己在 WebDriver 的executeScript里把它塞进去再调用,这中间要处理脚本注入时机、异步执行、结果格式化等等问题。而 Deque 官方针对 WebDriver 提供了封装包,你只需要传一个已创建好的 driver 实例,剩下的事情库帮你处理。

国内网络环境下安装依赖时有个小坑:chromedriver版本必须跟本机 Chrome 版本匹配,否则启动浏览器的时候直接报错session not created。我用过的稳定办法是安装chromedriver的 npm 包后,用npx chromedriver --version确认版本,跟 chrome://version 里的版本号比对一下就能排查是不是这个原因。

3.2 编写基础扫描脚本:先跑通再说

创建一个scan.js,把最基础的无障碍扫描脚本跑通:

const { AxeBuilder } = require('@axe-core/webdriverio'); const { Builder } = require('selenium-webdriver'); async function runAccessibilityScan() { // 创建 WebDriver 实例 const driver = new Builder() .forBrowser('chrome') .build(); try { // 打开目标页面 await driver.get('https://example.com'); // 注入 axe-core 并执行扫描 const builder = new AxeBuilder(driver); const results = await builder.analyze(); // 只输出 violation(违规项) console.log('发现违规数:', results.violations.length); // 按严重级别分组输出 const grouped = results.violations.reduce((acc, v) => { acc[v.impact] = acc[v.impact] || []; acc[v.impact].push(v); return acc; }, {}); console.log('严重级别分布:', Object.keys(grouped).map(k => `${k}: ${grouped[k].length}`)); } finally { // 不管成功失败,都要关掉浏览器 await driver.quit(); } } runAccessibilityScan().catch(err => { console.error('执行失败:', err); process.exit(1); });

跑一下node scan.js,如果环境没问题,你会在控制台看到类似“发现违规数:2”这样的输出。这里有个容易踩的坑:axe-core默认只扫描静态 DOM里当前可见的内容。如果页面里有很多元素是通过display: none隐藏的、或者懒加载还没渲染出来的,它都可能扫不到。通常第一次跑完没事儿,不代表这个页面真的干净。

3.3 看懂 violation 结构:合规性报告的重要前提

results.violations数组里每一个对象的字段都是有讲究的,你后续做报告、做拦截规则都得依赖这些字段。我挑几个核心的字段解释一下:

字段说明实际用途
id规则唯一标识,如color-contrast用来跟 WCAG 规则映射
impact严重级别:critical/serious/moderate/minor决定阻断策略
description问题的文字说明生成报告内容
tags规则标签数组,如wcag2awcag21aa按合规级别过滤
nodes受影响的具体 DOM 节点数组定位到具体代码位置

tags这个字段容易被忽略,但它非常有用。比如你的项目只需要满足 WCAG 2.1 AA 级别,那你可以这样过滤:

const wcagAaViolations = results.violations.filter(v => v.tags.some(tag => tag === 'wcag2a' || tag === 'wcag21a' || tag === 'wcag2aa' || tag === 'wcag21aa') );

为什么要手动过滤而不是直接analyze()?因为axe-core默认会把你当前没有开启的实验性规则也跑一遍,其中偶尔会包含一些不属于 WCAG 2.1 AA 范围的规则(比如未来标准的草案规则)。做合规性验收时,你要的是跟标准严格对齐的结果,多出来的“建议类”问题会打乱优先级。所以我一般在正式流程里都会加这个过滤。

3.4 引入测试断言:让扫描结果变成可执行的合格标准

光打印报告还不够,自动化测试的核心是“对结果做断言”。用 Node.js 自带的assert模块就能实现:

const assert = require('assert'); // 只保留 AA 级别相关的严重违规 const severeViolations = results.violations.filter(v => v.impact === 'critical' || v.impact === 'serious' ); // 断言:不应存在严重级别违规 assert.strictEqual( severeViolations.length, 0, `存在 ${severeViolations.length} 个无障碍严重违规,请修复后再提交。详情:\n` + severeViolations.map(v => `- [${v.impact}] ${v.id}: ${v.help}`).join('\n') );

这一步做完,你的“无障碍自动化测试”才真正有了“测试”的意义——不合格就报错,CI 流程就能拦住代码合入。

4. 接入 CI/CD 的关键决策:哪些违规必须拦截、哪些先放行

很多团队落地无障碍自动化测试死在哪一步?不是工具跑不起来,而是一接入 CI 就全面飘红,然后大家为了赶版本把测试禁用了。这种“一刀切”的策略设计从一开始就是错误的。

4.1 分级拦截策略:先堵住灾难性问题

我建议按下面的策略来配置你的 CI 拦截规则:

严重级别CI 是否阻断处理建议
critical必须阻断这类问题直接阻止用户操作或信息获取,属于严重功能缺陷
serious必须阻断会导致部分用户完全无法获取信息,应视为发布级缺陷
moderate不阻断但告警记录到报告里,计入技术债
minor不阻断但记录作为后续优化项

实际执行时,我一般把拦截阈值放在serious及以上,同时在告警阶段就把moderateminor加进一个单独的“无障碍待办清单”里。这样既保证了关键问题不被放过,又不会因为鸡毛蒜皮的小问题卡死发布流程,团队成员才愿意持续配合维护。

4.2 增量扫描思路:别让存量问题挡住新代码

还有一套更聪明的做法,特别适用于改造中的老项目。假设你的项目现在有一堆历史违规,一次性修完根本不现实。这时候你可以用“增量约束”策略:

  1. 先跑一次全量扫描,把当前所有违规项导出为一个baseline.json
  2. 之后的每次 CI 扫描,跟 baseline 对比,只拦截新增加的违规项
  3. 每周或每个迭代排期修复一批 baseline 里的存量问题
# 生成基线 node scan.js --export baseline.json # 后续 CI 中对比增量 node scan.js --compare baseline.json

这个思路跟代码质量平台的“增量检查”一模一样的逻辑。它最大的价值是让无障碍整改能够循序渐进地推进,而不是“要么全改,要么全不改”的极端局面。我在一个用户量很大的老后台系统上就是这么落地的,从最初一千多个违规到现在长期归零,整整花了三个月,但整个过程团队没有一个人想过放弃,因为每天的增量数字都是可控的。

4.3 与已有测试框架的融合:Selenium、Playwright、Cypress 怎么选

最后的集成形态,取决于你项目里已有的自动化测试框架是什么。我给出三种组合方式供你参考:

  • Selenium WebDriver:使用@axe-core/webdriverio,在上面的 3.2 节里已经演示过,适合已有 Selenium 用例体系、需要复用登录状态的项目。
  • Playwright:使用@axe-core/playwright,写法更现代,支持page对象直接调用,速度和稳定性都更优。新项目我个人更推荐这条路。
  • Cypress:官方推荐cypress-axe插件,用法是cy.injectAxeFromCdnn()然后cy.checkA11y(),上手最快,但社区维护节奏依赖 Cypress 自身的更新频率。

其实选哪个不关键,关键是你一定要把无障碍检查挂到真实的端到端场景里,而不是只扫描静态的首页。很多无障碍问题是动态交互触发的——点了展开菜单之后焦点没有跟随、打开弹窗后背景没有正确屏蔽、日期选择器没有键盘可达性——这些场景不进入实际交互流程,扫描是发现不了的。

5. 自动化的边界:哪些规则测不出来、哪些结果要人工复核

讲完怎么搭,得泼一盆冷水:axe-core 再强,也有很大的测不到的地带。如果你不清楚这些边界,很容易拿着自动化报告去跟领导汇报“我们已经合规了”,然后被人工审计一纸报告打回原形,非常尴尬。

5.1 自动化一定能测好的领域:这些可以放心交给工具

下面这些规则类目是 axe-core 的强项,漏检率很低,你可以在合规验收时放心依赖:

  • 文本颜色对比度(color-contrast):这里是能精准计算颜色的相对亮度并对比 WCAG 的 4.5:1(普通文本)和 3:1(大号文本)标准。
  • 图片替代文本(image-alt):检测<img>标签是否有alt属性,以及alt是否为空字符串(当图片纯粹装饰时可以接受)。
  • 表单标签关联(label):检测每个输入控件是否有对应的<label>aria-labelaria-labelledby
  • 按钮和链接的可访问名称(link-name, button-name):确保可交互元素有语义化名称,而不是靠图标糊弄屏幕阅读器。
  • 重复 ID(duplicate-id):同一页面里多个相同 ID 会导致无障碍贴标失效,这个规则能精准命中。
  • ARIA 属性使用规范(aria-系列)*:比如aria-hidden用在可聚焦元素上、ARIA 值超出枚举范围等等。

5.2 自动化测不了的领域:必须编排人工测试的清单

反过来,下面这些 WCAG 成功标准是 axe-core无法判断的,你必须编排人工测试来覆盖:

WCAG 成功标准为什么自动化测不了人工验证方法
1.2.2 字幕(视频)工具看不到视频内容抽查视频是否有准确字幕
1.3.3 感官特征提示“点击红色按钮”这类依赖颜色的指令,工具不理解语义人工阅读文案,确认不以形状/颜色/位置为唯一区分条件
2.4.7 焦点可见性工具能看到元素,但无法判断焦点的视觉样式是否醒目键盘 Tab 全流程操作一遍,肉眼观察焦点框
3.1.2 部分语言多语言混排页面需要标注lang变化,工具无法判断语义人工核对页面上其他语言的部分是否有lang标记
4.1.3 状态消息表单提交成功/失败的提示,screen reader 是否能感知打开屏幕阅读器实操一次完整流程

每次项目发布前,我建议至少抽出半天做一个“键盘+读屏走查”:只靠 Tab、Shift+Tab、Enter、空格键走完核心用户路径,再用一次 NVDA(Windows 免费屏幕阅读器)或者 VoiceOver(macOS 自带)把页面从头到尾听一遍。这个环节自动化测试替代不了,但恰恰是合规性审计专家最看重的内容,因为无合规审查报告最终一定要有人工测试记录做支撑

5.3 关于误报的处理:color-contrast 的坑我帮你踩过了

所有用 axe-core 的团队都会遇到一个常见误报场景——颜色对比度规则误判。为什么会误判?因为axe-core计算颜色对比度时,默认把文本背景当成纯色来处理。如果你的背景是渐变、是半透明叠加、是复杂图片,工具拿到的背景颜色值跟用户实际看到的效果就会不一致,对比度计算结果自然不准。

这种情况正确的处理方式是用axe.configure()把问题节点加入忽略清单,但必须在注释和报告里写明原因:

const builder = new AxeBuilder(driver) .withRules(['color-contrast']) .exclude('.hero-banner-text'); // 渐变背景,需人工验证

exclude不是让你用来规避问题的。它是让工具把“它测不准的地方”让给人去测。做好映射记录,未来人工审计的时候可以直接照着这个清单去复核,反而能提高整个合规流程的效率。

6. 真实整改案例:一个后台系统的从千级违规到归零

最后分享一个我在实际项目中完整走下来的案例,希望能帮你把前面所有内容串起来。

那是一个典型的后台管理系统,技术栈是 React + Ant Design,页面四十多个。第一次跑全量扫描,结果触目惊心:critical 12 个、serious 78 个、moderate 156 个、minor 341 个。这里面最典型的问题集中在几个类型:

  1. 表格操作列都是图标按钮,没有文字标签。屏幕阅读器用户完全不知道那三个图标是“编辑、删除、查看”。
  2. 自定义单选样式时隐藏了原生的<input type="radio">,但没有做可访问的名称关联
  3. Modal 弹窗打开时,背景页面的元素还在 Tab 焦点序列里
  4. 日期选择器没有任何键盘操作说明,按方向键没有反馈
  5. 提示信息完全依赖颜色区分成功/失败,红绿色盲用户根本看不出来。

我们当时定的整改策略是“三步走”:

第一步,先把 critical 和 serious 问题全部修完,也就是把表单控件缺失的标签补上、把图标按钮加上aria-label、把 Modal 的焦点圈禁做对。这花了大概两周。

第二步,针对 moderate 的问题开技术债清单,按页面模块分给对应的研发同学,每人在自己的迭代里带一两个走。这个阶段不能急,急了又会把质量打回原形。

第三步,启动增量扫描机制,把基线固定下来,任何新增严重问题在 CI 直接拦截。同时每个迭代安排一次人工键盘走查,覆盖主要用户路径。

整个周期三个月,最后的成果是:全量扫描零违规,人工走查除了几个文案语义优化建议,没有发现阻断问题。但比结果更重要的是团队认知的变化——一开始很多研发觉得“无障碍是给少数人服务,优先级不高”,到后来大家主动在提测单里写“已自查无障碍”,这中间的转变完全靠流程倒逼出来的。

我个人在做这个项目的过程中最深的一个体会是:无障碍自动化测试的本质不是“加了一个测试工具”,而是把一套外部标准翻译成了开发团队日常就能触碰到的质量红线。工具只是载体,真正起作用的是规则分级、增量约束、人工补位这三板斧的组合。你在自己团队落地时,只要把这三件事做扎实了,不管底层用 axe-core 还是别的什么,结果都不会差到哪里去。

最后再分享一个小技巧。很多团队一开始踌躇满志要把所有项目都接入无障碍自动化,我建议千万别这么干。先找一两个核心业务页面跑通全流程,让大家看到“一个严重违规被 CI 拦下来”的实感,再把范围逐步铺开。这比直接全网上线、然后被乱枪打死要稳妥太多了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询