Playwrightのaria snapshot(ARIAスナップショット)は、ページのアクセシビリティツリーをYAMLで書き出し、期待するYAMLと突き合わせるテスト手法です。アサーションはtoMatchAriaSnapshot()、YAMLを取り出すメソッドはariaSnapshot()で、どちらも1.49.0(2024年11月18日公開)で入りました。その後1.52で/childrenと/url、1.59でpage.ariaSnapshot()、1.63でariaSnapshotJSON()と機能が増えました。1.64(2026年10月7日公開)では--update-snapshotsの既定モードの名前とmissingの成否が変わっています。このページでは、Playwright 1.64.1で実行した結果をもとに、YAMLの読み方、照合ルール、スナップショットの生成と更新、CIでの更新モードの選び方、旧page.accessibility.snapshot()からの移行、axe-coreとの使い分けを順に説明します。
まとめ:aria snapshotの要点
- aria snapshotはアクセシビリティツリーのYAML表現です。1行が
- ロール "アクセシブルネーム" [属性]の形で、インデントが親子関係を表します。 - 照合は部分一致が既定です。テンプレートで省いた名前・属性・子要素は無視されます。ただし順序は区別され、子の過不足まで見たいときは
/children: equalかdeep-equalを書きます。 - テンプレートは
toMatchAriaSnapshot(``)と空で書き、npx playwright test -uで生成します。既定の更新方式はpatchなので、test-results/rebaselines.patchをgit applyするまでソースは書き換わりません。 -uを付けない実行は1.64からdefaultモードと呼ばれ、無いスナップショットを書き出したうえでテストを失敗させます。1.64以降は--update-snapshots=missingを指定すると、書き出したテストも合格になります。CIでファイルを書かせたくないときはnoneを指定します。page.accessibility.snapshot()は1.57で削除されました。ツリーの取得はariaSnapshot()、WCAG違反の検査はaxe-coreに置き換えます。- aria snapshotが検証するのは構造と名前です。コントラスト不足はaria snapshotの照合対象外です。ボタン名の欠落は期待する名前との照合でも検出できますが、名前の必須性などのルール検査にはaxe-coreを併用します。
aria snapshotが検証する対象:アクセシビリティツリー
アクセシビリティツリーとDOMツリーの違い
アクセシビリティツリーは、ブラウザがDOMから組み立ててスクリーンリーダーなどの支援技術に渡す木構造です。DOMのすべての要素が載るわけではありません。各ノードは「ロール(button、heading、listなど)」「アクセシブルネーム」「状態(checked、expandedなど)」を持ち、display:noneの要素や装飾だけのdivは取り除かれます。ロールはHTMLの暗黙のロールや有効なrole属性から決まります。名前は要素に応じ、aria-labelledby、aria-label、関連付けられたラベル、テキストなどから計算されます。aria-*属性の考え方はWAI-ARIAの概要と目的で解説しています。
PlaywrightのgetByRole()も、要素のロールやアクセシブルネームを条件にDOM要素を探します。aria snapshotは、ロール・名前・状態をページ単位や部品単位でまとめて書き出し、そのままテストの期待値として使います。
実際の出力:1.64.1でページを書き出した例
次のHTMLをpage.setContent()で読み込み、await page.ariaSnapshot()で書き出しました。ヘッダー、リスト、チェックボックス、アイコンだけのボタン、非表示のボタン、薄いグレーの文字を含むページです。
<!doctype html><html lang="ja"><body>
<header><h1>注文履歴</h1><nav aria-label="メイン"><a href="/orders">注文</a><a href="/account">アカウント</a></nav></header>
<main>
<h2>Issues 12</h2>
<ul aria-label="機能"><li>Feature A</li><li>Feature B</li><li>Feature C</li></ul>
<label><input type="checkbox" checked> メールで通知</label>
<button aria-pressed="true">お気に入り</button>
<button><svg width="16" height="16"><path d="M0 0h16v16H0z"/></svg></button>
<input type="text" aria-label="メールアドレス" placeholder="[email protected]">
<details><summary>詳細</summary><p>本文</p></details>
<div style="display:none"><button>非表示ボタン</button></div>
<p style="color:#ccc;background:#fff">薄いグレーの文字</p>
</main></body></html>
出力されたYAMLは次のとおりです(Playwright 1.64.1、Chrome for Testing 156.0.8078.4)。1.63.0で同じHTMLを書き出した結果と同じでした。
- banner:
- heading "注文履歴" [level=1]
- navigation "メイン":
- link "注文":
- /url: /orders
- link "アカウント":
- /url: /account
- main:
- heading "Issues 12" [level=2]
- list "機能":
- listitem: Feature A
- listitem: Feature B
- listitem: Feature C
- checkbox "メールで通知" [checked]
- text: メールで通知
- button "お気に入り" [pressed]
- button:
- img
- textbox "メールアドレス":
- /placeholder: [email protected]
- group: 詳細
- paragraph: 薄いグレーの文字
DOMとの違いは3点に表れています。headerとmainはbannerとmainというランドマークに変わり、display:noneの「非表示ボタン」は出力されていません。アイコンだけのボタンは名前の無い- button:として出ています。スクリーンリーダーにとって、このボタンは「ボタン」としか読み上げられない要素です。
YAML構文:ロール・名前・属性・プロパティ
公式ドキュメントの定義では、1ノードは- role "name" [attribute=value]で表します。各要素の意味は次の表のとおりです。
| 記法 | 意味 | 例 |
|---|---|---|
| ロール | ARIAロールまたはHTMLから計算したロール | - heading |
"名前" |
アクセシブルネームの完全一致 | - link "注文" |
| /正規表現/ | 名前・テキストの正規表現一致 | - heading /Issues \d+/ |
| [属性] | checked・disabled・expanded・invalid・level・pressed・selected | - checkbox [checked] |
| : テキスト | 子として持つテキスト | - listitem: Feature A |
| /url | リンク先(1.52で追加) | - /url: /orders |
| /placeholder | 入力欄のplaceholder(1.56で追加) | - /placeholder: [email protected] |
| /children | 子要素の照合方式(1.52で追加) | - /children: equal |
出力では[pressed]と書かれていますが、テンプレートに公式例どおり[pressed=true]と書いても照合は通りました。aria-invalidは、trueなら[invalid]、grammar・spellingならその値が[invalid=spelling]のように出ます。falseの場合は出力されません。
toMatchAriaSnapshotの書き方と照合ルール
アサーションは要素単位のexpect(locator)でも、ページ全体のexpect(page)でも書けます。expect(page)は1.60で対応し、page.locator('body')を指定した場合と同じ動きをします。照合では大文字・小文字が区別され、名前とテキスト内の空白は正規化されます。ただし、YAMLのインデントは親子関係を決めるため、階層が変わるインデントの変更は照合結果に影響します。またtoHaveText()などと同じく、テンプレートと一致するまでexpectのタイムアウト(既定5秒)の範囲で再試行します。
部分一致の照合対象と省略ルール
既定では、テンプレートに書いたノードがツリーのどこかに順番どおり含まれていれば合格です。次のテストは、リストに3項目あるうちの1項目だけを書いていますが通ります。
import { test, expect } from '@playwright/test';
test('機能リストにFeature Bがある', async ({ page }) => {
await page.setContent('<ul aria-label="機能"><li>Feature A</li><li>Feature B</li><li>Feature C</li></ul>');
await expect(page.getByRole('list')).toMatchAriaSnapshot(`
- list:
- listitem: Feature B
`);
});
テンプレートの- list:には名前「機能」を書いていませんが、省略した名前と属性は照合の対象外です。ボタンのラベル文言を変えるたびにテストを直したくない場合は、- buttonとロールだけを書きます。
順序の区別と/children: equal・deep-equal
部分一致でも順序は区別されます。テンプレートでFeature BをFeature Aより前に書くと失敗します。項目の追加・削除まで検出したいときは/childrenを指定します。次のテンプレートは、実際のリストにあるFeature Cを書いていないので失敗します。
await expect(page.getByRole('list')).toMatchAriaSnapshot(`
- list:
- /children: equal
- listitem: Feature A
- listitem: Feature B
`);
失敗時のメッセージには、期待値と実際のツリーの差分が表示されます。1.64.1での出力(抜粋)は次のとおりです。
Error: expect(locator).toMatchAriaSnapshot(expected) failed
Locator: getByRole('list')
- Expected - 4
+ Received + 4
- - list:
+ - list "機能":
- - /children: equal
+ - listitem: Feature A
- - listitem: Feature A
+ - listitem: Feature B
- - listitem: Feature B
+ - listitem: Feature C
差分の行がずれて見えますが、内容は「Feature Cが余分にある」という意味です。equalは直下の子だけを厳密に比べ、deep-equalは孫以下も厳密に比べます。既定値はcontainです。プロジェクト全体の既定を変える場合は、設定ファイルのexpect.toMatchAriaSnapshot.childrenに書きます。
import { defineConfig } from '@playwright/test';
export default defineConfig({
expect: {
toMatchAriaSnapshot: {
children: 'equal',
},
},
});
件数・日付の正規表現照合とエスケープ
件数や日時のように実行のたびに変わる文字列は、名前を/.../で書くと正規表現で照合されます。JavaScriptのテンプレートリテラルの中では、バックスラッシュを二重に書く必要があります。
await expect(page.getByRole('main')).toMatchAriaSnapshot(`
- heading /Issues \\d+/ [level=2]
- button "お気に入り" [pressed=true]
`);
リンク先も- /url: /https://www.example.com/orders/.*/のように正規表現で書けます。セッションIDなどのクエリ文字列を含むURLに使えます。
スナップショットの生成と更新:空テンプレート・-u・.aria.yml
空テンプレートと-uによる生成・反映手順
期待値のYAMLを手で書く必要はありません。toMatchAriaSnapshot(``)と空で書き、-u(--update-snapshots)を付けて実行します。
npx playwright test -u
ここで気をつけたいのは、既定ではテストファイルが書き換わらないことです。1.64.1の--helpによると、値を省いた-uはchanged(不一致と欠落だけ更新)で、ソースへの反映方法--update-source-methodの既定はpatchです。実行すると「New baselines created for」の後にgit apply test-results/rebaselines.patchという案内が出て、差分はパッチファイルに書き出されるだけです。-u付きの実行自体は通りますが、パッチを当てずに-uなしで再実行すると、空テンプレートのテストは毎回「A snapshot is not provided, generating new baseline.」で失敗し続けます。
公式ドキュメントには、空テンプレートは「-uなしでも動く」とあります。これは生成されるという意味で、テストの成否は別です。1.64.1で-uなしで実行すると、パッチは作られますが、テストは上のメッセージで失敗しました。ソースを直接書き換える場合は次のようにします。
npx playwright test -u --update-source-method=overwrite
1.64.1で実行すると、空だったテンプレートは- list "機能":の行とFeature A〜Cの3行、計4行のYAMLに置き換わりました。3wayを指定すると、ソースにコンフリクトマーカーが挿入され、採用する側を選べます。test-resultsは実行のたびに初期化される出力先(outputDir)なので、パッチはその実行の直後に当ててください。差分を確認せずにoverwriteで一括更新すると、本来なら失敗すべき変更まで期待値に取り込んでしまいます。これは公式ドキュメントもスナップショットテストの欠点として挙げている点です。
.aria.yml保存と更新モード別の初回の成否
大きなツリーをテストコードに書きたくない場合は、nameオプションで別ファイルに保存できます(1.50で追加)。.aria.ymlはパッチを経由せず、スナップショットの保存先へ直接書き出されます。
await expect(page.getByRole('main')).toMatchAriaSnapshot({ name: 'main.aria.yml' });
ファイルがまだ無いときの初回の成否は、更新モードで変わります。1.64.1で、ファイルを消してから各モードで1回ずつ実行した結果は次のとおりです。
| 指定 | モード | .aria.ymlの初回 | 空テンプレート(インライン) |
|---|---|---|---|
| 無指定 | default |
書き出して失敗 | パッチを作って失敗 |
--update-snapshots=missing |
missing |
書き出して合格 | パッチを作って合格 |
-u |
changed |
書き出して合格 | パッチを作って合格 |
--update-snapshots=none |
none |
書き出さずに失敗 | パッチを作らずに失敗 |
無指定の初回は「A snapshot doesn’t exist at tests/c_file.spec.js-snapshots/features.aria.yml, writing actual.」で失敗し、ファイルはこの時点で書き出されるので2回目から照合が通ります。1.63までは無指定の実行がmissingと呼ばれ、この「書き出して失敗」の動きをしていました。1.64でこの動きの名前がdefaultに変わり、missingは書き出したテストも合格させるモードになりました。リリースノートは、新しいスナップショットの生成と既存の照合を1回のCI実行で済ませられるように、と理由を書いています。1.63までの記事やCI設定で--update-snapshots=missingを「新規のときは失敗させる」つもりで使っている場合、1.64へ上げると新しいスナップショットが黙って合格になります。
保存先の既定は、テストファイルと同じ階層の<ファイル名>-snapshotsディレクトリです。ブラウザごとにファイルは分かれず、1つだけ保存されます。置き場所はexpect.toMatchAriaSnapshot.pathTemplateで変えられます。
CIで使う更新モードの選び方
CIで書き出されたスナップショットは、成果物として取り出さない限りリポジトリに残りません。CIの役割に応じて、モードを次のように選びます。
- 新規の期待値はローカルで作ってレビューする運用:CIは
--update-snapshots=noneにします。期待値の無いテストはファイルを作らずに失敗するため、コミット漏れに気づけます。 - コミット漏れを失敗で知らせたいが、生成物も取り出したい運用:無指定(
default)のままにします。失敗したジョブの-snapshotsディレクトリとrebaselines.patchを成果物として保存すれば、生成された値をそのまま確認できます。 - 期待値をCIで作らせる運用:
missingを使います。ただし初回は照合が行われないまま合格するので、生成物をプルリクエストに含めてレビューする手順と組み合わせてください。
-u(changed)をCIで常に付けると、不一致がすべて期待値の更新として処理され、回帰を検出できなくなります。CIで付けるのは避けてください。
codegenのAssert snapshotによる生成
npx playwright codegenのツールバーにある「Assert snapshot」を使うと、選んだ要素のtoMatchAriaSnapshotがテストコードに記録されます。「Aria snapshot」タブでは、選んだロケーターのツリーを確認できます。要素をピックしてaria snapshotを作るボタンは1.50で追加されました。codegenなどCLIの各コマンドはPlaywright CLIコマンド一覧にまとめています。
ariaSnapshot()の出力オプションとJSON取得
テンプレートを作るときやデバッグのときは、ariaSnapshot()でYAMLを文字列として取り出します。オプションは版ごとに増えています。
| API・オプション | 追加版 | 内容 |
|---|---|---|
locator.ariaSnapshot() |
1.49 | 要素以下のYAML |
page.ariaSnapshot() |
1.59 | body以下のYAML |
depth |
1.59 | 出力する階層の深さを制限 |
mode: 'ai' |
1.59 | [ref=e2]の参照とiframe内を含める |
boxes |
1.60 | [box=x,y,width,height]を付ける |
page.getByRef() |
1.64 | aiモードの[ref=e9]から要素を取る |
ariaSnapshotJSON() |
1.63 | 同じツリーをJSONで返す(JSのみ) |
mode: 'ai'で先ほどのmainを書き出すと、各ノードに[ref=e1]のような参照が付きます。labelの囲みもgenericとして出力されます。テスト用の照合には既定モードを使い、aiモードはLLMに画面の状態を渡す用途で使います。Playwright MCPのREADMEにも「Uses Playwright’s accessibility tree, not pixel-based input.」とあり、ブラウザ操作エージェントが画面を把握する仕組みと同じ系統です。MCP側の使い方はPlaywright MCPの使い方を参照してください。
const yaml = await page.locator('main').ariaSnapshot({ depth: 1 });
const tree = await page.locator('ul').ariaSnapshotJSON();
// [{ role: 'list', name: '機能', children: [{ role: 'listitem', text: 'Feature A' }, ...] }]
// 1.64以降:aiモードの参照から要素を取る
const ai = await page.ariaSnapshot({ mode: 'ai' });
// ai には「- button "お気に入り" [pressed] [ref=e15]」の行が含まれる
await page.getByRef('e15').click();
1.64.1で上のページを読み込んだ直後にpage.ariaSnapshot({ mode: 'ai' })で書き出すと、「お気に入り」ボタンには[ref=e15]が付き、page.getByRef('e15')のテキストは「お気に入り」でした。同じページで先にmainだけをaiモードで書き出した場合は、同じボタンがe9になり、その後にページ全体を書き出してもe9のままでした。APIリファレンスには、refはそのフレームで最後に取ったスナップショットに対して解決されるとあります。番号は書き出す順序や範囲で変わるので、テストの期待値に固定するのには向きません。LLMにツリーを渡し、返ってきたrefで操作する処理に使います。
1.60からは、expectが失敗するとその時点のaria snapshotがエラーコンテキスト(test-results/…/error-context.md)に残ります。1.63では、トレースのsnapshots: { dom: true, aria: true, screen: true }で操作ごとのaria snapshotも記録できるようになりました。
page.accessibility.snapshot()からの移行(1.57で削除)
以前のpage.accessibility.snapshot()は、アクセシビリティツリーをオブジェクトで返すAPIでした。1.57.0(2025年11月25日公開)のリリースノートには「After 3 years of being deprecated, we removed page.accessibility from our API.」とあり、3年間の非推奨期間を経て削除されています。1.57以上で呼ぶとpage.accessibilityがundefinedになるため、TypeErrorで止まります。
| 旧コードの目的 | 1.57以降の置き換え先 |
|---|---|
| ツリーを取り出して目視・保存 | page.ariaSnapshot()(YAML) |
| ツリーをプログラムで走査 | ariaSnapshotJSON()(1.63・JS) |
| 構造が変わっていないか検証 | toMatchAriaSnapshot() |
| アクセシビリティ違反の検査 | axe-core(@axe-core/playwright) |
リリースノートでも、アクセシビリティ検査にはAxeなどのライブラリを使うよう案内されています。旧APIの返り値はrole・name・childrenを持つ入れ子のオブジェクトでした。走査する処理はariaSnapshotJSON()に置き換えるのが最も手間がかかりません。1.62以下で止める場合はJSONメソッドが使えないため、YAMLのariaSnapshot()を使います。
aria snapshotで見落とす問題:axe-coreとの分担
aria snapshotは「アクセシビリティ」の名前が付いていますが、アクセシビリティ要件を満たしているかは判定しません。同じHTMLに@axe-core/playwright 4.14.0をかけたところ(lang="ja"あり・title要素なしの上記ページ)、違反が3件出ました。
| 問題 | aria snapshot | axe-core |
|---|---|---|
| アイコンボタンに名前が無い | - button:と出るだけ |
button-name(critical) |
| 文字色#cccと背景#fffのコントラスト不足 | 出力に現れない | color-contrast(serious) |
| titleが無い | 対象外 | document-title(serious) |
| 見出しや項目が消えた・並びが変わった | 検出できる | 違反にならない |
aria snapshotは「前回と構造が同じか」「設計どおりの名前が付いているか」を確かめる回帰テストです。名前が無いボタンも、現状のYAMLをそのまま期待値にすれば合格になります。名前の欠落を検出できるのは、テンプレートに- button "閉じる"と期待する名前を書くか、- button /.+/のように空ではない名前を要求した場合です(1.64.1では、名前の無いボタンは/.+/との照合で失敗しました)。WCAGの基準で検査するのはaxe-coreの役割なので、CIでは両方を流してください。自動検査で拾えない読み上げ順やキーボード操作まで含めて既存サイトを点検・改修したい場合は、一創のWebアクセシビリティ対応でもご相談いただけます。axe-coreをCIへ組み込む手順と自動検出できる範囲はaxe DevToolsとは、コントラストの基準値はWebアクセシビリティのコントラスト比で扱っています。
ほかのテスト手法との分担は次のとおりです。toHaveText()などのDOMアサーションは1つの値を厳密に確かめる用途に向き、aria snapshotはページや部品の構造をまとめて確かめる用途に向きます。色・余白・レイアウトの崩れはツリーに出てこないため、スクリーンショット比較(VRT(ビジュアルリグレッションテスト))が適しています。特定のCSS値だけを確かめるならtoHaveCSS()も使えます。aria snapshotは、色や余白だけの変更では通常失敗しませんが、CSSで要素を非表示にするなど、アクセシビリティツリーが変わる変更では失敗することがあります。見た目の調整のたびに期待値を更新する必要が無いのは、スクリーンショット比較に対する利点です。
aria snapshotの適用を避ける条件と運用上の失敗例
- ページ全体を1つのスナップショットにする:広告枠・おすすめ欄・通知件数のどれかが変わるたびに失敗し、差分も長くなります。
getByRole('main')やフォームなど、確かめたい部品に絞ってください。 - 多言語サイトで文言を固定する:ロケールごとにテンプレートが必要になります。構造を共通のテンプレートで検証し、文言をロケール別の
toHaveText()で検証すると、構造と翻訳の変更を分けて管理できます。 - 差分を読まずに-uで更新する運用:失敗のたびに更新すると、スナップショットは現状を記録するだけになり、回帰を検出できません。更新はプルリクエストの差分としてレビューできる
patchモードのまま運用してください。 - 値1つを確かめたいだけのテスト:入力欄の値や計算結果は
toHaveValue()などの方が意図が明確で、失敗時の原因も追いやすくなります。
Jest・Vitestのスナップショットテストにも同じ注意点があります。運用上の判断はスナップショットテストとはにまとめています。
aria snapshot関連機能の追加履歴(1.49〜1.64)
| 版(公開日) | 追加・変更 |
|---|---|
| 1.49(2024-11-18) | toMatchAriaSnapshot・locator.ariaSnapshot |
| 1.50(2025-01-23) | .aria.yml保存、pathTemplate、-uのchanged、overwrite・3way |
| 1.52(2025-04-17) | /childrenと/url |
| 1.56(2025-10-06) | placeholderの出力と照合 |
| 1.57(2025-11-25) | page.accessibilityを削除 |
| 1.59(2026-04-01) | page.ariaSnapshot、depth・mode |
| 1.60(2026-05-11) | expect(page)対応、boxes、errorContext |
| 1.63(2026-09-04) | ariaSnapshotJSON、トレースへのaria記録 |
| 1.64(2026-10-07) | page.getByRef、無指定の更新モードをdefaultに改名しmissingは生成したテストも合格に |
次の1.65は2026年11月4日時点でベータ版(npmのbetaタグ)です。リポジトリのリリースノートには、スロットに割り当てられていないテキストや、閉じた<details>の直下にあるテキストなど、描画されないテキストをアクセシブルネームとaria snapshotから外す変更が載っています。該当する部品がある場合、1.65へ上げたときに期待値の更新が必要になる可能性があります。
手元の版はnpx playwright --versionで確認できます。確認手順と導入方法はPlaywrightのインストール方法、各版の変更点の全体と現在の最新版はPlaywrightの最新バージョンにまとめています。
よくある質問
aria snapshotとは何ですか?
Playwrightが提供する、アクセシビリティツリーをYAMLで表したものです。toMatchAriaSnapshot()で期待値のYAMLと照合し、ロール・名前・状態・順序が変わっていないかを確かめます。
PythonやJavaでもaria snapshotは使えますか?
使えます。Pythonはexpect(page).to_match_aria_snapshot()とpage.aria_snapshot()、JavaはassertThat(page).matchesAriaSnapshot()、.NETはToMatchAriaSnapshotAsync()です。ただし、公式ドキュメントの--update-snapshotsによる自動生成の節はJavaScript向けで、ariaSnapshotJSON()もJavaScript限定です。
DOMスナップショットやスクリーンショット比較とは何が違いますか?
DOMスナップショットはHTMLの構造そのもの、スクリーンショット比較は見た目を記録します。aria snapshotはロール・名前・状態などを記録するため、ツリーに影響しないクラス名や装飾の変更では通常失敗せず、見出しやボタン名が変わったときに失敗します。
-uを付けずに実行すると、新しいスナップショットのテストが失敗するのはなぜですか?
無指定の実行(1.64からはdefaultモード)は、無いスナップショットを書き出したうえでテストを失敗させます。書き出された.aria.ymlを確認してから再実行すれば通ります。1.64以降で、生成したテストも合格させたい場合は--update-snapshots=missingを指定します。
-uを付けたのにテストコードが更新されないのはなぜですか?
既定の--update-source-methodがpatchだからです。差分はtest-results/rebaselines.patchに出力されるので、git applyで当ててください。ソースを直接書き換える場合は--update-source-method=overwriteを付けます。
page.accessibility.snapshot()が使えなくなりました。どうすればよいですか?
1.57で削除されたためです。ツリーの取得はpage.ariaSnapshot()かariaSnapshotJSON()、構造の検証はtoMatchAriaSnapshot()、アクセシビリティ違反の検査は@axe-core/playwrightに置き換えます。