Playwrightで楽にアクセシビリティのチェックをする

# はじめに

Playwrightの公式を読み漁っていた際に`Accessibility testing`の項目を見つけまして、実際にプロジェクトに導入してみたのと、調べたことをアウトプットします。

:::note warn
私はアクセシビリティ浅瀬にも到達できていない砂場ザッザッ勢です
:::

https://playwright.dev/docs/accessibility-testing

# @axe-core/playwright

Playwightの公式では`@axe-core/playwright`を推奨しています。
Axe Accessibility Testing ToolsをPlaywrightテストの一部として使用できるライブラリとのことです。
便利ですね。

# Axe、axe-coreとは

Axe、axe-coreとは何なのかGoogle先生に聞いてみました。

> HTML向けのアクセシビリティテストツール。 プラガブルな作りで、Lighthouseのアクセシビリティテストをする時にも使われている。 Axeのコアとなる機能はaxe-coreとしてOSS化されている。

なるほど、初めて知りました。
axe-coreとしてOSS化されていて、それをplaywrightで使用できるようにしたものが`@axe-core/playwright`というわけですね。
Lighthouseのアクセシビリティテストに使われているのは非常に信頼性が高いと思いました。

https://github.com/dequelabs/axe-core

# Axeについてもう少し深掘りします

## そもそもAxeでチェックできるアクセシビリティのルールって？

こちらのREADMEにまとまっていました。

https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md

## Axeのチェックがパスすればアクセシビリティは問題ないの？
そんなことはないです。
デジタル庁のウェブアクセシビリティ導入ガイドラインにも記載がある通り、ツールで機械的に確認できるものには限りがあります。

> ツールで機械的に確認できるのは達成基準の 2割から 3割程度にとどまります。また、ツールによっては開発から時間も経過し、最新のウェブテクノロジーを採用して実装されたページのテストが困難な場合もあります。このため、試験は必ず人が目視とスクリーンリーダーで行う必要があります。

20ページ目参照

https://www.digital.go.jp/assets/contents/node/basic_page/field_ref_resources/08ed88e1-d622-43cb-900b-84957ab87826/377fe31d/20221212_introduction_to_weba11y.pdf

なので、明確に基準A、AAを満たしています！と言えるのかというとそうではないという理解をしています。
ただ、プロジェクトを開発する上で`axe-coreで担保できる部分まではテストで担保する`という落とし所はありなのかなとも思います。
この辺りはすごく難しい話だなと物凄く感じました...

## じゃあAxeでチェックできないルールを把握しておく必要があるよね

把握しておく必要がありますよね。
調べてみましたがなかなかヒットせず、WCAGのガイドラインから差分を抽出する元気もなかった為(言い訳です)ChatGPT大先生に質問しました。

> Q.  
> axe-core/playwrightでチェックしきれないアクセシビリティのルールを教えてください。

> A.
> Axe-coreやPlaywrightなどの自動化ツールは多くのアクセシビリティルールをチェックしますが、一部のルールは自動化ツールでは完全に検出できない場合があります。
> 1. コンテンツの意味論的な適切さ：自動化ツールはHTML構造を分析しますが、コンテンツの意味や文脈に関する詳細なチェックは難しいです。
> 2. ユーザー体験に関する規則：例えば特定の色のコントラストが低い場合に警告を出すルールなど、デザインの身的側面や実際の利用状況に関連する規則は自動化ツールでは限界があります。
> 3. 一部の動的なコンテンツ：ユーザーの操作や状況に応じて変化するコンテンツは、自動化ツールが確実にチェックするのが難しいことがあります。
> 4. 複雑なウェブアプリケーション内のアクセシビリティ：動的なコンポーネントや複雑なUIパターンは、完全に自動化されたテストでは全てのアクセシビリティ問題を捉えきれないことがあります。

まあ、そうだよねと。
コンテンツの意味論的な適切さ、UXなどセマンティック含めたアクセシビリティの適切さはデザインレベルまで落ちるので、`あくまで実装レベルの品質担保という点`では`@axe-core/playwright`で十分だという結論になりました。

https://waic.jp/guideline/#:~:text=%E5%85%AC%E5%85%B1%E3%83%BB%E6%B0%91%E9%96%93%E3%82%92%E5%95%8F%E3%82%8F%E3%81%9A,%E3%81%8C%E5%90%AB%E3%81%BE%E3%82%8C%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99%E3%80%82

# 実装

さて、ここまでつらつらと調べたことを書いてきましたが、ここからは導入方法とテストコードについて記載していきます。
公式に書いていると言えばそうですが、念の為です(？)

## @axe-core/playwrightの導入

```
npm i -D @axe-core/playwright
```

