Skip to main content

無障礙測試

介紹

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

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

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

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

免責聲明

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

若進行手動評估,我們推薦使用 Accessibility Insights for Web,這是一套免費開源的開發工具,可引導您評估網站對 WCAG 2.1 AA 的涵蓋程度。

無障礙測試範例

無障礙測試的運作方式與一般的 Playwright 測試相同。您可以為它們建立獨立的測試案例,或將無障礙掃描與斷言整合到既有的測試中。

以下示範幾個基本的無障礙測試情境。

掃描整個頁面

此範例示範如何在整個頁面上執行可自動偵測的無障礙違規檢查。測試會:

  1. 匯入 @axe-core/playwright 套件
  2. 使用一般的 Playwright Test 語法定義測試案例
  3. 使用一般的 Playwright 語法導覽至受測頁面
  4. 等待 AxeBuilder.analyze() 對頁面執行無障礙掃描
  5. 使用一般的 Playwright Test 斷言 驗證回傳的掃描結果中沒有違規
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright'; // 1

test.describe('homepage', () => { // 2
test('should not have any automatically detectable accessibility issues', async ({ page }) => {
await page.goto('https://your-site.com/'); // 3

const accessibilityScanResults = await new AxeBuilder({ page }).analyze(); // 4

expect(accessibilityScanResults.violations).toEqual([]); // 5
});
});

設定 axe 只掃描頁面的特定區域

@axe-core/playwright 支援多種 axe 設定。您可以使用 AxeBuilder 類別搭配 Builder 模式來指定這些選項。

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

呼叫 AxeBuilder.analyze() 時會以頁面「當下狀態」進行掃描。若需掃描透過 UI 互動後才會顯示的區塊,請在呼叫 analyze() 之前先用定位器與頁面互動:

