Skip to main content

無障礙測試

介紹

Playwright 可用於針對多種無障礙(Accessibility)問題測試您的應用程式。

以下是一些可自動偵測出的問題範例:

  • 由於與背景色的對比不足,導致視覺障礙使用者難以閱讀的文字
  • 沒有可被螢幕閱讀器辨識之標籤的 UI 控制項與表單元素
  • 具有重複 ID 的互動元素,可能使輔助技術混淆

以下範例仰賴 com.deque.html.axe-core/playwright Maven 套件,讓您能在 Playwright 測試中執行 axe 無障礙測試引擎

免責聲明

自動化無障礙測試可偵測部分常見問題,例如缺少或無效的屬性。但仍有許多無障礙問題必須透過人工測試才能發現。我們建議結合自動化測試、手動無障礙評估,以及包容性的使用者測試。

若進行手動評估,我們推薦使用 Accessibility Insights for 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 類別搭配 Builder 模式來指定這些選項。

例如,可使用 AxeBuilder.include() 將無障礙掃描限制在頁面某個特定區域。

AxeBuilder.analyze() 呼叫時會以頁面「當下狀態」進行掃描。若需掃描透過 UI 互動後才會顯示的區塊,請在呼叫 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 的 Automated Checks 只包含用於檢測 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-core 文件中有完整的 axe 規則清單

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

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

使用違規指紋(fingerprints)允許特定已知問題

若您需要更細緻地允許某些已知問題,可以採用以下模式:

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

// 您可以視需求調整「指紋」的具體程度。以下將「同一元素上對應同一 Axe 規則的違規」視為相同。
//
// 使用 record 類型有助於用 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 設定的好方法。適用情境包含:

  • 在所有測試中使用共同的一組規則
  • 針對出現在多個頁面的共用元素,抑制已知違規
  • 持續為多次掃描附加獨立的無障礙報告

以下示範擴充測試執行器範例中的 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()
// 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 物件等能力。