aptpod Tech Blog

株式会社アプトポッドのテクノロジーブログです

PlaywrightのAria snapshotsでアクセシビリティツリーをチェックする

aptpod Advent Calendar 2024 12月12日の記事です。

こんにちは、開発本部Visual M2Mグループの遠藤です。フロントエンドエンジニアとして働いています。

Playwright 1.49.0からAria snapshotsという機能が新たに追加されました。この記事では、PlaywrightのAria snapshotsを実際に試しながら機能の紹介を行いたいと思います。

Aria snapshots とは

PlaywrightのAria snapshotsでは、ページのアクセシビリティツリーをYAML形式の表現として提供します。提供されたツリーのスナップショットを保存することで、ページの変更時に差分を比較し、ページ構造が期待通りであるかをチェックすることができます。

たとえば次のようなHTML構造があるとします。

<h1>Title</h1>
<h2>SubTitle</h2>

こちらは次のYAML形式で表現することができます。

- heading "Title" [level=1]
- heading "SubTitle" [level=2]

アクセシビリティツリーについて

アクセシビリティツリーはUI構造を表すアクセス可能なオブジェクトのツリーです。DOMやCSSOMに基づいて生成され、スクリーンリーダーや他の支援技術が情報をユーザーに提供するための基盤となります。

Chrome DevToolsを利用することでどのようなアクセシビリティツリーが生成されているかを確認することもできます。たとえば前述したHTML構造は、Chrome DevToolsで次のようなアクセシビリティツリーになっていることを確認できます。

Aria snapshots を使ってみる

ここからは実際にAria snapshotsを試しながら機能を確認していきたいと思います。

インストール

次のコマンドでPlaywrightのインストールを行います。

$ npm init playwright@latest

もしくはVS CodeのPlaywright拡張機能を使用して作業を行うこともできます。

基本的な使い方

まずはじめに次のHTML構造を想定してください。

<header id="header">
  <h1>Title</h1>
</header>

次に、Playwrightのテストコードを書いてみます。localhost:5500は上記のHTMLをホストしているローカルサーバーを想定しています。

Aria snapshotsでは、toMatchAriaSnapshot()メソッドを使用することで、locatorの範囲内のアクセシビリティツリーと、引数のアクシビリティツリーを比較することができます。

tests/example.spec.tsを次のように書き換えてください。

import { test, expect } from "@playwright/test";

test("test example", async ({ page }) => {
  await page.goto("http://localhost:5500/");
  await expect(page.locator("#header")).toMatchAriaSnapshot(`
  - banner:
    - heading "Title" [level=1]
  `);
});

次のコマンドを実行してみましょう。上記のテストは成功します。

$ npx playwright test

Running 1 test using 1 worker
  1 passed (525ms)

次にHTMLファイルに次のような変更を加えてみましょう。

  • <h1>タグを<h2>タグに変更
  • テキストを"Title"から"SubTitle"に変更
<header id="header">
  <h2>SubTitle</h2>
</header>

再度テストを実行してみましょう。テストが失敗すれば期待通りです。

$ npx playwright test

Running 1 test using 1 worker
  1) [chromium] › example.spec.ts:3:5 › test example ───────────────────────────────────────────────

    Error: Timed out 5000ms waiting for expect(locator).toMatchAriaSnapshot(expected)

    Locator: locator('#header')
    - Expected  - 1
    + Received  + 1

      - banner:
    -   - heading "Title" [level=1]
    +   - heading "SubTitle" [level=2]

テストの修正

与えられたエラーメッセージを元に、テストを修正して、再度実行してみましょう。

import { test, expect } from "@playwright/test";

test("test example", async ({ page }) => {
  await page.goto("http://localhost:5500/");
  await expect(page.locator("#header")).toMatchAriaSnapshot(`
  - banner:
    - heading "SubTitle" [level=2]
  `);
});

今度は成功しました。

$ npx playwright test

Running 1 test using 1 worker
  1 passed (475ms)

部分一致と正規表現を使用したマッチング

部分一致や正規表現を利用することで、より柔軟なテストが可能となります。

たとえば、以下のようにアクセブルな名前や属性を省略することで、部分一致を適用することができます。

import { test, expect } from "@playwright/test";

test("test example", async ({ page }) => {
  await page.goto("http://localhost:5500/");
  await expect(page.locator("#header")).toMatchAriaSnapshot(`
  - banner:
    - heading
  `);
});

上記は、header > headingという構造を持つHTML要素を期待します。この場合、heading要素のレベルやテキストは無視されます。つまり要素がh1でもh2でも、またテキストがTitleでもSubTitleでも、テストは成功します。

