无障碍测试
简介
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()
之前使用 定位符 与页面进行交互。
test('navigation menu should not have automatically detectable accessibility violations', async ({
page,
}) => {
await page.goto('https://your-site.com/');
await page.getByRole('button', { name: 'Navigation Menu' }).click();
// It is important to waitFor() the page to be in the desired
// state *before* running analyze(). Otherwise, axe might not
// find all the elements your test expects it to scan.
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('should not have any automatically detectable WCAG A or AA violations', 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('should not have any accessibility violations outside of elements with known issues', 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('should not have any accessibility violations outside of rules with known issues', 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 片段;如果将这些包含在快照中,每当相关组件因不相关的原因发生变化时,你的测试就容易失败。
// Don't do this! This is fragile.
expect(accessibilityScanResults.violations).toMatchSnapshot();
相反,创建相关违规的指纹,其中仅包含足以唯一标识问题的信息,并使用该指纹的快照。
// This is less fragile than snapshotting the entire violations array.
expect(violationFingerprints(accessibilityScanResults)).toMatchSnapshot();
// my-test-utils.js
function violationFingerprints(accessibilityScanResults) {
const violationFingerprints = accessibilityScanResults.violations.map(violation => ({
rule: violation.id,
// These are CSS selectors which uniquely identify each element with
// a violation of the rule in question.
targets: violation.nodes.map(node => node.target),
}));
return JSON.stringify(violationFingerprints, null, 2);
}
将扫描结果导出为测试附件
大多数无障碍测试主要关注 axe 扫描结果的 violations
属性。然而,扫描结果不仅仅包含 violations
。例如,结果还包含有关通过的规则以及 axe 发现某些规则结果不确定元素的的信息。这些信息对于调试未能按预期检测到所有违规的测试很有用。
为了将所有扫描结果作为测试结果的一部分用于调试目的,你可以使用 testInfo.attach()
将扫描结果添加为测试附件。报告器 随后可以将完整结果嵌入或链接到你的测试输出中。
以下示例演示了将扫描结果附加到测试中
test('example with attachment', async ({ page }, testInfo) => {
await page.goto('https://your-site.com/');
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
await testInfo.attach('accessibility-scan-results', {
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;
};
// Extend base test by providing "makeAxeBuilder"
//
// This new "test" can be used in multiple test files, and each of them will get
// a consistently configured AxeBuilder instance.
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;
// Extend base test by providing "makeAxeBuilder"
//
// This new "test" can be used in multiple test files, and each of them will get
// a consistently configured AxeBuilder instance.
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;
使用夹具
要使用该夹具,将前面示例中的 new AxeBuilder({ page })
替换为新定义的 makeAxeBuilder
夹具
const { test, expect } = require('./axe-test');
test('example using custom fixture', async ({ page, makeAxeBuilder }) => {
await page.goto('https://your-site.com/');
const accessibilityScanResults = await makeAxeBuilder()
// Automatically uses the shared AxeBuilder configuration,
// but supports additional test-specific configuration too
.include('#specific-element-under-test')
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});