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:

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

以下示例依赖于 com.deque.html.axe-core/playwright Maven 包,该包支持在你的 Playwright 测试中运行 axe 无障碍测试引擎

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

免责声明

🌐 Disclaimer

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

🌐 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.

对于手动评估,我们推荐使用 Accessibility Insights for Web,这是一个免费且开源的开发工具,可引导你评估网站的 WCAG 2.1 AA 覆盖情况。

🌐 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.

示例 1:扫描整页

🌐 Example 1: Scanning an entire page

此示例演示了如何测试整个页面的自动可检测的无障碍违规情况。测试内容如下:

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

  1. 导入 com.deque.html.axe-core/playwright
  2. 使用普通的 JUnit 5 @Test 语法来定义测试用例
  3. 使用标准的 Playwright 语法打开浏览器并导航到被测页面
  4. 调用 AxeBuilder.analyze() 对页面进行无障碍扫描
  5. 使用标准的 JUnit 5 测试断言来验证返回的扫描结果中是否存在违规行为
import com.deque.html.axecore.playwright.*; // 1
import com.deque.html.axecore.utilities.axeresults.*;

import org.junit.jupiter.api.*;
import com.microsoft.playwright.*;

import static org.junit.jupiter.api.Assertions.*;

public class HomepageTests {
@Test // 2
void shouldNotHaveAutomaticallyDetectableAccessibilityIssues() throws Exception {
Playwright playwright = Playwright.create();
Browser browser = playwright.chromium().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();

page.navigate("https://your-site.com/"); // 3

AxeResults accessibilityScanResults = new AxeBuilder(page).analyze(); // 4

assertEquals(Collections.emptyList(), accessibilityScanResults.getViolations()); // 5
}
}

示例 2:配置 axe 扫描页面的特定部分

🌐 Example 2: Configuring axe to scan a specific part of a page

com.deque.html.axe-core/playwright 支持许多 axe 的配置选项。你可以使用 AxeBuilder 类通过 Builder 模式来指定这些选项。

例如,你可以使用 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() 在你调用它时会扫描页面的当前状态。要扫描根据用户界面交互显示的页面部分,请在调用 analyze() 之前使用 Locators 与页面进行交互:

