Puppeteer とそのセレクタへのアプローチ
Puppeteer は、Node 用のブラウザ自動化ライブラリです。シンプルで最新の JavaScript API を使用してブラウザを制御できます。
ブラウザの最も重要なタスクは、もちろんウェブページの閲覧です。このタスクを自動化することは、基本的にウェブページとのやり取りを自動化することになります。
Puppeteer では、文字列ベースのセレクタを使用して DOM 要素をクエリし、要素のクリックやテキストの入力などのアクションを実行することで、これを実現します。たとえば、developer.google.com を開き、検索ボックスを見つけて、puppetaria を検索するスクリプトは次のようになります。
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://developers.google.com/', { waitUntil: 'load' });
// Find the search box using a suitable CSS selector.
const search = await page.$('devsite-search > form > div.devsite-search-container');
// Click to expand search box and focus it.
await search.click();
// Enter search string and press Enter.
await search.type('puppetaria');
await search.press('Enter');
})();
そのため、クエリセレクタを使用して要素を識別する方法は、Puppeteer のエクスペリエンスを定義する重要な要素です。これまで、Puppeteer のセレクタは CSS セレクタと XPath セレクタに限定されていました。これらは表現力に優れているものの、スクリプトでブラウザの操作を保持するうえで欠点となる可能性があります。
構文セレクタとセマンティック セレクタ
CSS セレクタは本質的に構文的なものであり、DOM から ID とクラス名を参照するという意味で、DOM ツリーのテキスト表現の内部動作に密接に関連しています。そのため、ウェブ デベロッパーがページ内の要素にスタイルを変更または追加するための不可欠なツールとなっていますが、そのコンテキストでは、デベロッパーがページとその DOM ツリーを完全に制御できます。
一方、Puppeteer スクリプトはページの外部オブザーバーであるため、このコンテキストで CSS セレクタを使用すると、ページの実施方法に関する隠れた前提が導入され、Puppeteer スクリプトはそれを制御できません。
その結果、このようなスクリプトは脆弱になり、ソースコードの変更の影響を受けやすくなります。たとえば、body 要素の 3 番目の子としてノード <button>Submit</button> を含むウェブ アプリケーションの自動テストに Puppeteer スクリプトを使用するとします。テストケースのスニペットは次のようになります。
const button = await page.$('body:nth-child(3)'); // problematic selector
await button.click();
ここでは、セレクタ 'body:nth-child(3)' を使用して送信ボタンを検索していますが、これはこのバージョンのウェブページに厳密にバインドされています。後でボタンの上に要素が追加されると、このセレクタは機能しなくなります。
これはテスト作成者にとって目新しいことではありません。Puppeteer ユーザーは、このような変更に強いセレクタを選択しようとしています。Puppetaria は、この探求を支援する新しいツールです。
Puppeteer には、CSS セレクタに依存するのではなく、ユーザー補助ツリーのクエリに基づく代替クエリ ハンドラが付属するようになりました。ここでの基本的な考え方は、選択する具体的な要素が変更されていない場合、対応するユーザー補助ノードも変更されていないはずだということです。
このようなセレクタは「ARIA セレクタ」と呼ばれ、ユーザー補助ツリーの計算されたユーザー補助機能用の名前とロールのクエリをサポートしています。CSS セレクタと比較すると、これらのプロパティは本質的にセマンティックです。これらは DOM の構文プロパティに関連付けられているのではなく、スクリーン リーダーなどの支援技術を介してページがどのように認識されるかを示す記述子です。
上記のテスト スクリプトの例では、代わりにセレクタ aria/Submit[role="button"] を使用して目的のボタンを選択できます。ここで、Submit は要素のユーザー補助機能用の名前を指します。
const button = await page.$('aria/Submit[role="button"]');
await button.click();
ここで、後でボタンのテキスト コンテンツを Submit から Done に変更することにした場合、テストは再び失敗しますが、この場合はそれが望ましい結果です。ボタンの名前を変更することで、ページの視覚的な表示や DOM での構造ではなく、ページのコンテンツを変更することになるからです。テストでは、このような変更が意図的なものであることを確認するために、このような変更について警告する必要があります。
検索バーを使用したより大きな例に戻ると、新しい aria ハンドラを活用して、
const search = await page.$('devsite-search > form > div.devsite-search-container');
+
const search = await page.$('aria/Open search[role="button"]');
検索バーを見つけてください。
より一般的には、このような ARIA セレクタを使用することで、Puppeteer ユーザーに次のようなメリットがもたらされると考えています。
- テスト スクリプトのセレクタを、ソースコードの変更に対してより復元力のあるものにする。
- テスト スクリプトが読みやすくなる(ユーザー補助機能用の名前はセマンティック記述子)。
- 要素にユーザー補助プロパティを割り当てるための適切な方法を促す。
この記事の残りの部分では、Puppetaria プロジェクトの実装方法について詳しく説明します。
設計プロセス
背景
前述のように、ユーザー補助機能用の名前とロールで要素をクエリできるようにする必要があります。これらは、ユーザー補助ツリーのプロパティです。ユーザー補助ツリーは、通常の DOM ツリーと対をなすもので、スクリーン リーダーなどのデバイスがウェブページを表示するために使用します。
ユーザー補助機能用の名前の計算に関する仕様を見ると、要素の名前の計算は簡単なタスクではないことがわかります。そのため、最初から Chromium の既存のインフラストラクチャを再利用することにしました。
実装へのアプローチ
Chromium のユーザー補助ツリーの使用に限定しても、Puppeteer で ARIA クエリを実装する方法はいくつかあります。その理由を理解するために、まず Puppeteer がブラウザを制御する方法を見てみましょう。
ブラウザは、Chrome DevTools Protocol(CDP)と呼ばれるプロトコルを介してデバッグ インターフェースを公開します。これにより、「ページを再読み込みする」や「ページでこの JavaScript を実行して結果を返す」などの機能が、言語に依存しないインターフェースを介して公開されます。
DevTools のフロントエンドと Puppeteer は、どちらも CDP を使用してブラウザと通信します。CDP コマンドを実装するために、Chrome のすべてのコンポーネント(ブラウザ、レンダラなど)に DevTools インフラストラクチャが組み込まれています。CDP は、コマンドを適切な場所にルーティングします。
クエリ、クリック、式の評価などの Puppeteer アクションは、ページ コンテキストで JavaScript を直接評価して結果を返す Runtime.evaluate などの CDP コマンドを使用して実行されます。色覚異常のエミュレーション、スクリーンショットの撮影、トレースのキャプチャなど、他の Puppeteer アクションでは、CDP を使用して Blink レンダリング プロセスと直接通信します。

