跳至主要内容

无障碍测试

简介

Playwright 可用于测试应用程序中的多种无障碍访问问题。

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

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

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.

免责声明

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

对于手动评估,我们推荐使用Accessibility Insights for Web,这是一款免费开源的开发工具,可引导您完成网站对WCAG 2.1 AA标准的覆盖评估。

无障碍测试示例

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

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

示例1:扫描整个页面

这个示例演示了如何测试整个页面是否存在可自动检测的无障碍访问违规。测试内容包括:

  1. Imports the com.deque.html.axe-core/playwright package
  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以扫描页面的特定部分

com.deque.html.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()来限制无障碍扫描仅针对页面的某个特定部分运行。

AxeBuilder.analyze() 会在调用时扫描页面当前状态。要扫描基于用户交互才显示出来的页面部分,请先使用Locators与页面交互,再调用analyze()

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违规情况

默认情况下,axe会检查各种无障碍访问规则。其中一些规则对应Web内容无障碍指南(WCAG)中的特定成功标准,另一些则是"最佳实践"规则,这些规则并非任何WCAG标准明确要求的。

您可以通过使用AxeBuilder.withTags()来限制无障碍扫描仅运行那些被"标记"为对应特定WCAG成功标准的规则。例如,Accessibility Insights for Web的自动化检查仅包含测试违反WCAG A和AA成功标准的axe规则;要匹配该行为,您可以使用标签wcag2awcag2aawcag21awcag21aa

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

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

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

您可以在axe API文档的"Axe-core Tags"部分找到axe-core支持的所有规则标签的完整列表。

处理已知问题

在应用程序中添加无障碍测试时,一个常见问题是"如何屏蔽已知违规项?"以下示例演示了几种可用的技术。

从扫描中排除单个元素

如果您的应用程序包含一些已知问题的特定元素,可以使用AxeBuilder.exclude()将它们排除在扫描范围之外,直到您能够修复这些问题。

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

  • exclude() 将排除指定元素及其所有子元素。避免在包含许多子组件的元素上使用该方法。
  • exclude() 将阻止所有规则对指定元素运行,而不仅仅是针对已知问题对应的规则。

以下是一个在特定测试中排除某个元素不被扫描的示例:

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

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

如果所讨论的元素在多个页面中重复使用,考虑使用测试夹具以便在多个测试中复用相同的AxeBuilder配置。

禁用个别扫描规则

如果您的应用程序中存在许多预先违反特定规则的情况,可以使用AxeBuilder.disableRules()临时禁用个别规则,直到您能够解决这些问题。

您可以在想要忽略的违规项的id属性中找到要传递给disableRules()的规则ID。axe规则的完整列表可以在axe-core的文档中找到。

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

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

使用违规指纹识别特定已知问题

如果您希望允许更细粒度的已知问题集,可以使用以下模式:

  1. 执行可访问性扫描,预计会发现一些已知的违规问题
  2. 将违规行为转换为"违规指纹"对象
  3. 断言指纹集合与预期指纹集合等价

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

以下是仅基于规则ID和指向每个违规项的"target"选择器使用指纹的示例:

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

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

TestFixtures是一种在多个测试间共享通用 AxeBuilder 配置的好方法。以下是一些可能适用的场景:

  • 在所有测试中使用一套通用的规则
  • 在多个不同页面中出现的常见元素中抑制已知违规
  • 为多次扫描持续附加独立的无障碍访问报告

以下示例演示了如何从Test Runners示例中扩展TestFixtures类,添加一个包含常见AxeBuilder配置的新fixture。

创建一个fixture

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

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

使用夹具

要使用该fixture,请将之前示例中的new AxeBuilder(page)替换为新定义的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对象等功能。