无障碍测试
简介
Playwright 可用于测试应用程序中的多种无障碍访问问题。
以下是一些可以检测到的问题示例:
- 由于与背景颜色对比度差,可能导致视力障碍用户难以阅读的文本
- 缺少屏幕阅读器可识别标签的 UI 控件和表单元素
- 具有重复 ID 的交互元素,可能会混淆辅助技术
以下示例依赖于 @axe-core/playwright
包,该包支持在 Playwright 测试中运行 axe 无障碍测试引擎。
自动化无障碍测试可以检测一些常见的无障碍问题,如缺失或无效的属性。但许多无障碍问题只能通过手动测试发现。我们建议结合使用自动化测试、手动无障碍评估和包容性用户测试。
对于手动评估,我们推荐使用 Accessibility Insights for Web,这是一个免费开源的开发工具,可引导您评估网站是否符合 WCAG 2.1 AA 标准。
无障碍测试示例
无障碍测试的工作方式与其他 Playwright 测试相同。您可以为它们创建单独的测试用例,也可以将无障碍扫描和断言集成到现有测试用例中。
以下示例演示了几个基本的无障碍测试场景。
扫描整个页面
这个示例展示了如何测试整个页面是否存在可自动检测的无障碍访问违规问题。测试步骤如下:
- 导入
@axe-core/playwright
包 - 使用常规的 Playwright Test 语法定义测试用例
- 使用常规的 Playwright 语法导航到待测页面
- 等待
AxeBuilder.analyze()
对页面执行无障碍扫描 - 使用常规的 Playwright Test 断言 来验证扫描结果中没有违规项
- TypeScript
- JavaScript
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright'; // 1
test.describe('homepage', () => { // 2
test('should not have any automatically detectable accessibility issues', async ({ page }) => {
await page.goto('https://your-site.com/'); // 3
const accessibilityScanResults = await new AxeBuilder({ page }).analyze(); // 4
expect(accessibilityScanResults.violations).toEqual([]); // 5
});
});
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default; // 1
test.describe('homepage', () => { // 2
test('should not have any automatically detectable accessibility issues', async ({ page }) => {
await page.goto('https://your-site.com/'); // 3
const accessibilityScanResults = await new AxeBuilder({ page }).analyze(); // 4
expect(accessibilityScanResults.violations).toEqual([]); // 5
});
});
配置 axe 扫描页面的特定部分
@axe-core/playwright
支持许多 axe 的配置选项。您可以通过使用 AxeBuilder
类的 Builder 模式来指定这些选项。
例如,您可以使用 AxeBuilder.include()
来限制无障碍扫描仅针对页面的一个特定部分运行。
AxeBuilder.analyze()
会在调用时扫描页面的当前状态。要扫描基于 UI 交互才显示出来的页面部分,可以在调用 analyze()
之前使用 Locators 与页面进行交互:
test('导航菜单不应存在可自动检测的无障碍问题', async ({
page,
}) => {
await page.goto('https://your-site.com/');
await page.getByRole('button', { name: '导航菜单' }).click();
// 在运行 analyze() 之前 waitFor() 页面达到预期状态非常重要
// 否则 axe 可能无法找到测试预期扫描的所有元素
await page.locator('#navigation-menu-flyout').waitFor();
const accessibilityScanResults = await new AxeBuilder({ page })
.include('#navigation-menu-flyout')
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
扫描 WCAG 违规情况
默认情况下,axe 会检查各种无障碍访问规则。其中部分规则对应 Web内容无障碍指南(WCAG) 的具体成功标准,其他则是"最佳实践"规则,不属于任何 WCAG 标准的强制要求。
您可以使用 AxeBuilder.withTags()
来限制无障碍扫描范围,仅运行被"标记"为对应特定 WCAG 成功标准的规则。例如,Accessibility Insights for Web 的自动化检查 仅包含检测 WCAG A 和 AA 成功标准违规的 axe 规则;要匹配此行为,您可以使用标签 wcag2a
、wcag2aa
、wcag21a
和 wcag21aa
。
请注意,自动化测试无法检测所有类型的 WCAG 违规。
test('不应存在任何可自动检测的 WCAG A 或 AA 违规', async ({ page }) => {
await page.goto('https://your-site.com/');
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
您可以在 axe API 文档的"Axe-core 标签"部分 找到 axe-core 支持的完整规则标签列表。
处理已知问题
在应用程序中添加无障碍测试时,一个常见问题是"如何屏蔽已知违规?"。以下示例演示了几种可用的技术。
从扫描中排除特定元素
如果您的应用程序包含一些已知问题的特定元素,可以使用 AxeBuilder.exclude()
将它们排除在扫描范围之外,直到您能够修复这些问题。
这通常是最简单的选择,但也有一些重要的缺点:
exclude()
会排除指定元素及其所有子元素。避免在包含许多子组件的元素上使用此方法。exclude()
会阻止所有规则对指定元素的检查,而不仅仅是针对已知问题的规则。
以下示例展示了如何在特定测试中排除一个元素的扫描:
test('除了已知问题的元素外,不应存在任何无障碍访问违规', async ({
page,
}) => {
await page.goto('https://your-site.com/page-with-known-issues');
const accessibilityScanResults = await new AxeBuilder({ page })
.exclude('#element-with-known-issue')
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
如果该元素在多个页面中重复使用,可以考虑使用测试夹具来在多个测试中复用相同的 AxeBuilder
配置。
禁用特定扫描规则
如果您的应用程序中存在大量违反特定规则的既有问题,您可以使用 AxeBuilder.disableRules()
来临时禁用个别规则,直到您能够修复这些问题。
您可以在想要忽略的违规项的 id
属性中找到需要传递给 disableRules()
的规则 ID。axe 规则的完整列表可以在 axe-core
的文档中找到。
test('应不存在已知问题规则之外的可访问性违规', async ({
page,
}) => {
await page.goto('https://your-site.com/page-with-known-issues');
const accessibilityScanResults = await new AxeBuilder({ page })
.disableRules(['duplicate-id'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
使用快照允许特定的已知问题
如果您希望允许更细粒度的已知问题集,可以使用快照来验证一组预先存在的违规行为是否发生变化。这种方法避免了使用 AxeBuilder.exclude()
的缺点,但代价是略微增加了复杂性和脆弱性。
不要对整个 accessibilityScanResults.violations
数组进行快照。它包含相关元素的实现细节,例如它们渲染的 HTML 片段;如果在快照中包含这些内容,每当相关组件因无关原因发生变化时,都会导致测试容易失败:
// 不要这样做!这种方式很脆弱
expect(accessibilityScanResults.violations).toMatchSnapshot();
相反,为相关违规创建一个指纹,该指纹仅包含足够唯一标识问题的信息,并对该指纹进行快照:
// 这种方式比快照整个违规数组更健壮
expect(violationFingerprints(accessibilityScanResults)).toMatchSnapshot();
// my-test-utils.js
function violationFingerprints(accessibilityScanResults) {
const violationFingerprints = accessibilityScanResults.violations.map(violation => ({
rule: violation.id,
// 这些是CSS选择器,可以唯一标识每个违反相关规则的元素
targets: violation.nodes.map(node => node.target),
}));
return JSON.stringify(violationFingerprints, null, 2);
}
将扫描结果导出为测试附件
大多数无障碍测试主要关注 axe 扫描结果中的 violations
属性。然而,扫描结果包含的内容远不止 violations
。例如,结果还包含通过的规则信息,以及 axe 发现某些规则结果不确定的元素信息。这些信息对于调试未能检测到预期违规的测试非常有用。
为了将所有扫描结果作为测试结果的一部分用于调试,您可以使用 testInfo.attach()
将扫描结果添加为测试附件。测试报告器 随后可以将完整结果嵌入或链接到您的测试输出中。
以下示例演示了如何将扫描结果附加到测试中:
test('带附件的示例', async ({ page }, testInfo) => {
await page.goto('https://your-site.com/');
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
await testInfo.attach('无障碍扫描结果', {
body: JSON.stringify(accessibilityScanResults, null, 2),
contentType: 'application/json'
});
expect(accessibilityScanResults.violations).toEqual([]);
});
使用测试夹具配置通用 axe 设置
测试夹具 是在多个测试间共享通用 AxeBuilder
配置的好方法。以下场景特别适用:
- 在所有测试中使用一组通用规则
- 抑制出现在多个页面中的公共元素的已知违规
- 为多次扫描一致地附加独立无障碍报告
以下示例演示了创建和使用涵盖上述所有场景的测试夹具。
创建测试夹具
这个示例夹具创建了一个 AxeBuilder
对象,该对象预先配置了共享的 withTags()
和 exclude()
设置。
- TypeScript
- JavaScript
import { test as base } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
type AxeFixture = {
makeAxeBuilder: () => AxeBuilder;
};
// 通过提供 "makeAxeBuilder" 扩展基础测试
//
// 这个新的 "test" 可以在多个测试文件中使用,每个文件都将获得
// 一个配置一致的 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';
const base = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;
// 通过提供 "makeAxeBuilder" 扩展基础测试
//
// 这个新的 "test" 可以在多个测试文件中使用,每个文件都将获得
// 一个配置一致的 AxeBuilder 实例
exports.test = base.test.extend({
makeAxeBuilder: async ({ page }, use) => {
const makeAxeBuilder = () => new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.exclude('#commonly-reused-element-with-known-issue');
await use(makeAxeBuilder);
}
});
exports.expect = base.expect;
使用 fixture
要使用 fixture,请将之前示例中的 new AxeBuilder({ page })
替换为新定义的 makeAxeBuilder
fixture:
const { test, expect } = require('./axe-test');
test('使用自定义 fixture 的示例', async ({ page, makeAxeBuilder }) => {
await page.goto('https://your-site.com/');
const accessibilityScanResults = await makeAxeBuilder()
// 自动使用共享的 AxeBuilder 配置,
// 但也支持额外的测试特定配置
.include('#specific-element-under-test')
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});