HTML構造を変更せずに再度テストを実行してみましょう。テストが成功することを確認できます。

$ npx playwright test

Running 1 test using 1 worker
  1 passed (475ms)

次のように正規表現を使用することで、動的なコンテンツに対してテストを行うこともできます。

test("test example", async ({ page }) => {
  await page.goto("http://localhost:5500/");
  await expect(page.locator("#header")).toMatchAriaSnapshot(`
    - banner:
      - heading /.*Title$/
  `);
});

スナップショットの生成と更新

スナップショットファイルの生成や更新は--update-snapshotsオプションを使用して行うことができます。

一度、HTML構造とテストファイルを次の状態に戻します。

<header id="header">
  <h2>SubTitle</h2>
</header>
test("test example", async ({ page }) => {
  await page.goto("http://localhost:5500/");
  await expect(page.locator("#header")).toMatchAriaSnapshot(`
  - banner:
    - heading "Title" [level=1]
  `);
});

この状態でnpx playwright testを行なうと、テストは失敗します。したがって先ほどは、HTML構造を自分で書き換えることで、テストを成功させました。今度は--update-snapshotsオプションを付与して実行してみます。

$ npx playwright test --update-snapshots

Running 1 test using 1 worker

New baselines created for:

  tests/example.spec.ts

  git apply test-results/rebaselines.patch
  1 passed (5.5s)

今回はテストが成功し、test-results/rebaselines.patchにパッチファイルが生成されました。

diff --git a/tests/example.spec.ts b/tests/example.spec.ts
--- a/tests/example.spec.ts
+++ b/tests/example.spec.ts
@@ -3,7 +3,7 @@
 test("test example", async ({ page }) => {
   await page.goto("http://localhost:5500/");
   await expect(page.locator("#header")).toMatchAriaSnapshot(`
-  - banner:
-    - heading "Title" [level=1]
+    - banner:
+      - heading "SubTitle" [level=2]
   `);
 });

git applyを使うことでパッチファイルの内容をtests/example.spec.tsに適用することができます。

$ git apply test-results/rebaselines.patch

tests/example.spec.tsを確認してみると、下記のように差分が適用されていることを確認できます。

import { test, expect } from "@playwright/test";

test("test example", async ({ page }) => {
  await page.goto("http://localhost:5500/");
  await expect(page.locator("#header")).toMatchAriaSnapshot(`
    - banner:
      - heading "SubTitle" [level=2]
  `);
});

また、toMatchAriaSnapshot()に空文字列を渡すことで、その場でスナップショットを生成することもできます。この方法を利用して初回テスト実行時に、簡単にスナップショットを生成することができます。

import { test, expect } from "@playwright/test";

test("test example", async ({ page }) => {
  await page.goto("http://localhost:5500/");
  await expect(page.locator("#header")).toMatchAriaSnapshot("");
});

上記の状態で--update-snapshotsを付与したテストを実行してみましょう。

$ npx playwright test --update-snapshots

test-results/rebaselines.patchに次のパッチファイルが生成されます。

diff --git a/tests/example.spec.ts b/tests/example.spec.ts
--- a/tests/example.spec.ts
+++ b/tests/example.spec.ts
@@ -2,5 +2,8 @@

 test("test example", async ({ page }) => {
   await page.goto("http://localhost:5500/");
-  await expect(page.locator("#header")).toMatchAriaSnapshot("");
+  await expect(page.locator("#header")).toMatchAriaSnapshot(`
+    - banner:
+      - heading "SubTitle" [level=2]
+  `);
 });

ariaSnapshot()メソッド

ariaSnapshot()メソッドは、指定されたlocatorの範囲内のスナップショットをYAML形式で出力します。これは、テスト実行中にスナップショットを動的に生成したい場合などで役に立ちます。

const snapshot = await page.locator("#header").ariaSnapshot();
console.log(snapshot);
// - banner:
//   - heading "SubTitle" [level=2]

おわりに

UIテストはその性質上、非常に繊細で難しい作業です。画面の構造や見た目の変化はアプリケーションの成長とともに頻繁に発生します。一方でテストコードのメンテナンスや、意図しない変更の検出にはなるべく労力をかけたくないものです。

そんな中、PlaywrightのAria snapshotsは、アクセシビリティツリーというアプローチでUIの構造を簡潔かつ柔軟にテストできる点で有用に感じました。YAML形式で視覚的にわかりやすくページの構造を記述できることもメリットの一つです。

今後も、実際のプロジェクトに適した方法を模索しながら、製品の品質向上につなげていければと思います。

参考文献