可访问性测试 Accessibility
本教程共 59 篇 · 第 51 篇 · 更新于 2026-08-04 · 约 8 分钟阅读
51. 可访问性测试 Accessibility
本节目标:学完你能把无障碍检查接进 Playwright 测试,自动抓出颜色对比、缺标签、重复 id 这类常见问题。
可访问性(accessibility,常缩写为 a11y)讲的是:残障用户能不能用好你的网站。自动测试抓不了全部,但能拦下不少低级错误——比如文字和背景对比太低、按钮没标签、重复 id 把辅助技术搞晕。
Note自动化只能发现一部分问题。完整的覆盖还要靠人工评估和无障碍用户实测。Playwright 官方推荐配 Accessibility Insights for Web 一起用。
靠哪个引擎
Playwright 自己不带无障碍引擎,它接的是社区成熟的 axe(由 Deque 出品)。装这个包就能用:
npm install -D @axe-core/playwright
扫整页
写法和普通 Playwright 测试没两样:导航、调用 AxeBuilder.analyze() 扫描、再用断言看有没有违规。
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('homepage', () => {
test('should not have any automatically detectable accessibility issues', async ({ page }) => {
await page.goto('https://your-site.com/');
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
});
violations 是数组,空数组就表示没发现问题。断言一失败,里面会列出每条违规的细节。
只扫页面某块
用 Builder 模式的 include(),把扫描范围缩到某个区域。比如先点开导航菜单,等它出现,再扫它:
test('navigation menu should not have violations', async ({ page }) => {
await page.goto('https://your-site.com/');
await page.getByRole('button', { name: 'Navigation Menu' }).click();
// 先等元素出现,再扫,否则 axe 可能扫不到
await page.locator('#navigation-menu-flyout').waitFor();
const results = await new AxeBuilder({ page })
.include('#navigation-menu-flyout')
.analyze();
expect(results.violations).toEqual([]);
});
Tip
analyze()扫的是「当下」的页面状态。动态出现的内容,先交互、等它稳定,再调analyze()。
按 WCAG 标签筛
axe 的规则很多,有些对应 WCAG 标准,有些只是「最佳实践」。想只查 WCAG A/AA 级,用 withTags():
test('should not have WCAG A or AA violations', async ({ page }) => {
await page.goto('https://your-site.com/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(results.violations).toEqual([]);
});
Warning自动化测不出所有 WCAG 违规。标签筛只是收窄范围,不代表「过了就合规」。
处理已知问题
真实项目常有暂时修不了的老问题。三种压制办法:
排除某个元素:用 exclude() 把它和它所有子节点踢出扫描。简单,但会顺带关掉所有规则,慎用于子节点多的组件。
const results = await new AxeBuilder({ page })
.exclude('#element-with-known-issue')
.analyze();
关掉某条规则:问题遍布全站、且都属同一条规则,用 disableRules() 临时关:
const results = await new AxeBuilder({ page })
.disableRules(['duplicate-id'])
.analyze();
用快照允许特定问题:想更精细,别直接 toMatchSnapshot() 整个 violations(那里面有渲染 HTML 片段,组件一改就碎)。改成只快照「指纹」:
function violationFingerprints(scanResults) {
return JSON.stringify(
scanResults.violations.map(v => ({
rule: v.id,
targets: v.nodes.map(n => n.target),
})),
null, 2
);
}
expect(violationFingerprints(accessibilityScanResults)).toMatchSnapshot();
把扫描结果存为附件
想调试「为什么没扫到预期的违规」,把完整结果作为测试附件挂上,报告里就能看:
test('example with attachment', async ({ page }, testInfo) => {
await page.goto('https://your-site.com/');
const results = await new AxeBuilder({ page }).analyze();
await testInfo.attach('accessibility-scan-results', {
body: JSON.stringify(results, null, 2),
contentType: 'application/json',
});
expect(results.violations).toEqual([]);
});
用 fixture 复用配置
很多测试要用同一套 axe 配置?扩展一个 test,把 withTags 和 exclude 预先配好:
// axe-test.ts
import { test as base } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
type AxeFixture = { makeAxeBuilder: () => AxeBuilder };
export const test = base.extend<AxeFixture>({
makeAxeBuilder: async ({ page }, use) => {
const makeAxeBuilder = () => new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.exclude('#commonly-reused-element-with-known-issue');
await use(makeAxeBuilder);
},
});
export { expect } from '@playwright/test';
用的时候直接取这个夹具:
import { test, expect } from './axe-test';
test('example using custom fixture', async ({ page, makeAxeBuilder }) => {
await page.goto('https://your-site.com/');
const results = await makeAxeBuilder()
.include('#specific-element-under-test')
.analyze();
expect(results.violations).toEqual([]);
});
什么时候该用
每次页面结构大改、或加新组件时跑一遍无障碍扫描,能提前挡掉一大批硬伤。把它当「质量门禁」接进 CI 最划算。但记住:自动过不等于真无障碍,关键路径还是得人工和无障碍用户实测。
小结
这章把 @axe-core/playwright 接进测试,用 AxeBuilder 扫整页或局部,按 WCAG 标签筛规则。已知问题可以排除元素、关规则、或者只快照「指纹」避免报告脆弱。几个注意点:动态内容先交互再扫,exclude() 会关掉该节点下所有规则,别图省事滥用。自动扫描只是底线,别拿它当无障碍的终点。