跳至主要内容

无障碍测试

简介

Playwright 可用于测试您的应用程序以查找多种类型的无障碍问题。

此功能可以发现的一些问题示例包括

  • 文本因与背景颜色对比度差,而难以被视力障碍用户阅读
  • 没有标签的 UI 控件和表单元素,屏幕阅读器无法识别
  • 具有重复 ID 的交互式元素,可能会混淆辅助技术

以下示例依赖于 @axe-core/playwright 包,它支持在 Playwright 测试中运行 axe 无障碍测试引擎

免责声明

自动化的无障碍测试可以检测一些常见的无障碍问题,例如缺少或无效的属性。但是,许多无障碍问题只能通过手动测试才能发现。我们建议使用自动测试、手动无障碍评估和包容性用户测试相结合的方式。

对于手动评估,我们建议使用 Accessibility Insights for Web,这是一个免费的开源开发工具,可帮助您评估网站以确保覆盖 WCAG 2.1 AA

无障碍测试示例

无障碍测试与任何其他 Playwright 测试的工作原理相同。您可以为它们创建单独的测试用例,也可以将无障碍扫描和断言集成到现有的测试用例中。

以下示例演示了一些基本的无障碍测试场景。

扫描整个页面

此示例演示如何测试整个页面以查找可自动检测到的无障碍违规行为。测试

  1. 导入 @axe-core/playwright
  2. 使用标准的 Playwright Test 语法定义测试用例
  3. 使用标准的 Playwright 语法导航到要测试的页面
  4. 等待 AxeBuilder.analyze() 对页面运行无障碍扫描
  5. 使用标准的 Playwright Test 断言 验证返回的扫描结果中是否没有违规行为
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
});
});

配置 axe 以扫描页面的特定部分

@axe-core/playwright 支持 axe 的许多配置选项。您可以使用 AxeBuilder 类的构建器模式指定这些选项。

例如,您可以使用 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 规则;要匹配此行为,您将使用标签 wcag2awcag2aawcag21awcag21aa

请注意,自动测试无法检测到所有类型的 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-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 代码片段;如果您在快照中包含这些内容,您的测试将会变得很脆弱,每当所涉及的组件因无关原因发生更改时,都会导致测试中断。

// 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() 配置。

axe-test.ts
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';

使用夹具

要使用夹具,请将之前示例中的 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([]);
});