この時点で、クエリ機能を実装する方法は 2 つあります。
- JavaScript でクエリロジックを記述し、
Runtime.evaluateを使用してページに挿入する。 - Blink プロセスでユーザー補助ツリーに直接アクセスしてクエリを実行できる CDP エンドポイントを使用します。
3 つのプロトタイプを実装しました。
- JS DOM トラバーサル - ページに JavaScript を挿入
- Puppeteer AXTree トラバーサル - ユーザー補助ツリーへの既存の CDP アクセスを使用
- CDP DOM トラバーサル - アクセシビリティ ツリーのクエリ用に特別に構築された新しい CDP エンドポイントを使用
JS DOM トラバーサル
このプロトタイプは、DOM の完全なトラバーサルを行い、ComputedAccessibilityInfo 起動フラグでゲートされた element.computedName と element.computedRole を使用して、トラバーサル中に各要素の名前とロールを取得します。
Puppeteer AXTree トラバーサル
ここでは、代わりに CDP を介して完全なユーザー補助ツリーを取得し、Puppeteer で走査します。生成されたユーザー補助ノードは、DOM ノードにマッピングされます。
CDP DOM トラバーサル
このプロトタイプでは、ユーザー補助ツリーのクエリ専用の新しい CDP エンドポイントを実装しました。これにより、JavaScript を介してページ コンテキストでクエリを実行するのではなく、C++ 実装を介してバックエンドでクエリを実行できます。
単体テストのベンチマーク
次の図は、3 つのプロトタイプで 4 つの要素を 1,000 回クエリした場合の合計実行時間を比較したものです。ベンチマークは、ページサイズとユーザー補助要素のキャッシュ保存が有効かどうかを変えて、3 つの異なる構成で実行されました。
CDP を利用したクエリ メカニズムと、Puppeteer のみで実装された他の 2 つのメカニズムの間には、パフォーマンスにかなりの差があることは明らかです。また、相対的な差はページサイズとともに劇的に増加するようです。JS DOM トラバーサル プロトタイプが、ユーザー補助機能のキャッシュ保存の有効化に非常にうまく対応しているのは、興味深いことです。キャッシュが無効になっている場合、ユーザー補助ツリーはオンデマンドで計算され、ドメインが無効になっている場合は各操作の後にツリーが破棄されます。ドメインを有効にすると、Chromium は代わりに計算されたツリーをキャッシュに保存します。
JS DOM トラバーサルの場合、トラバーサル中にすべての要素のユーザー補助機能用の名前とロールをリクエストするため、キャッシュが無効になっていると、Chromium はアクセスするすべての要素のユーザー補助ツリーを計算して破棄します。一方、CDP ベースのアプローチでは、ツリーは CDP の呼び出し間、つまりクエリごとにのみ破棄されます。これらのアプローチでも、キャッシュを有効にすると、アクセシビリティ ツリーが CDP 呼び出し間で保持されるため、パフォーマンスが向上しますが、その向上は比較的小さいものです。
ここではキャッシュを有効にすることが望ましいように見えますが、メモリ使用量が増加するというコストが伴います。トレース ファイルを記録する Puppeteer スクリプトなどでは、これが問題になる可能性があります。そのため、ユーザー補助ツリーのキャッシュをデフォルトで有効にしないことにしました。ユーザーは、CDP のユーザー補助ドメインを有効にすることで、キャッシュを有効にできます。
DevTools テストスイート ベンチマーク
前回のベンチマークでは、CDP レイヤにクエリ メカニズムを実装することで、臨床ユニットテスト シナリオのパフォーマンスが向上することが示されました。
この違いが、完全なテストスイートを実行するというより現実的なシナリオで顕著になるかどうかを確認するため、DevTools のエンドツーエンド テストスイートにパッチを適用して、JavaScript と CDP ベースのプロトタイプを使用し、ランタイムを比較しました。このベンチマークでは、合計 43 個のセレクタを [aria-label=…] からカスタムクエリ ハンドラ aria/… に変更し、各プロトタイプを使用して実装しました。
一部のセレクタはテスト スクリプトで複数回使用されるため、aria クエリハンドラの実際の実行回数は、スイートの実行ごとに 113 回でした。クエリ選択の合計数は 2, 253 で、プロトタイプを介して行われたクエリ選択はごく一部でした。
上の図に示すように、合計ランタイムには顕著な違いがあります。データにノイズが多すぎるため、具体的な結論を出すことはできませんが、このシナリオでも 2 つのプロトタイプのパフォーマンスの差が表れていることは明らかです。
新しい CDP エンドポイント
上記のベンチマークを考慮し、また、起動フラグベースのアプローチは一般的に望ましくないため、ユーザー補助ツリーをクエリするための新しい CDP コマンドを実装することにしました。次に、この新しいエンドポイントのインターフェースを決定する必要がありました。
Puppeteer でのユースケースでは、エンドポイントが、いわゆる RemoteObjectIds を引数として受け取る必要があります。また、後で対応する DOM 要素を見つけられるように、DOM 要素の backendNodeIds を含むオブジェクトのリストを返す必要があります。
下のグラフに示すように、このインターフェースを満たすアプローチをいくつか試しました。このことから、返されるオブジェクトのサイズ、つまり、完全なユーザー補助ノードを返したか、backendNodeIds のみを返したかによる違いは、ほとんどないことがわかりました。一方、既存の NextInPreOrderIncludingIgnored を使用すると、トラバーサル ロジックの実装で顕著な速度低下が発生するため、不適切な選択であることがわかりました。
最後のまとめ
CDP エンドポイントが用意できたので、Puppeteer 側にクエリハンドラを実装しました。この作業の大部分は、ページ コンテキストで評価された JavaScript を介してクエリを実行するのではなく、CDP を介してクエリを直接解決できるように、クエリ処理コードを再構築することでした。
次のステップ
新しい aria ハンドラは、組み込みのクエリ ハンドラとして Puppeteer v5.4.0 に付属しています。ユーザーがこの機能をテスト スクリプトにどのように組み込むか、そしてこの機能をさらに便利にするためのアイデアを皆様からお聞きできることを楽しみにしています。
プレビュー チャネルをダウンロードする
デフォルトの開発用ブラウザとして、Chrome の Canary、Dev、Beta のいずれかを使用することをおすすめします。これらのプレビュー チャンネルでは、最新の DevTools 機能にアクセスしたり、最先端のウェブ プラットフォーム API をテストしたり、ユーザーが気づく前にサイトの問題を見つけたりできます。
Chrome DevTools チームに問い合わせる
DevTools の新機能、更新、その他の関連事項について話し合うには、次のオプションを使用します。
- フィードバックや機能リクエストは、crbug.com からお送りください。
- DevTools のその他 > [ヘルプ] > [DevTools の問題を報告] を使用して、DevTools の問題を報告してください。
- @ChromeDevTools をメンションしてツイートしてください。
- DevTools の新機能に関する YouTube 動画または DevTools のヒントに関する YouTube 動画にコメントを残す。