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形式で視覚的にわかりやすくページの構造を記述できることもメリットの一つです。
今後も、実際のプロジェクトに適した方法を模索しながら、製品の品質向上につなげていければと思います。