test('navigation menu should not have automatically detectable accessibility violations', async ({
page,
}) => {
await page.goto('https://your-site.com/');

await page.getByRole('button', { name: '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.
await page.locator('#navigation-menu-flyout').waitFor();

const accessibilityScanResults = await new AxeBuilder({ page })
.include('#navigation-menu-flyout')
.analyze();

expect(accessibilityScanResults.violations).toEqual([]);
});

掃描 WCAG 違規

預設情況下,axe 會檢查多種無障礙規則。其中有些對應到 Web 內容無障礙指南(WCAG) 的特定成功準則,另外一些則屬於非 WCAG 強制、但建議遵循的「最佳實務」規則。

您可以使用 AxeBuilder.withTags() 將掃描限制為只執行「標記為」某些 WCAG 成功準則的規則。例如,Accessibility Insights for Web 的 Automated Checks 只包含用於檢測 WCAG A 與 AA 成功準則違規的 axe 規則;若要符合該行為,請使用標記 wcag2awcag2aawcag21awcag21aa

請注意,自動化測試無法偵測到所有類型的 WCAG 違規。

test('should not have any automatically detectable WCAG A or AA violations', async ({ page }) => {
await page.goto('https://your-site.com/');

const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();

expect(accessibilityScanResults.violations).toEqual([]);
});

您可以在 axe API 文件的「Axe-core Tags」章節 找到 axe-core 支援的規則標記完整清單。

處理已知問題

在為應用程式加入無障礙測試時,常見問題是「要如何暫時抑制已知的違規?」以下示範幾種可用技巧。

從掃描中排除個別元素

若應用程式中有少量具已知問題的特定元素,您可以使用 AxeBuilder.exclude() 將它們自掃描中排除,直到有時間修復為止。

這通常是最簡單的作法,但也有幾個重要缺點:

  • exclude() 會排除指定元素與其「所有子孫」。若該元件包含許多子元素,請避免使用。
  • exclude() 會阻止「所有」規則在指定元素上執行,而不僅是與已知問題相關的規則。

以下範例示範在單一測試中排除某個元素不被掃描:

test('should not have any accessibility violations outside of elements with known issues', async ({
page,
}) => {
await page.goto('https://your-site.com/page-with-known-issues');

const accessibilityScanResults = await new AxeBuilder({ page })
.exclude('#element-with-known-issue')
.analyze();

expect(accessibilityScanResults.violations).toEqual([]);
});

如果該元素在許多頁面重複使用,考慮使用測試佈置 讓多個測試共用相同的 AxeBuilder 設定。

停用單一掃描規則

若應用程式中存在許多某特定規則的既有違規,您可以使用 AxeBuilder.disableRules() 暫時停用單一規則,直到您修復問題為止。

可在您想抑制之違規項目的 id 屬性中找到要傳給 disableRules() 的規則 ID。axe-core 文件中有完整的 axe 規則清單

test('should not have any accessibility violations outside of rules with known issues', async ({
page,
}) => {
await page.goto('https://your-site.com/page-with-known-issues');

const accessibilityScanResults = await new AxeBuilder({ page })
.disableRules(['duplicate-id'])
.analyze();

expect(accessibilityScanResults.violations).toEqual([]);
});

使用快照允許特定已知問題

若您需要更細緻地允許某些已知問題,可使用快照 驗證既有違規集合沒有變動。此作法可避免 AxeBuilder.exclude() 的缺點,但代價是稍微增加複雜度且較脆弱。

請勿直接對整個 accessibilityScanResults.violations 陣列做快照,因為其中包含元素的實作細節(例如渲染後的 HTML 片段)。若將這些內容納入快照,當相關元件因無關原因變動時,您的測試將更容易失敗:

// Don't do this! This is fragile.
expect(accessibilityScanResults.violations).toMatchSnapshot();

建議改為建立違規項目的「指紋」物件,只保留足以唯一識別問題的資訊,並對該指紋做快照:

// This is less fragile than snapshotting the entire violations array.
expect(violationFingerprints(accessibilityScanResults)).toMatchSnapshot();

// my-test-utils.js
function violationFingerprints(accessibilityScanResults) {
const violationFingerprints = accessibilityScanResults.violations.map(violation => ({
rule: violation.id,
// These are CSS selectors which uniquely identify each element with
// a violation of the rule in question.
targets: violation.nodes.map(node => node.target),
}));

return JSON.stringify(violationFingerprints, null, 2);
}

將掃描結果匯出為測試附件

大多數無障礙測試主要關注 axe 掃描結果中的 violations 屬性。不過,掃描結果不僅只有 violations。例如,結果也包含通過的規則,以及 axe 對某些規則判定結果不明確的元素資訊。這些資訊對於除錯「未如預期偵測到所有違規」的測試很有幫助。

若要在測試輸出中完整保留掃描結果以利除錯,您可以使用 testInfo.attach() 將掃描結果作為測試附件附加上去。之後報告器 就能在測試輸出中嵌入或連結完整結果。

以下範例示範如何把掃描結果附加到測試:

test('example with attachment', async ({ page }, testInfo) => {
await page.goto('https://your-site.com/');

const accessibilityScanResults = await new AxeBuilder({ page }).analyze();

await testInfo.attach('accessibility-scan-results', {
body: JSON.stringify(accessibilityScanResults, null, 2),
contentType: 'application/json'
});

expect(accessibilityScanResults.violations).toEqual([]);
});

使用測試佈置共用 axe 設定

測試佈置 是在多個測試間共用 AxeBuilder 設定的好方法。適用情境包含:

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

以下範例示範如何建立並使用一個涵蓋上述情境的測試佈置。

建立佈置

此佈置會建立一個 AxeBuilder 物件,並預先設定共用的 withTags()exclude()

axe-test.ts
import { test as base } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

type AxeFixture = {
makeAxeBuilder: () => AxeBuilder;
};

// Extend base test by providing "makeAxeBuilder"
//
// This new "test" can be used in multiple test files, and each of them will get
// a consistently configured AxeBuilder instance.
export const test = base.extend<AxeFixture>({
makeAxeBuilder: async ({ page }, use) => {
const makeAxeBuilder = () => new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.exclude('#commonly-reused-element-with-known-issue');

await use(makeAxeBuilder);
}
});
export { expect } from '@playwright/test';

使用佈置

若要使用此佈置,請將先前範例中的 new AxeBuilder({ page }) 改為使用新定義的 makeAxeBuilder 佈置:

const { test, expect } = require('./axe-test');

test('example using custom fixture', async ({ page, makeAxeBuilder }) => {
await page.goto('https://your-site.com/');

const accessibilityScanResults = await makeAxeBuilder()
// Automatically uses the shared AxeBuilder configuration,
// but supports additional test-specific configuration too
.include('#specific-element-under-test')
.analyze();

expect(accessibilityScanResults.violations).toEqual([]);
});