public class HomepageTests {
@Test
void navigationMenuFlyoutShouldNotHaveAutomaticallyDetectableAccessibilityViolations() throws Exception {
page.navigate("https://your-site.com/");

page.locator("button[aria-label=\"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.
page.locator("#navigation-menu-flyout").waitFor();

AxeResults accessibilityScanResults = new AxeBuilder(page)
.include(Arrays.asList("#navigation-menu-flyout"))
.analyze();

assertEquals(Collections.emptyList(), accessibilityScanResults.getViolations());
}
}

示例 3:扫描 WCAG 违规情况

🌐 Example 3: Scanning for WCAG violations

默认情况下,axe 会检查各种可访问性规则。其中一些规则对应于《网页内容可访问性指南(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 成功标准的规则。例如,Web 的无障碍洞察的自动检查 只包括测试 WCAG A 和 AA 成功标准违规的 axe 规则;要匹配该行为,你可以使用标签 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.

AxeResults accessibilityScanResults = new AxeBuilder(page)
.withTags(Arrays.asList("wcag2a", "wcag2aa", "wcag21a", "wcag21aa"))
.analyze();

assertEquals(Collections.emptyList(), accessibilityScanResults.getViolations());

你可以在 axe 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() 将阻止所有规则在指定元素上运行,而不仅仅是针对已知问题的规则。

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

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

AxeResults accessibilityScanResults = new AxeBuilder(page)
.exclude(Arrays.asList("#element-with-known-issue"))
.analyze();

assertEquals(Collections.emptyList(), accessibilityScanResults.getViolations());

如果相关元素在许多页面中反复使用,可以考虑使用测试夹具在多个测试中重用相同的 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 preexisting 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 的文档中可以找到 axe 规则的完整列表

🌐 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.

AxeResults accessibilityScanResults = new AxeBuilder(page)
.disableRules(Arrays.asList("duplicate-id"))
.analyze();

assertEquals(Collections.emptyList(), accessibilityScanResults.getViolations());

使用违规指纹来定位特定已知问题

🌐 Using violation fingerprints to specific known issues

如果你希望更详细地了解已知问题,可以使用以下模式:

🌐 If you would like to allow for a more granular set of known issues, you can use the following pattern:

  1. 执行可访问性扫描,预计会发现一些已知的违规行为。
  2. 将违规行为转换为“违规指纹”对象
  3. 断言指纹集与预期指纹集相同

这种方法避免了使用 AxeBuilder.exclude() 的缺点,但代价是稍微增加了复杂性和脆弱性。

🌐 This approach avoids the downsides of using AxeBuilder.exclude() at the cost of slightly more complexity and fragility.

这是一个仅基于规则 ID 和指向每个违规项的“目标”选择器使用指纹的示例:

🌐 Here is an example of using fingerprints based on only rule IDs and "target" selectors pointing to each violation:

public class HomepageTests {
@Test
shouldOnlyHaveAccessibilityViolationsMatchingKnownFingerprints() throws Exception {
page.navigate("https://your-site.com/");

AxeResults accessibilityScanResults = new AxeBuilder(page).analyze();

List<ViolationFingerprint> violationFingerprints = fingerprintsFromScanResults(accessibilityScanResults);

assertEquals(Arrays.asList(
new ViolationFingerprint("aria-roles", "[span[role=\"invalid\"]]"),
new ViolationFingerprint("color-contrast", "[li:nth-child(2) > span]"),
new ViolationFingerprint("label", "[input]")
), violationFingerprints);
}

// You can make your "fingerprint" as specific as you like. This one considers a violation to be
// "the same" if it corresponds the same Axe rule on the same element.
//
// Using a record type makes it easy to compare fingerprints with assertEquals
public record ViolationFingerprint(String ruleId, String target) { }

public List<ViolationFingerprint> fingerprintsFromScanResults(AxeResults results) {
return results.getViolations().stream()
// Each violation refers to one rule and multiple "nodes" which violate it
.flatMap(violation -> violation.getNodes().stream()
.map(node -> new ViolationFingerprint(
violation.getId(),
// Each node contains a "target", which is a CSS selector that uniquely identifies it
// If the page involves iframes or shadow DOMs, it may be a chain of CSS selectors
node.getTarget().toString()
)))
.collect(Collectors.toList());
}
}

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

🌐 Using a test fixture for common axe configuration

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

🌐 A TestFixtures class is a good way to share common AxeBuilder configuration across many tests. Some scenarios where this might be useful include:

  • 在所有测试中使用一组通用规则
  • 抑制出现在许多不同页面中的公共元素中的已知违规行为
  • 为多次扫描一致地附加独立的可访问性报告

下面的示例演示了如何从 测试运行器示例 扩展 TestFixtures 类,并添加一个包含一些常见 AxeBuilder 配置的新夹具。

🌐 The following example demonstrates extending the TestFixtures class from the Test Runners example with a new fixture that contains some common AxeBuilder configuration.

创建夹具

🌐 Creating a fixture

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

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

class AxeTestFixtures extends TestFixtures {
AxeBuilder makeAxeBuilder() {
return new AxeBuilder(page)
.withTags(new String[]{"wcag2a", "wcag2aa", "wcag21a", "wcag21aa"})
.exclude("#commonly-reused-element-with-known-issue");
}
}

使用夹具

🌐 Using a fixture

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

🌐 To use the fixture, replace the earlier examples' new AxeBuilder(page) with the newly defined makeAxeBuilder fixture:

public class HomepageTests extends AxeTestFixtures {
@Test
void exampleUsingCustomFixture() throws Exception {
page.navigate("https://your-site.com/");

AxeResults accessibilityScanResults = makeAxeBuilder()
// Automatically uses the shared AxeBuilder configuration,
// but supports additional test-specific configuration too
.include("#specific-element-under-test")
.analyze();

assertEquals(Collections.emptyList(), accessibilityScanResults.getViolations());
}
}

请参阅实验性的 JUnit 集成,以自动初始化 Playwright 对象等功能。

🌐 See experimental JUnit integration to automatically initialize Playwright objects and more.