Skip to main content

无障碍测试

介绍

Introduction

Playwright 可用于测试你的应用是否存在多种类型的可访问性问题。

Playwright can be used to test your application for many types of accessibility issues.

这可以捕获的一些问题示例包括:

A few examples of problems this can catch include:

  • 由于与背景的颜色对比度较差,有视力障碍的用户难以阅读文本

    Text that would be hard to read for users with vision impairments due to poor color contrast with the background behind it

  • UI 控件和表单元素没有屏幕阅读器可以识别的标签

    UI controls and form elements without labels that a screen reader could identify

  • 具有重复 ID 的交互元素可能会混淆辅助技术

    Interactive elements with duplicate IDs which can confuse assistive technologies

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

The following examples rely on the @axe-core/playwright package which adds support for running the axe accessibility testing engine as part of your Playwright tests.

免责声明

自动可访问性测试可以检测一些常见的可访问性问题,例如丢失或无效的属性。但很多可访问性问题只能通过手动测试才能发现。我们建议结合使用自动化测试、手动可访问性评估和包容性用户测试。对于手动评估,我们推荐 Web 的可访问性见解,这是一个免费的开源开发工具,可引导你评估网站的 WCAG 2.1 AA 覆盖范围。

Automated accessibility tests can detect some common accessibility problems such as missing or invalid properties. But many accessibility problems can only be discovered through manual testing. We recommend using a combination of automated testing, manual accessibility assessments, and inclusive user testing. For manual assessments, we recommend Accessibility Insights for Web, a free and open source dev tool that walks you through assessing a website for WCAG 2.1 AA coverage.

可访问性测试示例

Example accessibility tests

可访问性测试的工作方式与任何其他 Playwright 测试一样。你可以为它们创建单独的测试用例,也可以将可访问性扫描和断言集成到现有的测试用例中。

Accessibility tests work just like any other Playwright test. You can either create separate test cases for them, or integrate accessibility scans and assertions into your existing test cases.

以下示例演示了一些基本的可访问性测试场景。

The following examples demonstrate a few basic accessibility testing scenarios.

扫描整个页面

Scanning an entire page

此示例演示如何测试整个页面是否存在可自动检测的可访问性违规。考试:

This example demonstrates how to test an entire page for automatically detectable accessibility violations. The test:

  1. 导入 @axe-core/playwright

    Imports the @axe-core/playwright package

  2. 使用普通的 Playwright Test 语法来定义测试用例

    Uses normal Playwright Test syntax to define a test case

  3. 使用正常的 Playwright 语法导航到正在测试的页面

    Uses normal Playwright syntax to navigate to the page under test

  4. 等待 AxeBuilder.analyze() 对页面运行辅助功能扫描

    Awaits AxeBuilder.analyze() to run the accessibility scan against the page

  5. 使用正常的 Playwright Test assertions 验证返回的扫描结果中没有违规

    Uses normal Playwright Test assertions to verify that there are no violations in the returned scan results

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 扫描页面的特定部分

Configuring axe to scan a specific part of a page

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

@axe-core/playwright supports many configuration options for axe. You can specify these options by using a Builder pattern with the AxeBuilder class.

例如,你可以使用 AxeBuilder.include() 来限制辅助功能扫描仅针对页面的一个特定部分运行。

For example, you can use AxeBuilder.include() to constrain an accessibility scan to only run against one specific part of a page.

当你调用 AxeBuilder.analyze() 时,AxeBuilder.analyze() 将扫描当前状态的页面。要扫描基于 UI 交互显示的页面部分,请在调用 analyze() 之前使用 定位器 与页面交互:

AxeBuilder.analyze() will scan the page in its current state when you call it. To scan parts of a page that are revealed based on UI interactions, use Locators to interact with the page before invoking 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 违规行为

Scanning for WCAG violations

默认情况下,ax 会检查各种可访问性规则。其中一些规则对应于 网页内容无障碍指南 (WCAG) 中的特定成功标准,其他规则是任何 WCAG 标准均未明确要求的 "最佳实践" 规则。

By default, axe checks against a wide variety of accessibility rules. Some of these rules correspond to specific success criteria from the Web Content Accessibility Guidelines (WCAG), and others are "best practice" rules that are not specifically required by any WCAG criterion.

你可以使用 AxeBuilder.withTags() 将辅助功能扫描限制为仅运行与特定 WCAG 成功标准相对应的 "tagged" 规则。例如,Web 自动检查的可访问性见解 仅包含测试是否违反 WCAG A 和 AA 成功标准的斧头规则;为了匹配该行为,你可以使用标签 wcag2awcag2aawcag21awcag21aa

You can constrain an accessibility scan to only run those rules which are "tagged" as corresponding to specific WCAG success criteria by using AxeBuilder.withTags(). For example, Accessibility Insights for Web's Automated Checks only include axe rules that test for violations of WCAG A and AA success criteria; to match that behavior, you would use the tags wcag2a, wcag2aa, wcag21a, and wcag21aa.

请注意,自动化测试无法检测所有类型的 WCAG 违规行为。

Note that automated testing cannot detect all types of WCAG violations.

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([]);
});

你可以在 ax API 文档的 "Axe-core 标签" 部分 中找到 axe-core 支持的规则标签的完整列表。

You can find a complete listing of the rule tags axe-core supports in the "Axe-core Tags" section of the axe API documentation.

处理已知问题

Handling known issues

向应用添加可访问性测试时的一个常见问题是 "如何压制已知的违规行为?" 以下示例演示了你可以使用的一些技术。

