可访问性测试
简介
Playwright 可用于测试应用程序是否存在多种类型的可访问性问题。
此功能可以捕获的一些问题的示例包括
- 由于与背景颜色对比度差,视力障碍用户难以阅读的文本
- 屏幕阅读器无法识别的无标签 UI 控件和表单元素
- 具有重复 ID 的交互式元素,这可能会使辅助技术感到困惑
以下示例依赖于 com.deque.html.axe-core/playwright
Maven 包,该包支持在 Playwright 测试中运行 axe 可访问性测试引擎。
免责声明
自动化的可访问性测试可以检测一些常见可访问性问题,例如缺少或无效的属性。但是,许多可访问性问题只能通过手动测试才能发现。我们建议结合使用自动化测试、手动可访问性评估和包容性用户测试。
对于手动评估,我们建议使用 Accessibility Insights for Web,这是一个免费的开源开发工具,可引导您评估网站的 WCAG 2.1 AA 覆盖范围。
可访问性测试示例
可访问性测试与任何其他 Playwright 测试的工作原理相同。您可以为其创建单独的测试用例,或将可访问性扫描和断言集成到现有的测试用例中。
以下示例演示了一些基本的可访问性测试场景。
示例 1:扫描整个页面
此示例演示如何测试整个页面是否存在可自动检测到的可访问性违规行为。测试
- 导入
com.deque.html.axe-core/playwright
包 - 使用普通的 JUnit 5
@Test
语法定义测试用例 - 使用普通的 Playwright 语法打开浏览器并导航到被测页面
- 调用
AxeBuilder.analyze()
对页面运行可访问性扫描 - 使用普通的 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()
@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 规则;要匹配该行为,您将使用标签 wcag2a
、wcag2aa
、wcag21a
和 wcag21aa
。
AxeResults accessibilityScanResults = new AxeBuilder(page)
.withTags(Arrays.asList("wcag2a", "wcag2aa", "wcag21a", "wcag21aa"))
.analyze();
assertEquals(Collections.emptyList(), accessibilityScanResults.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-core
的文档中找到 axe 规则的完整列表。
AxeResults accessibilityScanResults = new AxeBuilder(page)
.disableRules(Arrays.asList("duplicate-id"))
.analyze();
assertEquals(Collections.emptyList(), accessibilityScanResults.getViolations());
使用违规指纹来指定已知问题
如果您希望允许更细粒度的已知问题集,可以使用以下模式
- 执行预期会发现一些已知违规行为的可访问性扫描
- 将违规行为转换为“违规指纹”对象
- 断言指纹集等效于预期指纹集
此方法避免了使用 AxeBuilder.exclude()
的缺点,但代价是复杂性和脆弱性略有增加。
以下是如何仅根据规则 ID 和指向每个违规行为的“目标”选择器使用指纹的示例
@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
配置的好方法。一些可能对这种情况有用的场景包括
- 在所有测试中使用一组通用的规则
- 抑制出现在许多不同页面中的常见元素中的已知违规行为
- 为许多扫描一致地附加独立的可访问性报告
以下示例演示了如何使用包含一些常见 AxeBuilder
配置的新夹具扩展 测试运行器示例 中的 TestFixtures
类。
创建夹具
此示例夹具创建了一个 AxeBuilder
对象,该对象预先配置了共享的 withTags()
和 exclude()
配置。
class AxeTestFixtures extends TestFixtures {
AxeBuilder makeAxeBuilder() {
return new AxeBuilder(page)
.withTags(['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()
// 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 对象等。