## テストコード
```
import AxeBuilder from '@axe-core/playwright';

test('Run HomePage E2E Test', async ({ page }) => {
  await test.step('Test accessibility', async () => {
    const accessibilityScanResults = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa',])
      .analyze();
    await expect(accessibilityScanResults.violations).toEqual([]);
  });
});
```
これだけで前述したアクセシビリティのルールをチェックしてくれます。
`expect(accessibilityScanResults.violations).toEqual([]);`の部分でルールに違反しているかを確認しています。

解析後に返ってくるデータの`accessibilityScanResults`の中身をlog出力してデータ構造を見てみます。
valueは削除していますが、データ構造は以下です。
```
{
  testEngine: { name: },
  testRunner: { name: },
  testEnvironment: {
    userAgent:
    windowWidth:
    windowHeight:
    orientationAngle:
    orientationType:
  },
  timestamp:　,
  url: ,
  toolOptions: { runOnly: { type: }, reporter: },
  inapplicable: [
    {
      id: 'area-alt',
      impact: null,
      tags: [Array],
      description: 'Ensures <area> elements of image maps have alternate text',
      help: 'Active <area> elements must have alternate text',
      helpUrl: 'https://dequeuniversity.com/rules/axe/4.8/area-alt?application=playwright',
      nodes: []
    },
    ...
  ],
  passes: [
    {
      id: 'aria-deprecated-role',
      impact: null,
      tags: [Array],
      description: 'Ensures elements do not use deprecated roles',
      help: 'Deprecated ARIA roles must not be used',
      helpUrl: 'https://dequeuniversity.com/rules/axe/4.8/aria-deprecated-role?application=playwright',
      nodes: [Array]
    }
    ...
  ],
  incomplete: [],
  ⭐️violations: []
}

```
ルール違反が存在した場合、⭐️の部分に違反内容が格納され、`await expect(accessibilityScanResults.violations).toEqual([])`のテストコードに引っかかるという流れなります。
では実際にエラーを出してみましょう。
```
    Error: expect(received).toEqual(expected) // deep equality
    
    - Expected  -  1
    + Received  + 82
    - Array []
    + Array [
    +   Object {
    +     "description": "Ensures each HTML document contains a non-empty <title> element",
    +     "help": "Documents must have <title> element to aid in navigation",
    +     "helpUrl": "https://dequeuniversity.com/rules/axe/4.8/document-title?application=playwright",
    +     "id": "document-title",
    +     "impact": "serious",
    +     "nodes": Array [
    +       Object {
    +         "all": Array [],
    +         "any": Array [
    +           Object {
    +             "data": null,
    +             "id": "doc-has-title",
    +             "impact": "serious",
    +             "message": "Document does not have a non-empty <title> element",
    +             "relatedNodes": Array [],
    +           },
    +         ],
    +         "failureSummary": "Fix any of the following:
    +   Document does not have a non-empty <title> element",
    +         "html": "<html><head></head><body></body></html>",
    +         "impact": "serious",
    +         "none": Array [],
    +         "target": Array [
    +           "html",
    +         ],
    +       },
    +     ],
    +     "tags": Array [
    +       "cat.text-alternatives",
    +       "wcag2a",
    +       "wcag242",
    +       "TTv5",
    +       "TT12.a",
    +       "EN-301-549",
    +       "EN-9.2.4.2",
    +       "ACT",
    +     ],
    +   },
    +   Object {
    +     "description": "Ensures every HTML document has a lang attribute",
    +     "help": "<html> element must have a lang attribute",
    +     "helpUrl": "https://dequeuniversity.com/rules/axe/4.8/html-has-lang?application=playwright",
    +     "id": "html-has-lang",
    +     "impact": "serious",
    +     "nodes": Array [
    +       Object {
    +         "all": Array [],
    +         "any": Array [
    +           Object {
    +             "data": Object {
    +               "messageKey": "noLang",
    +             },
    +             "id": "has-lang",
    +             "impact": "serious",
    +             "message": "The <html> element does not have a lang attribute",
    +             "relatedNodes": Array [],
    +           },
    +         ],
    +         "failureSummary": "Fix any of the following:
    +   The <html> element does not have a lang attribute",
    +         "html": "<html><head></head><body></body></html>",
    +         "impact": "serious",
    +         "none": Array [],
    +         "target": Array [
    +           "html",
    +         ],
    +       },
    +     ],
    +     "tags": Array [
    +       "cat.language",
    +       "wcag2a",
    +       "wcag311",
    +       "TTv5",
    +       "TT11.a",
    +       "EN-301-549",
    +       "EN-9.3.1.1",
    +       "ACT",
    +     ],
    +   },
    + ]
```
というような感じでエラー内容が配列に格納され`空配列`と`空ではない配列`の比較となりテストに失敗します。
簡単ですね。

# 終わりに
Playwrightで楽にアクセシビリティのチェックができることが分かりました。
@axe-core/playwrightを導入することによって、CIで自動化することもできますし、導入する価値は十分にあると考えています。
ただ、過信はしすぎず`実装レベルの品質担保`という線引きは必ず必要になってきそうです。
以上です。