无障碍测试
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('navigation menu flyout should not have automatically detectable accessibility violations', async ({ page }) => {
await page.goto('https://your-site.com/');
await page.getByRole('button', { name: 'Navigation Menu' }).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。
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 Tags”部分 中找到 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([]);
});
如果相关元素在许多页面中重复使用,请考虑 使用测试 fixture 在多个测试中重用相同的 AxeBuilder 配置。
禁用单个扫描规则
如果您的应用程序包含许多针对特定规则的不同预先存在的违规,您可以使用 AxeBuilder.disableRules() 暂时禁用单个规则,直到您能够修复这些问题。
您可以在要抑制的违规的 id 属性中找到要传递给 disableRules() 的规则 ID。可以在 axe-core 的文档中找到 axe 规则的完整列表。
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 片段;如果您将这些包含在快照中,那么每当相关组件之一因不相关的原因而更改时,您的测试就很容易中断:
// 不要这样做!这很脆弱。
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() 将扫描结果添加为测试附件。Reporters 然后可以将完整结果嵌入或链接为测试输出的一部分。
以下示例演示了将扫描结果附加到测试:
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([]);
});
使用测试 fixture 进行通用 axe 配置
Test fixtures 是在许多测试中共享通用 AxeBuilder 配置的好方法。这可能有用的一些场景包括:
- 在所有测试中使用一组通用规则
- 抑制出现在许多不同页面中的通用元素中的已知违规
- 为许多扫描一致地附加独立的无障碍报告
以下示例演示了创建和使用涵盖每个场景的测试 fixture。
创建 fixture
此示例 fixture 创建一个 AxeBuilder 对象,该对象预先配置了共享的 withTags() 和 exclude() 配置。
- TypeScript
- JavaScript
// axe-test.ts
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, testInfo) => {
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';
// axe-test.js
const base = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;
// 通过提供 "makeAxeBuilder" 扩展基础测试
//
// 这个新的 "test" 可以在多个测试文件中使用,并且每个文件都将获得
// 一个配置一致的 AxeBuilder 实例。
exports.test = base.test.extend({
makeAxeBuilder: async ({ page }, use, testInfo) => {
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('example using custom 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([]);
});