
很多团队被“无障碍合规”四个字整得头大不是没工具是不知道从哪下手。我见过不少项目上线半年被告知产品不满足无障碍条款法务一封邮件下来整个前端和测试组集体加班手动过一遍几十个页面改完都不知道自己改没改对。这套路我熟这几年我在多个产品的质量保障里陆续接入了无障碍自动化测试把合规性检查变成流水线里的常驻环节实测下来比临时抱佛脚手点屏幕靠谱得多。这篇就把我的实施思路、工具选型、具体脚本和踩坑记录都摊开讲想给你的产品套上无障碍合规“安全网”的测试开发、前端和QA负责人可以直接照着落地。1. 先别急着写脚本看清“合规”到底在测什么1.1 无障碍合规不是“按钮有没有文案”这么简单很多第一次接触无障碍测试的人上来就问“是不是检查下图片有没有alt、按钮能不能Tab到”。方向对但格局小了。无障碍合规的核心依据现在业内基本都对齐到WCAGWeb Content Accessibility Guidelines这套标准它把“无障碍”拆成四个维度可感知、可操作、可理解、健壮。这四个词听起来抽象落到测试里其实非常具体。可感知说的是用户能不能“看到”内容。图片没替代文字视频没有字幕颜色对比度不够导致字看不清都算这一类的失败项。可操作关心的是一切交互能不能用键盘、语音等多样化方式完成。按钮只能鼠标点、键盘完全跳不过去就是失败。可理解要求页面文案清晰、导航一致、输入提示明确。很多表单没有错误提示或者提示不知所云就栽在这里。健壮性则是看代码能不能被不同辅助技术屏幕阅读器、语音控制等稳定解析语义标签乱用嵌套错误的就是这一类问题。自动化测试能做的就是把这四个维度里“规则明确、可通过代码判断”的部分提取出来转成一条条断言。比如检查图片标签是否存在替代文本检查按钮是否拥有可访问名称检查页面标题层级是否跳跃检查前景背景色对比度是否达到指定数值。这些检查在几千上万个页面里人工做完不现实但脚本跑一遍几分钟就能出结果。这也正是无障碍自动化最大的价值不是替代人工评测而是把重复、机械、高遗漏率的批量检查任务用最低成本做掉。1.2 自动化与人工评测的分界线在哪里这条分界线如果不划清楚很容易走两个极端要么觉得自动化能解决一切要么觉得自动化不够智能就没必要做。我的观点始终是自动化做“规则体检”人工做“真实体验诊断”。这有点像一个比喻自动化扫描就像每年体检的抽血和影像检查能通过标准值快速筛出异常指标而人工评测像医生面诊能结合个体情况判断那些“指标正常但人不舒服”的问题。两者谁也代替不了谁。比如“aria-label写没写”是机器可以查的“aria-label写得合不合理屏幕阅读器用户听了会不会困惑”就得人工判断。“焦点能Tab到某个元素”可以自动化“用键盘完整走完一个下单流程需要多少次奇怪操作”就必须真人模拟。所以我在往流水线里塞自动化测试时从来不说“加了它我们就不用做手工无障碍测试了”而是说“加了它人工测试可以专注在真正需要人判断的复杂场景上”。2. 工具选型先选对棋子再下棋2.1 一套能进CI的Web检测栈无障碍自动化的工具圈子里数得上的就有axe-core、Lighthouse CI、pa11y、WAVE这些。真到选型时不要看谁名气大要看能不能落进你现有的测试体系。我的主力是axe-core原因有三个规则全、可编程、报告结构清晰。axe-core底层有几百条检测规则大量对应WCAG的A/AA级成功标准而且能作为npm库被任意Node.js测试框架调用无损集成到Jest、Playwright、Cypress里。Lighthouse CI也值得在列表里留一个位置它更适合做性能与无障碍的“概览分”跑完出一个分数帮你判断一个大版本有没有把无障碍搞出明显反弹。但它是页面级别扫描定位具体DOM元素和精确失败原因的能力不如axe-core所以我把它放在巡检报表里不作为阻断门禁的唯一依据。pa11y相对轻量命令行就能跑适合小项目或者临时巡检但规则自定义能力弱团队大了以后容易不够用。WAVE是浏览器插件人工点一点很方便但它不适合做自动化只适合开发者在本地快速自查。我的建议是WAVE留给团队里不写代码的同学做抽查axe-core进自动化Lighthouse CI做趋势监控三层各干各的。这一套组合拳下来Web端的合规基线就有保障了。2.2 移动端怎么做移动端的无障碍自动化起步比Web晚工具和链路都散一些但思路是通的。Android这边Espresso和UIAutomator都提供了断言辅助功能信息的手段可以检查控件的contentDescription是否为空、可点击元素的类名是否被正确识别等。iOS这边XCUITest 的API里可以直接读取元素的 accessibilityLabel、accessibilityTraits、accessibilityFrame等属性。Appium作为跨平台框架把这两套能力都包裹统一了所以如果你的团队已经有Appium做UI自动化在现有用例里加无障碍断言是最顺的路。但移动端有个现实问题iOS和Android系统自身的辅助机制差异极大很多iOS上合理的交互设计在Android上就是不可达的。比如VoiceOver和TalkBack对手势、焦点顺序的模型完全不同。自动化和手工测试在移动端更需要平行推进自动化保证“属性没丢、关键节点可访问”手工用TalkBack和VoiceOver跑主路径才能覆盖那些系统机制层面的体验问题。2.3 合规基线来自哪里工具选了规则也配好了但“合规”到底以什么为基线这事必须在项目启动时就锤死。我的经验是先看你的产品面向什么市场。不同市场对无障碍的要求有差异有些只要求满足基础规范有些明确规定必须达到WCAG 2.1 AA。团队应该把目标白纸黑字写进测试计划而不是默认“跑过axe就没问题”。锤定基线之后再去工具里核对规则覆盖度。axe-core自带规则就有一份映射表标明每条规则对应WCAG的哪个成功标准、属于A级还是AA级。我每接手一个新项目第一件事就是同步这份映射表把团队承诺要达到的等级对应规则打开把不相关的等级规则关掉或降级为警告避免扫描报告里出现一堆“合规范围外”的噪音。这一步不做后面的报告质量会非常差。3. 核心实操从零把无障碍测试嵌进流水线3.1 在Playwright测试里集成axe-corePlaywright搭配axe-core/playwright是目前我用的最顺的Web自动化组合。安装依赖就两步然后在一个测试文件里注入扫描即可。这里的核心思想是无障碍检查不是单独的测试而是每条关键页面用例里的“必经卡口”。我通常写一个自定义断言在打开页面后自动执行可访问性扫描并把结果序列成JSON挂到测试报告里。import { test, expect } from playwright/test; import AxeBuilder from axe-core/playwright; test(商品列表页无障碍扫描, async ({ page }) { await page.goto(https://your.site/products); const results await new AxeBuilder({ page }).analyze(); expect(results.violations).toEqual([]); });如果不加任何配置上面的用例扫描完后只要存在axe判定为violation违反规则的项目测试就失败。但实际项目里我从不直接这么写因为一个老项目第一次接入时violations可能上百条直接把门禁关了团队寸步难行。我采用的策略是先跑一轮“摸底扫描”把历史存量问题全部记录到基线文件里。这需要在Page Object里封装一层扫描方法允许传入一个已知问题清单把存量问题降级为警告只对新增问题报红。async function assertAccessible(page, pageName) { const results await new AxeBuilder({ page }).analyze(); const knownIssues readBaseline(pageName); // 从基线文件读取历史存量问题 const newViolations results.violations.filter(v !knownIssues.some(k k.id v.id k.target v.nodes[0]?.target) ); expect(newViolations).toEqual([]); }3.2 独立巡检脚本和CI触发策略Playwright的用例适合在回归的时候跑但合规性检查还需要一个“定时巡检”的机制。因为产品是在不断变化的一个按钮文案调整、一次图片替换、一次样式重构都可能引入无障碍问题。纯等回归用例触发太被动所以我额外维护了一个独立的巡检脚本用Playwright的编程模式写成一个Node脚本定期对所有核心页面跑一遍axe扫描把结果输出为HTML和JSON报告。这个巡检脚本我放在CI里跑了两种触发方式。第一种是MR触发前端代码有改动时半个小时内在合并请求上返回一条“无障碍检查未通过”的评论把失败规则和具体DOM定位贴上开发改起来就很快。第二种是夜间定时触发跑全量核心页面发现失败就自动建工单。相比“周报里说一句无障碍测试覆盖了多少页面”这种机制能真正做到问题当天暴露而不是上线前才发现。MR触发时我会把失败断言的门槛设严一点夜间巡检可以稍微放宽把“规则警告”也纳进来。为什么呢因为给开发在合并请求里看的东西要尽量鲜明不能一堆黄条给夜间巡检看的是趋势可以多收集一些“潜在风险”做汇总。3.3 移动端实操Appium里加一条无障碍断言移动端的自动化我以Appium为主Android上跑原生的UIAutomator能力。下面这个例子是Python写的Appium测试核心思路是在已有UI测试里获取一组可点击元素的列表断言它们的content-description属性不为空。这属于“可感知/可操作”维度里最基础的检查代码不多但能拦住大量低级的问题比如后端只放了图标没加点位文案之类的。from appium import webdriver from appium.webdriver.common.appiumby import AppiumBy desired_caps { platformName: Android, deviceName: emulator-5554, appPackage: com.example.app, appActivity: .MainActivity, automationName: UiAutomator2 } driver webdriver.Remote(http://localhost:4723/wd/hub, desired_caps) clickable_elements driver.find_elements( AppiumBy.ANDROID_UIAUTOMATOR, new UiSelector().clickable(true) ) missing_desc [] for el in clickable_elements: desc el.get_attribute(content-desc) text el.get_attribute(text) if not desc and not text: missing_desc.append(el.get_attribute(resource-id)) assert len(missing_desc) 0, f以下可点击控件缺少无障碍名称: {missing_desc}写这种断言的关键是“先理解控件语义再写选择器”。很多刚入门自动化测试的人一上来就盯着xpathAppium选择器尽量用 resource-id 和 class 这类稳定属性不要用层级索引。控件一变动xpath就废了而无障碍断言本身关心的是控件属性无关层级。另外一个经验是移动端自动化跑起来比Web慢断言和断言之间一定要等关键元素出现再继续不然极易出现“拖拽没结束就开始读属性读了个空”。所以实际我还会在find_elements之后加一个显式等待确保页面渲染完成。3.4 报告与问题分类扫描结果如果只是白底黑字的“3 violations”开发根本不知道先改哪个。我做的演示项目里会把结果分成三个级别映射到发布决策上。阻断项比如“图片无alt且该图片是唯一表达按钮含义的元素”必须修复才能发版。严重项比如“表单输入无标签屏幕阅读器用户无法知道需要填什么”尽快修复留到下一个迭代风险很高。优化项比如“对比度处于临界值但不影响大部分文字可读性”可以排期处理但不能无限拖。级别典型问题处理策略阻断核心操作无键盘可达、图片替代文本缺失且无法理解含义当轮迭代必须修复严重表单缺少关联label、ARIA状态表达错误、路由切换后焦点丢失下一个迭代优先修复优化列表语义可优化、标题层级可调整、对比度处于临界纳入技术债定期清理这个映射表我会在团队里反复强调。为什么因为如果所有问题都当成“必须立刻改”开发会疲然后开始想办法绕过扫描如果所有问题都当成“可优化”合规就永远落不了地。分级是让一个问题进入可讨论的管理轨道而不是单纯靠自动化脚本当枪使。4. 常见问题与排查技巧实录4.1 误报和漏报颜色对比度是重灾区自动化接入一段时间后团队最容易产生“扫描是不是有毛病”的情绪而对比度检查是最大导火索。axe-core算对比度时会使用元素在DOM里计算出来的前景色和背景色但如果背景是渐变、半透明叠加、或图片上有文字工具的算法就很容易算出一个“理论上失败”的结果而视觉上人眼是可以接受的。遇到这种情况我的第一反应不是关掉规则而是去核对真实渲染的截图。很多对比度误报是因为CSS里用了透明度混合浏览器渲染后的实际颜色和代码里写的不一致。你可以在扫描时覆盖样式强制设定成不透明背景再测也可以人工确认后把这一条加入“已知且合理”的豁免清单。豁免不是把规则关了再用“特殊情况”四个字搪塞而是要记录原因和复核时间让整个审计过程可追溯。4.2 aria-label乱用比不用更糟这是我在大量项目里见过最隐蔽的坑。为了应付自动化扫描前端在可点击的图标按钮上随手塞一个aria-label扫描确实过了因为元素“有可访问名称”。但等真实用户用屏幕阅读器操作时听到的是机器读出来的超长、重复、毫无语义的单词串体验比没有label还糟糕。aria-label的价值是按人类认知给出简短准确的名称比如“关闭弹窗”“收藏商品”而不是把整个页面介绍塞进去。自动化在这里只能做一重保障查“有没有”查“好不好”必须人工评审。所以我在团队里定了一条规矩任何涉及ARIA属性的改动都必须写一句注释说明用户会听到什么并让另一个同事配合屏幕阅读器验证。别嫌这条规矩重它实际省下的是上线后来自真实用户的投诉和返工。4.3 动态内容和单页应用的路由陷阱单页应用是自动化测试里的老难题无障碍检查同样受影响。一个典型的坑是页面切换时视觉上内容变了但页面焦点和文档标题没有更新导致屏幕阅读器用户停留在旧焦点上感知不到页面变化。axe-core的静态扫描对这类状态类问题通常是无能为力的因为页面DOM结构完全合法甚至可访问性都挺好但实际体验是断的。处理这种问题需要在自动化用例里加上“交互场景断言”。比如在React或Vue应用里路由跳转后断言document.activeElement是否切到了新页面的顶层容器或者断言页面标题是否更新。这属于把无障碍测试从“页面体检”推进到“行为验收”覆盖场景化的问题。我建议有一定资源投入的团队都往这个方向延伸因为合规严查时这类体验问题恰恰是高优先级。4.4 “扫描全绿”不等于“真无障碍”我接触过的项目里有些团队无人值守地让自动化跑了大半年报告一直绿团队就开始把无障碍当成“已经达标”的事直到真实测试才发现很多问题扫描测不出来。前面反复说过自动化的定位是“基线保障”它的产出不是一张全绿报告而是一张告诉你“底线在哪、风险在哪”的图。全绿只能说明“基线范围内的规则没有翻车”不代表键盘用户能顺畅完成任务也不代表屏幕阅读器用户能理解全部信息。我现在每个季度都会安排一次“真实环境走查”用屏幕阅读器比如NVDA、VoiceOver、TalkBack模拟完成几条核心业务链路把自动化覆盖不到的问题人工录一轮。做这个不是自我否定自动化而是把自动化省下来的时间花在最值得花的地方。比例上我建议自动化至少承担六到七成重复检查任务剩下三到四成人工聚焦体验和复杂场景。写在最后的几个实际交代如果你团队现在刚准备启动无障碍自动化我最想叮嘱的一句话是别想着“工具能测我就不用懂标准了”。工具只是把你对WCAG的理解固化成规则真正决定排查深度的是你对“用户怎么和设备交互”这件事的认知。我早期接过一个项目严格照着扫描结果改了三个月每个红点都消掉结果界面重构后颜色换了、焦点顺序乱了后知后觉才明白自己一直在修的是报告不是产品。后来我把时间和精力重新分配规则型问题交给自动化复杂性体验问题用固定周期人工走查补位这才把合规测试做成了一条可持续的流程。还有一个所有人都可以立刻用上的小技巧在你的自动化扫描报告里给每条violation附上WCAG对应的成功标准号比如“1.1.1 非文本内容”“2.4.3 焦点顺序”。这不仅方便开发去查标准原文也能逐步让团队形成一种语境——大家讨论问题时不再说“工具报错”而是说“这违反了哪条原则、影响哪些用户”这就是无障碍文化在团队里真正落地的那一天。