Skip to main content

無障礙測試

簡介

Playwright 可以用來測試應用程式的多種類型的無障礙問題。

一些此方法可以捕捉到的問題範例如下:

  • 由於與背景的顏色對比差,視障用戶難以閱讀的文字
  • 屏幕閱讀器無法識別的無標籤 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 assertions 來驗證返回的掃描結果中沒有違規
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() 之前使用 Locators 與頁面互動:

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 Content Accessibility Guidelines (WCAG)中的特定成功標準,其他則是未被任何 WCAG 標準特別要求的“最佳實踐”規則。

你可以使用 AxeBuilder.withTags() 將無障礙掃描限制為僅執行那些被“標記”為對應特定 WCAG 成功標準的規則。例如,Accessibility Insights for Web's 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([]);
});

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

如果你希望允許更細緻的一組已知問題,你可以使用 Snapshots 來驗證一組預先存在的違規行為是否未改變。這種方法避免了使用 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 配置

Test fixtures 是在多個測試中共享常見 AxeBuilder 配置的一種好方法。一些可能有用的情境包括:

  • 在所有測試中使用一組通用規則
  • 抑制在許多不同頁面中出現的常見元素中的已知違規
  • 一致地附加獨立的無障礙報告以進行多次掃描

以下範例展示了建立和使用涵蓋每個這些情境的測試裝置。

建立一個 fixture

此範例裝置建立一個 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, testInfo) => {
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';

使用固定裝置

要使用這個 fixture,將之前範例中的 new AxeBuilder({ page }) 替換為新定義的 makeAxeBuilder fixture:

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([]);
});