メインコンテンツにスキップ

アクセシビリティ・テスト

はじめに

Playwright を使って、アプリケーションのさまざまな種類のアクセシビリティ問題をテストすることができます。

この機能が検出できる問題の例をいくつか示します。

  • 背景との色のコントラストが悪く、視覚障がいのあるユーザーには読みにくいテキスト
  • スクリーンリーダーが識別できないラベルのないUIコントロールやフォーム要素
  • 支援技術を混乱させる可能性のある、IDが重複しているインタラクティブ要素

以下の例は、Playwright テストの一部として axe アクセシビリティ・テスト・エンジンの実行をサポートする @axe-core/playwright パッケージに依存しています。

免責事項

自動アクセシビリティ・テストは、プロパティの欠落や無効なプロパティなど、一般的なアクセシビリティ問題を検出できます。しかし、多くのアクセシビリティ問題は手動テストによってのみ発見できます。自動テスト、手動によるアクセシビリティ評価、およびインクルーシブなユーザーテストの組み合わせを使用することをお勧めします。

手動評価には、WCAG 2.1 AA のカバレッジについてウェブサイトを評価するのに役立つ、無料のオープンソース開発ツールである Accessibility Insights for Web をお勧めします。

アクセシビリティテストの例

アクセシビリティ・テストは、他の 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() を呼び出す前に 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 の自動チェックには、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-coreがサポートするルールタグの完全なリストは、axe APIドキュメントの「Axe-core Tags」セクションで見つけることができます。

既知の課題の対処

アプリケーションにアクセシビリティテストを追加する際によくある質問は「既知の違反をどうやって抑制するか?」です。以下の例は、使用できるいくつかの手法を示しています。

スキャンから個々の要素を除外する

アプリケーションに既知の課題がある特定の要素がいくつか含まれている場合、課題を修正できるまでそれらをスキャンから除外するために、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([]);
});

対象の要素が多くのページで繰り返し使用されている場合は、共通の axe 設定のためにテストフィクスチャを使用することを検討し、複数のテストで同じ AxeBuilder 設定を再利用してください。

個別のスキャンルールの無効化

アプリケーションに特定のルールに対する既存の違反が多数ある場合、問題を修正できるまで、AxeBuilder.disableRules() を使用して個々のルールを一時的に無効にすることができます。

disableRules() に渡すルールIDは、抑制したい違反の 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 設定を共有するのに良い方法です。これが役立つシナリオには、以下のようなものがあります。

  • すべてのテストで共通のルールセットを使用する
  • 異なる多くのページに現れる共通要素における既知の違反を抑制する
  • 多くのスキャンに対して、一貫してスタンドアロンのアクセシビリティレポートを添付する

以下の例は、これらのシナリオをすべてカバーするテストフィクスチャの作成と使用方法を示しています。

フィクスチャの作成

このフィクスチャの例は、共有の withTags() および exclude() 設定が事前構成された AxeBuilder オブジェクトを作成します。

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