無障礙測試
介紹
Playwright 可用於針對多種無障礙(Accessibility)問題測試您的應用程式。
以下是一些可自動偵測出的問題範例:
- 由於與背景色的對比不足,導致視覺障礙使用者難以閱讀的文字
- 沒有可被螢幕閱讀器辨識之標籤的 UI 控制項與表單元素
- 具有重複 ID 的互動元素,可能使輔助技術混淆
以下範例仰賴 @axe-core/playwright
套件,讓您能在 Playwright 測試中執行 axe 無障礙測試引擎。
自動化無障礙測試可偵測部分常見問題,例如缺少或無效的屬性。但仍有許多無障礙問題必須透過人工測試才能發現。我們建議結合自動化測試、手動無障礙評估,以及包容性的使用者測試。
若進行手動評估,我們推薦使用 Accessibility Insights for Web,這是一套免費開源的開發工具,可引導您評估網站對 WCAG 2.1 AA 的涵蓋程度。
無障礙測試範例
無障礙測試的運作方式與一般的 Playwright 測試相同。您可以為它們建立獨立的測試案例,或將無障礙掃描與斷言整合到既有的測試中。
以下示範幾個基本的無障礙測試情境。
掃描整個頁面
此範例示範如何在整個頁面上執行可自動偵測的無障礙違規檢查。測試會:
- 匯入
@axe-core/playwright
套件 - 使用一般的 Playwright Test 語法定義測試案例
- 使用一般的 Playwright 語法導覽至受測頁面
- 等待
AxeBuilder.analyze()
對頁面執行無障礙掃描 - 使用一般的 Playwright Test 斷言 驗證回傳的掃描結果中沒有違規
- TypeScript
- JavaScript
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
});
});
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default; // 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 規則;若要符合該行為,請使用標記 wcag2a
、wcag2aa
、wcag21a
與 wcag21aa
。
請注意,自動化測試無法偵測到所有類型的 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()
。
- TypeScript
- JavaScript
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';
const base = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;
// 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.
exports.test = base.test.extend({
makeAxeBuilder: async ({ page }, use) => {
const makeAxeBuilder = () => new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.exclude('#commonly-reused-element-with-known-issue');
await use(makeAxeBuilder);
}
});
exports.expect = base.expect;
使用佈置
若要使用此佈置,請將先前範例中的 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([]);
});