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

アクセシビリティテスト

はじめに

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

これにより検出できる問題の例をいくつか示します。

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

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

免責事項

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

手動評価には、無料のオープンソース開発ツールである 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 クラスでビルダーパターンを使用することで指定できます。

たとえば、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([]);
});

問題の要素が多くのページで繰り返し使用されている場合は、テストフィクスチャの使用を検討して、同じ 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([]);
});