无障碍测试
简介
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
类的构建器模式来指定这些选项。
例如,您可以使用AxeBuilder.include()
来限制无障碍扫描仅针对页面的某个特定部分运行。
AxeBuilder.analyze()
会在调用时扫描页面当前状态。要扫描基于用户交互才显示出来的页面部分,请先使用Locators与页面交互,再调用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 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([]);
});
如果所讨论的元素在多个页面中重复使用,考虑使用测试夹具以便在多个测试中复用相同的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([]);
});
使用快照功能允许特定的已知问题
如果您希望允许更细粒度的已知问题集,可以使用Snapshots来验证一组预先存在的违规行为是否未发生变化。这种方法避免了使用AxeBuilder.exclude()
的缺点,代价是稍微增加了复杂性和脆弱性。
Do not use a snapshot of the entire accessibilityScanResults.violations
array. It contains implementation details of the elements in question, such as a snippet of their rendered HTML; if you include these in your snapshots, it will make your tests prone to breaking every time one of the components in question changes for an unrelated reason:
// 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配置
Test fixtures 是一种在多个测试间共享通用 AxeBuilder
配置的好方法。以下场景可能会用到这种模式:
- 在所有测试中使用一套通用的规则
- 在多个不同页面中出现的常见元素中抑制已知违规
- 为多次扫描持续附加独立的无障碍访问报告
以下示例演示了创建和使用涵盖所有这些场景的测试夹具。
创建一个fixture
这个示例夹具创建了一个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, 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;
// 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, 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,请将之前示例中的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()
// Automatically uses the shared AxeBuilder configuration,
// but supports additional test-specific configuration too
.include('#specific-element-under-test')
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});