browser-useの要素マッチングの簡易解説とテスト自動化への応用
はじめに
今回は、browser-useの要素マッチングとそれを応用したテスト自動化手法について簡易的に解説します。なお、browser-useとはどのようなものか?といった基本的な理解がある前提で話を進めます。
browser-useの要素取得方法
browser-useでは、DOM構成ツリーのJSファイルを利用し、ハイライトした要素にユニークなインデックスを付与してHASHMAP、SelectorMapとして管理しています。また、要素のハッシュ化も行っており、再帰的に要素を取得できる仕組みになっています。
これにより、要素の取得を簡素化し、トークン数を確保しています。DOMを都度呼び出すことで、動的に要素取得が必要なSPAサイトにも対応可能です。
通常、langchainによりchain化するとトークン履歴がトークン数を圧迫しますが、トークン数の制約を回避するためにトークン数を節約する手法が採用されています。
ブラウザについては、iframeでハイライト付き要素を取得するためにブラウザのデバッグモードを使用しています。この設定がOFFだと、iframeを利用したカレンダーなどから要素を取得できません。また、セキュリティ上の理由から、通常のブラウザにアタッチせず、プロキシを介して操作しています。
要素取得に関しては、AIではなくコーディングにより担保されている点を説明します。
詳細な仕組み(長いので生成AIで解説しています)
1. JavaScript側の処理概要
1.1. DOMツリーの再帰的走査とノードデータの生成
-
再帰的走査:
-
buildDomTree
関数がdocument.body
を起点としてDOM全体を再帰的に走査 - 要素ノード(
Node.ELEMENT_NODE
)とテキストノード(Node.TEXT_NODE
)を区別し、処理可能なノードのみを対象 - 空のテキスト、スクリプト・スタイルタグなどは早期にスキップ
-
-
ノードデータ(nodeData)の生成:
- タグ名(
tagName
):要素の場合のタグ名(小文字) - 属性(
attributes
):特にインタラクティブな要素の場合の属性情報 - XPath(
xpath
):そのノードまでのパス情報 - 子ノード(
children
):子ノードのIDリスト - 可視性(
isVisible
)、最前面かどうか(isTopElement
)、インタラクティブか(isInteractive
)の判定結果 - ハイライト用の
highlightIndex
やiframe/シャドウDOMの判定情報なども付加
- タグ名(
1.2. ユニークID生成とハッシュマップによる管理
-
IDの生成:
- グローバル変数
ID.current
を使用し、各ノード処理時に連番(文字列化したID)を生成
- グローバル変数
-
DOM_HASH_MAPへの登録:
- 生成されたIDをキーとして、各
nodeData
をグローバルなハッシュマップDOM_HASH_MAP
に登録 - これにより、ツリー構造をたどる際に、IDをキーとして各ノードに高速にアクセス可能
- 生成されたIDをキーとして、各
1.3. パフォーマンス最適化とキャッシュ
-
タイミングスタックとパフォーマンス測定:
- デバッグモード時に各関数の実行時間を測定するための
pushTiming
やpopTiming
などのタイミング管理関数を使用
- デバッグモード時に各関数の実行時間を測定するための
-
キャッシュの利用:
- DOM操作のコスト削減のため、
getCachedBoundingRect
やgetCachedComputedStyle
関数でWeakMap
を使ったキャッシュを実装 - 同一要素に対する再計算を防止
- DOM操作のコスト削減のため、
2. Python側の処理概要
Python側では、JavaScriptで生成したDOMツリーのハッシュマップ情報を受け取り、Pythonのデータクラス(DOMElementNode
、DOMTextNode
、DOMState
など)に変換し、ツリー構造として再構築します。
2.1. DomServiceクラスの役割
-
初期化:
- コンストラクタで対象のブラウザページ(Playwrightの
Page
オブジェクト)と、JS側のコード(buildDomTree.js
)を読み込み - XPathのキャッシュなども初期化
- コンストラクタで対象のブラウザページ(Playwrightの
-
クリック可能要素の取得:
-
get_clickable_elements
メソッドがハイライト処理やフォーカス対象の指定、viewportの拡張値などのパラメータを受け取り、内部でDOMツリーの構築とパース処理を実行
-
2.2. JSコードの実行とDOMツリー情報の取得
-
_build_dom_treeメソッド:
- まず、ページ上で簡単なJS評価(
1+1
)を行い、JavaScriptが正常に実行できるかを確認 - 引数(ハイライト要素の有無、フォーカス対象、viewportの拡張、デバッグモードの有無)を含むオブジェクトを渡し、
self.js_code
(ビルドされたDOMツリー生成スクリプト)をpage.evaluate
で実行 - 返却された結果は、JS側で作成されたハッシュマップ(
map
)とルートノードのID(rootId
)、およびデバッグモードの場合はパフォーマンス計測情報を含む
- まず、ページ上で簡単なJS評価(
2.3. Python側でのDOMツリーの再構築
-
_construct_dom_treeメソッド:
- 取得したJSの結果から、
js_node_map
とjs_root_id
を取り出す - 各ノードのデータについて、内部メソッド
_parse_node
を呼び出し、Python側のデータクラスに変換 - 各ノードはユニークなIDにより一度
node_map
に登録され、インタラクティブな要素についてはhighlight_index
をキーとしたselector_map
も作成 - ツリーは「ボトムアップ」で再構築され、すでにパース済みの子ノード情報を利用して親子関係が組み立てられる
- 最後に、不要な変数(
node_map
やjs_node_map
)を削除し、ガベージコレクション(gc.collect()
)を実施してメモリを解放
- 取得したJSの結果から、
-
_parse_nodeメソッド:
- 各ノードのデータ(辞書形式)をチェックし、テキストノードの場合は
DOMTextNode
、要素ノードの場合はDOMElementNode
に変換 - 要素ノードでは、
tag_name
、xpath
、属性情報、可視性、インタラクティブ性、ハイライトインデックス、シャドウDOMの有無、さらに存在すればviewport情報などを設定 - 変換後、該当ノードの子ノードIDリストも返し、上位の再構築処理で親子関係のリンク付けに利用
- 各ノードのデータ(辞書形式)をチェックし、テキストノードの場合は
3. 両者の連携と要素マッチングの仕組み
3.1. JavaScriptでのハッシュマップ利用
-
ユニークIDによる高速アクセス:
- JavaScript側で各ノードにユニークなIDを与え、
DOM_HASH_MAP
に登録することで、ツリー全体の情報を平坦なハッシュマップとして保持 - これにより、後続の処理で特定のノードに対して直接アクセスが可能
- JavaScript側で各ノードにユニークなIDを与え、
-
ハイライト用のインデックス:
- ノードがインタラクティブかつ可視な場合、
highlightIndex
が付与され、highlightElement
関数により実際のDOM上で視覚的にハイライトが行われる - これがPython側で、クリック可能な要素(インタラクティブな要素)の選別に使われ、
selector_map
として管理される
- ノードがインタラクティブかつ可視な場合、
3.2. Python側での再構築とマッチング
-
データクラスへの変換:
- JSから返されたハッシュマップを元に、Python側では各ノードが
DOMElementNode
やDOMTextNode
として再構築され、ツリー構造(親子関係)が復元される
- JSから返されたハッシュマップを元に、Python側では各ノードが
-
セレクタマップの生成:
- インタラクティブな要素に対しては
highlight_index
が設定されているため、これをキーとしてselector_map
が作成され、特定のハイライトインデックスに対応する要素に迅速にアクセスできるようになる
- インタラクティブな要素に対しては
3.3. 連携のメリット
-
効率的なDOM情報の取得:
- JavaScript側でキャッシュやパフォーマンス計測を行いながらDOMツリー全体の情報を抽出し、Python側ではその情報を元に再構築するため、ブラウザとPython側の連携がスムーズに実現される
-
正確な要素マッチング:
- ユニークIDとハイライトインデックスを用いることで、ツリー構造を横断しても各要素に一意にアクセスでき、後続のクリック操作などの処理で正確な要素の特定が可能となる
4. 結論
このシステムは、ブラウザ上で実行されるJavaScriptによってDOMツリー全体を再帰的に走査し、各ノードに対して必要な情報(タグ名、属性、xpath、可視性、インタラクティブ性、ハイライトインデックスなど)を抽出し、ユニークなIDをキーとするハッシュマップに登録します。
その後、Python側のDomService
クラスがこのハッシュマップを受け取り、各ノードをデータクラスに変換してツリー構造を再構築することで、特定のクリック可能な要素を含むDOM状態(DOMState
)として管理します。
これにより、後続の処理(例えば自動クリックや要素の操作)において、迅速かつ正確に対象の要素にマッチング・アクセスできる仕組みが実現されています。
要素取得方法を応用したテスト自動化
結論からいうと、この要素取得方法を応用してテスト自動化のフレームワークとして利用することは可能です。筆者は既にAIのみで実装させてテスト自動化まで行うことに成功しています(もちろん課題はありますが)。
テスト自動化の流れ
以下は、筆者が実装したテスト自動化の流れです(細かい処理は割愛しています):
-
CSVでテストケースを用意
- テスト手順の1は画面遷移からスタートで統一
-
画面に遷移したら、必ず画面スクリーンショットで画面タイトルとサマリーを生成
- 画面遷移で必ず行うようにフラグを設定
-
DOMから要素取得してHASHマップ化
- 操作の度に取得を実行
- テスト設計の自動化も将来的に考慮するなら、データフレームなどに格納できるようにしておく
-
langchainのシステムプロンプトで、2)と3)を入れて呼び出す
-
テスト開始
-
テスト結果を記載
- NGや保留の場合は理由をAIに記載させる
- テスト実行時、打鍵した要素のパスは自動記載
-
2)から6)をループ
AIにNG理由や保留理由を記載させることで、人間がテストしながら記載する雑な記載よりも質の高い結果が得られます。負荷テストでも同様に測定結果を記録できるようにしています。
テスト自動化におけるメリット/デメリット
メリット
- 要素取得の手間とPlaywrightのスクリプトを書く手間がゼロになる
- 自然言語でテストケースが記載できるため、非エンジニアでもテストケースを自然言語で記載して対応可能(これが最大のメリット)
デメリット
- コーディングによる実装が必要となる点
- ただし、生成AIコーディングにより現在は1700行程度なら比較的容易に記述可能
- 負荷テスト、クロスブラウザテストなどは自身で実装が必要
負荷テスト、セキュリティテスト、クロスブラウザテストについて
これらは自身で実装が必要となります。特にクロスブラウザテストは実装が多少面倒です。