A common question when adding accessibility tests to an application is "how do I suppress known violations?" The following examples demonstrate a few techniques you can use.

从扫描中排除单个元素

Excluding individual elements from a scan

如果你的应用包含一些存在已知问题的特定元素,你可以使用 AxeBuilder.exclude() 将它们排除在扫描之外,直到你能够解决这些问题。

If your application contains a few specific elements with known issues, you can use AxeBuilder.exclude() to exclude them from being scanned until you're able to fix the issues.

这通常是最简单的选择,但它有一些重要的缺点:

This is usually the simplest option, but it has some important downsides:

  • exclude() 将排除指定的元素及其所有后代。避免将其与包含许多子组件的组件一起使用。

    exclude() will exclude the specified elements and all of their descendants. Avoid using it with components that contain many children.

  • exclude() 将阻止所有规则针对指定元素运行,而不仅仅是与已知问题对应的规则。

    exclude() will prevent all rules from running against the specified elements, not just the rules corresponding to known issues.

以下是在一项特定测试中排除一个元素进行扫描的示例:

Here is an example of excluding one element from being scanned in one specific test:

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 配置。

If the element in question is used repeatedly in many pages, consider using a test fixture to reuse the same AxeBuilder configuration across multiple tests.

禁用个别扫描规则

Disabling individual scan rules

如果你的应用包含许多不同的预先存在的违反特定规则的行为,你可以使用 AxeBuilder.disableRules() 暂时禁用各个规则,直到你能够解决这些问题。

If your application contains many different pre-existing violations of a specific rule, you can use AxeBuilder.disableRules() to temporarily disable individual rules until you're able to fix the issues.

你可以在要抑制的违规行为的 id 属性中找到要传递给 disableRules() 的规则 ID。斧头规则的完整列表 可以在 axe-core 的文档中找到。

You can find the rule IDs to pass to disableRules() in the id property of the violations you want to suppress. A complete list of axe's rules can be found in axe-core's documentation.

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([]);
});

使用快照来解决特定的已知问题

Using snapshots to allow specific known issues

如果你希望允许更细化的已知问题集,你可以使用 快照 来验证一组预先存在的违规行为是否未发生更改。这种方法避免了使用 AxeBuilder.exclude() 的缺点,但代价是稍微增加了复杂性和脆弱性。

If you would like to allow for a more granular set of known issues, you can use Snapshots to verify that a set of pre-existing violations has not changed. This approach avoids the downsides of using AxeBuilder.exclude() at the cost of slightly more complexity and fragility.

不要使用整个 accessibilityScanResults.violations 数组的快照。它包含相关元素的实现细节,例如渲染的 HTML 的片段;如果你将这些包含在快照中,那么每当相关组件之一因不相关的原因发生更改时,你的测试就很容易中断:

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();

相反,创建相关违规行为的指纹,其中仅包含足以唯一识别问题的信息,并使用指纹的快照:

Instead, create a fingerprint of the violation(s) in question that contains only enough information to uniquely identify the issue, and use a snapshot of the fingerprint:

// 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);
}

将扫描结果导出为测试附件

Exporting scan results as a test attachment

大多数可访问性测试主要关注 ax 扫描结果的 violations 属性。然而,扫描结果不仅仅包含 violations。例如,结果还包含有关通过的规则的信息以及有关发现对某些规则具有不确定结果的元素的信息。此信息对于调试未检测到你期望的所有违规行为的测试非常有用。

Most accessibility tests are primarily concerned with the violations property of the axe scan results. However, the scan results contain more than just violations. For example, the results also contain information about rules which passed and about elements which axe found to have inconclusive results for some rules. This information can be useful for debugging tests that aren't detecting all the violations you expect them to.

要将所有扫描结果作为测试结果的一部分以进行调试,你可以使用 testInfo.attach() 将扫描结果添加为测试附件。然后,报告器 可以嵌入或链接完整结果作为测试输出的一部分。

To include all of the scan results as part of your test results for debugging purposes, you can add the scan results as a test attachment with testInfo.attach(). Reporters can then embed or link the full results as part of your test output.

以下示例演示了将扫描结果附加到测试:

The following example demonstrates attaching scan results to a test:

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([]);
});

使用通用轴配置的测试夹具

Using a test fixture for common axe configuration

测试治具 是在许多测试中共享通用 AxeBuilder 配置的好方法。这可能有用的一些场景包括:

Test fixtures are a good way to share common AxeBuilder configuration across many tests. Some scenarios where this might be useful include:

  • 在所有测试中使用一组通用规则

    Using a common set of rules among all of your tests

  • 抑制出现在许多不同页面中的公共元素中的已知违规行为

    Suppressing a known violation in a common element which appears in many different pages

  • 为多次扫描一致地附加独立的可访问性报告

    Attaching standalone accessibility reports consistently for many scans

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

The following example demonstrates creating and using a test fixture that covers each of those scenarios.

创建夹具

Creating a fixture

此示例夹具创建一个 AxeBuilder 对象,该对象预先配置了共享 withTags()exclude() 配置。

This example fixture creates an AxeBuilder object which is pre-configured with shared withTags() and exclude() configuration.

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';

使用夹具

Using a fixture

要使用该夹具,请将前面示例的 new AxeBuilder({ page }) 替换为新定义的 makeAxeBuilder 夹具:

To use the fixture, replace the earlier examples' new AxeBuilder({ page }) with the newly defined 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([]);
});