跳转到主要内容

无障碍测试

简介

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 类与 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 成功标准的规则。例如,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 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-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) => {
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([]);
});