アクセシビリティツリーの写真の撮り方
目次
この記事は木Advent Calendar 2025の19日目の記事です。
こんにちは。個人ブログだけどアドベントカレンダーなので一応自己紹介をしておきます。フロントエンドエンジニアのmehm8128です。
今回はアクセシビリティツリーの写真の撮り方を解説します。
アクセシビリティツリーの写真の撮り方 #
数ヶ月前に、PlaywrightにAria Snapshotsという機能が入りました。これは、アクセシビリティツリー(以降ax tree)をyamlに変換してスナップショットテストを行うことができるものです。今までDOMのスナップショットテストはJestなどのテストツールで行うことができましたが、ax treeのスナップショットテストを行うことのできるものは普及していなかったと思います。
また、Playwright MCPでも内部ではこのax treeのyaml形式を扱っているらしいです。
Aria Snapshotsには、普段testing-libraryやPlaywrightでexpect()などを用いるアサーションテストや、これまでのDOMのスナップショットテストと比べて、いくつかのメリットがあります。
テストしたい項目を1つ1つ考えて、コードに落とし込む必要がない #
testing-libraryでアサーションテストをする場合は、例えばReact Ariaでは去年の記事で書いたように、「このロールがついている」「tabIndexが0である」「aria-disabledがついている」というようなテストを1つ1つ書いていく必要があります。また、「aria-labelledbyにちゃんとこのIDが指定されている」のようなテストもありました。 しかし、Aria Snapshotsでは、テスト対象のコンポーネントに対してax treeという形でスナップショットを撮り、前回分と比較するだけで済むようになります。 もちろんこれによるデメリットもいくつかあります。アサーションテストとスナップショットテストの比較については、もう少し詳細に公式ドキュメントに記載があるので、読んでみてください。
Assertion testing vs Snapshot testing
抽象化されることで余計な差分が検出されない #
DOMのスナップショットテストだと、例えばちょっとDOM構造を変えたとか、CSSの修正・リファクタのためにclassNameを変えたとか、<a>要素のリンクを変えた、というだけでも差分が検出されてしまいます。
しかし、ax treeというレベルまで抽象化され、roleとaccessible nameとARIA states(とその他いくつかの情報)だけになることで、ユーザーの体験に直接影響しない余計な差分が検出されずに済みます。
さらに、その分スナップショットの文字数も減るので、差分が検出されたときの確認も楽になります。
ただ、今回はメリットを確認するのはそこまで重要ではありません。写真の撮り方を見ていきます。
写真の撮り方 #
写真の撮り方、つまりPlaywrightが実際にどのような方法でax treeのスナップショットを作成しているのかを見ていきます。
まず、テストするときのスナップショットの撮り方は2パターンあります。
- 渡すテンプレート文字列と実際のDOMを比較するパターン。
await expect(page).toMatchAriaSnapshot(`
- link "Test Link" [disabled="true"]
`);
- 保存しているyamlと実際のDOMを比較するパターン。
await expect(page).toMatchAriaSnapshot({
name: "main.aria.yaml",
});
どちらの場合も、まず実際のDOMをax treeオブジェクトに変換します。そして、toMatchAriaSnapshot()に渡される文字列のyamlもしくは保存しているyamlをax treeオブジェクトに変換し、ax treeオブジェクト同士で比較を行っています。
実装の詳細を見ていきます。
ax treeのオブジェクトを内部ではAriaNodeという型で扱っているので、内部で扱うax treeのオブジェクトのことを今後AriaNodeと呼びます。
isomorphic #
これは、yaml to AriaNodeを行うファイルです。
toMatchAriaSnapshot()に渡された文字列や、ファイルから読み込んだyamlを比較用にAriaNodeに変換します。
パーサーでひたすらパースするだけなので、特に言うことはありません。
ariasnapshot.ts / generateAriaTree() #
ariasnapshot.tsは、DOMをAriaNodeに変換し、それをさらにyamlに変換する処理が一通り書かれたファイルです。
generateAriaTree()では、DOM to AriaNodeの部分を行います。
主にvisit()を用いてDOMを走査し、順番にAriaNodeに変換していきます。toAriaNode()という関数で、Element型のノードから様々な情報を抽出し、AriaNodeに変換します。その後、それをprocessElement()という関数でさらに疑似要素やslotからテキストを抽出したり、URLやplaceholderなどを抽出したりしてAriaNodeに情報を追加します。
toAriaNode()をもう少し見ていきます。
この関数では、主にaccessible nameやrole、ARIA statesなどの計算・取得をしていて、それらの情報をAriaNodeとしてオブジェクトに詰め込んだものをreturnします。
これらの計算処理は、過去に書いた記事で軽く紹介しているgetByRoleなどで行われている計算処理と同じ関数が用いられています。具体的にはroleUtils.tsというファイルのgetAriaRole()やgetElementAccessibleName()、getAriaSelected()などです。
Testing Libraryだとここらへんの処理は、記事で紹介しているaria-queryやdom-accessibility-apiのような外部ライブラリを用いて実行しているのですが、Playwrightでは全部自前実装しています。ただ、W3Cのwai-ariaやhtml-aam、acc-nameなどの仕様書へのリンクがたくさん貼られているので、だいぶ読みやすいです。
ariasnapshot.ts / renderAriaTree() #
renderAriaTree()は、AriaNodeをyamlに変換する関数です。
ひたすら文字列にしていきます。
matchesExpectAriaTemplate() #
AriaNodeを比較する関数です。
内部でgenerateAriaTree()で対象のDOMをAriaTreeに変換し、それと渡されたyamlとをAriaNodeに変換したものを、matchesNodeDeep()関数で比較します(ただし、コード見れば分かるように、後者の型はAriaNodeとは微妙に違うAriaTemplateNodeという型のようです)。
メインの処理はmatchesNode()にあります。
まとめ #
そんなに深堀りせずに概要をざっくり見てきました。気になる人はもう少しコードを読んでみたり、自分で実装してみたりしてください。Playwrightでだけ実現できるようなものでもない気がしているので、Playwrightだけでなくtesting-libraryなどでもできるようになってほしいです。
また、ax treeについてもっと知りたい方は、最近以下のブログで何記事か公開されている関連記事を読むといいかもしれません。