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

アクセシビリティテスト

はじめに

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 クラスでビルダーパターンを使用して指定できます。

たとえば、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 ルールのみが含まれています。その動作に一致させるには、wcag2awcag2aawcag21a、および 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-core がサポートするルールタグの完全なリストは、axe API ドキュメントの「Axe-core タグ」セクション にあります。

既知の問題の処理

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

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

アプリケーションに既知の問題のある特定の要素がいくつか含まれている場合は、AxeBuilder.exclude() を使用して、問題を修正できるようになるまでスキャン対象から除外できます。

これは通常、最も簡単なオプションですが、いくつかの重要な欠点があります。

  • exclude() は、指定された要素とそのすべての子孫を除外します。多くの子を含むコンポーネントで使用することは避けてください。
  • exclude() は、既知の問題に対応するルールだけでなく、指定された要素に対してすべてのルールが実行されるのを防ぎます。

これは、特定のテストで 1 つの要素をスキャンから除外する例です。

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 のスニペットなど、問題の要素の実装の詳細が含まれています。これらをスナップショットに含めると、関係のない理由で問題のコンポーネントの 1 つが変更されるたびにテストが壊れやすくなります。

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