可访问性测试
介绍
Playwright 可用于测试应用程序中的多种可访问性问题。
一些可能发现的问题示例如下:
-
由于与背景色的对比度差,文字对于视力障碍用户来说可能难以阅读
-
没有标签的 UI 控件和表单元素,屏幕阅读器无法识别
-
具有重复 ID 的交互元素,可能会混淆辅助技术
以下示例依赖于 @axe-core/playwright
包,它为 Playwright 测试添加了运行 【axe 可访问性测试引擎】的支持。
免责声明
自动化可访问性测试可以检测一些常见的可访问性问题,如缺失或无效的属性。但许多可访问性问题只能通过手动测试发现。我们建议结合使用自动化测试、手动可访问性评估和包容性用户测试。 对于手动评估,我们推荐使用 Accessibility Insights for Web,它是一个免费的开源开发工具,可以帮助您评估网站是否符合 WCAG 2.1 AA 标准。 |
可访问性测试示例
可访问性测试和其他 Playwright 测试一样工作。您可以为它们创建单独的测试用例,也可以将可访问性扫描和断言集成到现有的测试用例中。
以下示例展示了一些基本的可访问性测试场景。
扫描整个页面
这个示例展示了如何测试整个页面,检查是否存在可自动检测的可访问性违规。测试过程如下:
-
导入
@axe-core/playwright
包 -
使用普通的 Playwright 测试语法定义一个测试用例
-
使用普通的 Playwright 语法导航到待测试页面
-
等待
AxeBuilder.analyze()
执行可访问性扫描 -
使用 Playwright 测试【断言】,验证返回的扫描结果中没有违规
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
支持多种配置选项。您可以通过使用 AxeBuilder
类的 Builder 模式指定这些选项。
例如,您可以使用 AxeBuilder.include()
来限制可访问性扫描只运行在页面的特定部分。
AxeBuilder.analyze()
将扫描当前页面状态。要扫描通过 UI 交互揭示的页面部分,使用 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 标签" 部分】找到 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([]);
});
如果该元素在多个页面中被反复使用,可以考虑【使用测试夹具(test fixture)】在多个测试中重用相同的 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
属性。然而,扫描结果不仅仅包含违规情况。例如,结果还包含关于通过的规则的信息,以及 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()
配置。
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';
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;
使用夹具
要使用该夹具,可以将先前示例中的 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([]);
});