跳到主要内容

无障碍测试

简介

Playwright 可以用于测试您的应用程序是否存在多种类型的无障碍问题。

以下是一些可以捕获的问题示例:

  • 由于与背景的颜色对比度差,视力障碍用户难以阅读的文本
  • 没有屏幕阅读器可以识别的标签的 UI 控件和表单元素
  • 具有重复 ID 的交互式元素,这可能会使辅助技术感到困惑

以下示例依赖于 @axe-core/playwright 包,该包添加了对运行 axe 无障碍测试引擎 作为 Playwright 测试一部分的支持。

免责声明

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

对于手动评估,我们推荐 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 成功标准的规则。例如,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 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 规则的完整列表 可以在 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 属性。但是,扫描结果包含的不仅仅是 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([]);
});

使用测试 fixture 进行常见的 axe 配置

测试 fixture 是在许多测试之间共享常见 AxeBuilder 配置的好方法。以下是一些可能有用的场景,包括:

  • 在所有测试中使用一组通用规则
  • 抑制在许多不同页面中出现的常见元素中的已知违规行为
  • 为许多扫描一致地附加独立的无障碍报告

以下示例演示了创建和使用涵盖每个场景的测试 fixture。

创建 fixture

此示例 fixture 创建一个 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';

使用 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()
// Automatically uses the shared AxeBuilder configuration,
// but supports additional test-specific configuration too
.include('#specific-element-under-test')
.analyze();

expect(accessibilityScanResults.violations).toEqual([]);
});