跳到主要內容

無障礙功能測試

簡介

Playwright 可用於測試您的應用程式是否存在多種類型的無障礙功能問題。

以下是一些可以捕捉到的問題範例:

  • 由於與背景的色彩對比度不佳,視力障礙使用者難以閱讀的文字
  • 沒有螢幕閱讀器可以識別的標籤的 UI 控制項和表單元素
  • 具有重複 ID 的互動式元素,可能會混淆輔助技術

以下範例依賴 @axe-core/playwright 套件,該套件增加了對執行 axe 無障礙功能測試引擎 作為 Playwright 測試一部分的支援。

免責聲明

自動化無障礙功能測試可以偵測到一些常見的無障礙功能問題,例如遺失或無效的屬性。但是,許多無障礙功能問題只能透過手動測試來發現。我們建議結合使用自動化測試、手動無障礙功能評估和包容性使用者測試。

對於手動評估,我們推薦 Accessibility Insights for Web,這是一個免費且開放原始碼的開發工具,可引導您評估網站的 WCAG 2.1 AA 涵蓋範圍。

無障礙功能測試範例

無障礙功能測試的工作方式與任何其他 Playwright 測試相同。您可以為它們建立個別的測試案例,或將無障礙功能掃描和斷言整合到您現有的測試案例中。

以下範例示範了一些基本的無障礙功能測試情境。

掃描整個頁面

此範例示範如何測試整個頁面是否存在可自動偵測到的無障礙功能違規。此測試:

  1. 匯入 @axe-core/playwright 套件
  2. 使用標準 Playwright 測試語法來定義測試案例
  3. 使用標準 Playwright 語法導航至受測頁面
  4. 等待 AxeBuilder.analyze() 以對頁面執行無障礙功能掃描
  5. 使用標準 Playwright 測試 斷言 來驗證傳回的掃描結果中沒有違規
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 Content Accessibility Guidelines (WCAG) 中的特定成功準則,而其他規則是並非任何 WCAG 準則明確要求的「最佳實務」規則。

您可以使用 AxeBuilder.withTags() 將無障礙功能掃描限制為僅執行那些「標記」為對應於特定 WCAG 成功準則的規則。例如,Accessibility Insights for Web 的自動檢查 僅包含測試 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 標籤」章節 中找到 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 規則的完整清單 可以在 axe-core 的文件中找到。

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, 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';

使用夾具

若要使用夾具,請將先前範例的 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([]);
});