无障碍测试
介绍
¥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
以下示例依赖于 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.
对于手动评估,我们推荐 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:
-
导入
com.deque.html.axe-core/playwright
包¥Imports the
com.deque.html.axe-core/playwright
package -
使用标准的 JUnit 5
@Test
语法定义测试用例¥Uses normal JUnit 5
@Test
syntax to define a test case -
使用标准的 Playwright 语法打开浏览器并导航到被测页面
¥Uses normal Playwright syntax to open a browser and navigate to the page under test
-
调用
AxeBuilder.analyze()
对页面运行可访问性扫描¥Invokes
AxeBuilder.analyze()
to run the accessibility scan against the page -
使用标准的 JUnit 5 测试断言来验证返回的扫描结果中是否存在违规行为
¥Uses normal JUnit 5 test assertions to verify that there are no violations in the returned scan results
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
类的构建器模式来指定这些选项。
¥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()
来限制辅助功能扫描仅针对页面的一个特定部分运行。
¥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()
:
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
默认情况下,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 成功标准的斧头规则;为了匹配该行为,你可以使用标签 wcag2a
、wcag2aa
、wcag21a
和 wcag21aa
。
¥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
.
¥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());
你可以在 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:
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
的文档中找到。
¥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:
-
执行可访问性扫描,预计会发现一些已知的违规行为。
¥Perform an accessibility scan which is expected to find some known violations
-
将违规行为转换为 "违规指纹" 对象
¥Convert the violations into "violation fingerprint" objects
-
断言指纹集与预期指纹集相同
¥Assert that the set of fingerprints is equivalent to the expected ones
这种方法避免了使用 AxeBuilder.exclude()
的缺点,但代价是稍微增加了复杂性和脆弱性。
¥This approach avoids the downsides of using AxeBuilder.exclude()
at the cost of slightly more complexity and fragility.
以下是一个仅基于规则 ID 和指向每个违规的 "target" 选择器使用指纹的示例:
¥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:
-
在所有测试中使用一组通用规则
¥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
以下示例演示了如何使用包含一些通用 AxeBuilder
配置的新 Fixture 从 测试运行器示例 扩展 TestFixtures
类。
¥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
此示例夹具创建一个 AxeBuilder
对象,该对象预先配置了共享 withTags()
和 exclude()
配置。
¥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.