我们如何使用 Playwright 和 Axe 自动化可访问性测试
Source: Dev.to
我们的工具箱:Axe + Playwright
我们选择了 Axe,这是来自 Deque Systems 的开源库,作为我们的可访问性测试引擎。它提供了在浏览器中直接运行测试的 JavaScript API,而 @axe-core/playwright 包则让集成变得无缝。
因为我们已经在使用 Playwright 进行视觉回归测试和端到端套件,在此基础上添加可访问性检查是显而易见的下一步——无需学习新工具,只需在我们熟悉的 Playwright 工作流中加入 Axe 引擎即可。
配置
首先,我们创建了一个帮助函数来获取预配置好的 Axe 实例。我们的配置侧重于 WCAG 2.1 Level A 和 AA 标准。
WCAG 是什么? 网络内容可访问性指南(WCAG)由 W3C 制定,旨在让网络内容更易于访问。
- Level A: 最低合规级别。
- Level AA: 我们目标的中等级别,解决更高级的障碍。
- Level AAA: 最高、最严格的级别。
我们还明确排除了一些我们无法直接控制的元素(例如第三方广告),以避免误报。
// /test/utils/axe.ts
import { Page } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
export const getAxeInstance = (page: Page) => {
return new AxeBuilder({ page })
// Target WCAG 2.1 A and AA success criteria
.withTags(['wcag2a', 'wcag2aa'])
// Exclude elements we don't control, like ads
.exclude('[id^="google_ads_iframe_"]')
.exclude('#skinadvtop2')
.exclude('#subito_skin_id');
};
实现:生成并保存报告
接下来,我们添加了一个 generateAxeReport 辅助函数,用于运行分析并将结果写入 JSON 文件。
// /test/utils/axe.ts
import { Page } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import { Result } from 'axe-core';
import * as fs from 'fs';
import * as path from 'path';
import { getAxeInstance } from './axe'; // assuming same file
export const generateAxeReport = async (
name: string,
page: Page,
isMobile: boolean,
includeSelector?: string
) => {
let axe = getAxeInstance(page);
// Optionally scope the analysis to a specific selector
if (includeSelector) {
axe = axe.include(includeSelector);
}
const results = await axe.analyze();
const violations = results.violations;
// Save the results to a JSON file
await saveAccessibilityResults(name, violations, isMobile);
return violations;
};
async function saveAccessibilityResults(
fileName: string,
violations: Array,
isMobile: boolean
) {
const outputDir = 'test/a11y/output';
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const filePath = path.join(
outputDir,
`${fileName}-${isMobile ? 'mobile' : 'desktop'}.json`
);
// Map violations to a clean object for serialization
const escapedViolations = violations.map((violation) => ({
id: violation.id,
impact: violation.impact,
description: violation.description,
help: violation.help,
helpUrl: violation.helpUrl,
nodes: violation.nodes,
}));
fs.writeFileSync(filePath, JSON.stringify(escapedViolations, null, 2));
console.log(`Accessibility results saved to ${filePath}`);
}
可访问性测试
有了这些辅助函数,在任何 Playwright 测试中加入可访问性检查都非常简单。
// /test/a11y/example.spec.ts
import { test } from '@playwright/test';
import { generateAxeReport } from '../utils/axe';
test('check Login page', async ({ page }) => {
await page.goto('/login_form');
await page.waitForLoadState('domcontentloaded');
// Run the helper
await generateAxeReport('login-page', page, false);
});
运行测试后会生成一个 JSON 报告(例如 login-page-desktop.json),其中包含所有可访问性发现。

与持续集成 (CI) 的集成
我们的 CI 工作流在每次 staging 部署时触发。它:
- 运行 对预定义关键页面的可访问性测试。
- 生成 JSON 报告。
- 在检测到违规时 更新或创建一个专门的 GitHub Issue 来记录结果。
自动创建的 Issue 如下所示:

以及违规的详细列表:

为什么使用 GitHub Issue?(而不是让构建失败)
与我们的视觉回归测试不同,后者会打开 PR 并发送 Slack 通知,我们选择在 GitHub Issue 中记录可访问性发现。
我们仍在逐步建立可访问性覆盖率,如果对每个违规都让流水线失败将难以维系。通过 Issue:
- 我们保留了可访问性债务的持久记录。
- 仓库所有者负责对这些问题进行分拣、优先级排序和安排修复。
下面是一个示例 Pull Request,展示了如何处理之前在 GitHub Issue 中记录的某条违规。
(图片略)