跳到主要内容

可访问性测试

简介

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

它能够发现的一些问题示例包括:

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

以下示例依赖于 com.deque.html.axe-core/playwright Maven 包,该包支持将 axe 可访问性测试引擎作为 Playwright 测试的一部分来运行。

免责声明

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

对于手动评估,我们推荐使用 Web 可访问性洞察,这是一款免费的开源开发工具,可引导您评估网站对 WCAG 2.1 AA 的覆盖情况。

可访问性测试示例

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

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

示例 1:扫描整个页面

此示例展示了如何测试整个页面,以查找可自动检测到的可访问性违规问题。该测试:

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

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

例如,你可以使用 AxeBuilder.include() 将可访问性扫描限制为仅针对页面的一个特定部分运行。

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

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

page.locator("button[aria-label=\"Navigation Menu\"]").click();

// 在运行 analyze() *之前*,使用 waitFor() 等待页面达到所需状态非常重要。否则,axe 可能找不到你的测试期望它扫描的所有元素。
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 成功标准的规则。例如,适用于 Web 的自动化检查的可访问性洞察 仅包含测试 WCAG A 和 AA 成功标准违规情况的 axe 规则;要匹配该行为,你可以使用标签 wcag2awcag2aawcag21awcag21aa

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

AxeResults 可访问性扫描结果 = new AxeBuilder(page)
.withTags(Arrays.asList("wcag2a", "wcag2aa", "wcag21a", "wcag21aa"))
.analyze();

assertEquals(Collections.emptyList(), 可访问性扫描结果.getViolations());

你可以在 axe API 文档的 “Axe-core 标签” 部分 中找到 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 和指向每个违规的 “目标” 选择器使用指纹的示例:

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

// 你可以根据需要使 “指纹” 尽可能具体。如果违规对应于同一元素上的相同 Axe 规则,则此指纹将其视为 “相同”。
//
// 使用记录类型可以轻松地使用 assertEquals 比较指纹
public record ViolationFingerprint(String ruleId, String target) { }

public List<ViolationFingerprint> fingerprintsFromScanResults(AxeResults results) {
return results.getViolations().stream()
// 每个违规都涉及一个规则和多个违反该规则的 “节点”
.flatMap(violation -> violation.getNodes().stream()
.map(node -> new ViolationFingerprint(
violation.getId(),
// 每个节点都包含一个 “目标”,这是一个唯一标识它的 CSS 选择器
// 如果页面涉及 iframe 或影子 DOM,它可能是一系列 CSS 选择器
node.getTarget().toString()
)))
.collect(Collectors.toList());
}
}

使用测试夹具进行通用 axe 配置

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

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

以下示例展示了如何从测试运行器示例中的 TestFixtures 类扩展出一个新的夹具,该夹具包含一些通用的 AxeBuilder 配置。

创建夹具

此示例夹具创建一个 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");
}
}

使用夹具

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

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

AxeResults accessibilityScanResults = makeAxeBuilder()
// 自动使用共享的 AxeBuilder 配置,
// 但也支持特定于测试的其他配置
.include("#specific-element-under-test")
.analyze();

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

有关自动初始化 Playwright 对象等内容,请参阅实验性的JUnit 集成