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 スクリプトでは制御できません。
その結果、このようなスクリプトは脆弱で、ソースコードの変更を受けやすくなる可能性があります。たとえば、ノード <button>Submit</button>
を body
要素の 3 番目の子として含むウェブ アプリケーションの自動テストに 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 コマンドを実装するために、DevTools インフラストラクチャは Chrome のすべてのコンポーネント(ブラウザ、レンダラなど)内にあります。コマンドを適切な場所にルーティングするのは CDP です。
式のクエリ、クリック、評価などの人形アクションは、Runtime.evaluate
などの CDP コマンドを利用して実行されます。このコマンドはページ コンテキストで直接 JavaScript を評価し、結果を返します。色覚異常のエミュレート、スクリーンショットの撮影、トレースのキャプチャなど、その他の 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 のみに実装されている他のクエリ メカニズムとの間には、パフォーマンスにかなりのギャップがあることは明らかで、ページサイズによって相対的な差が劇的に増加しているように見えます。興味深いことに、JS DOM トラバーサル プロトタイプが、アクセシビリティ キャッシュを有効にしたときに非常にうまく反応します。キャッシュが無効になると、アクセシビリティ ツリーはオンデマンドで計算され、ドメインが無効な場合はインタラクションのたびにツリーが破棄されます。ドメインを有効にすると、代わりに計算されたツリーが Chromium のキャッシュに保存されます。
JS DOM トラバーサルでは、走査中にすべての要素にアクセス可能な名前と役割が必要です。したがって、キャッシュが無効になっている場合、Chromium はアクセスするすべての要素のユーザー補助ツリーを計算して破棄します。それに対して、CDP ベースのアプローチでは、CDP への各呼び出しの間、つまりすべてのクエリの間にのみ、ツリーが破棄されます。これらのアプローチでは、キャッシュを有効にすることでもメリットがあります。アクセス ツリーが CDP 呼び出し間で保持されるため、パフォーマンスの向上は比較的小さくなります。
ここではキャッシュを有効にすることが望ましいように見えますが、メモリ使用量が増えるという代償が伴います。これは、トレース ファイルを記録するなど、Puppeteer スクリプトでは問題になる可能性があります。そのため、ユーザー補助ツリーのキャッシュをデフォルトでは有効にしないことにしました。ユーザーは CDP のユーザー補助ドメインを有効にすることで、ユーザー自身でキャッシュ保存を有効にできます。
DevTools テストスイートのベンチマーク
以前のベンチマークでは、CDP レイヤにクエリ メカニズムを実装することで、臨床単体テストのシナリオでパフォーマンスが向上することが示されました。
完全なテストスイートを実行するというより現実的なシナリオで、違いが顕著にわかるか確認するため、JavaScript と CDP ベースのプロトタイプを使用するように DevTools のエンドツーエンド テストスイートにパッチを適用し、ランタイムを比較しました。このベンチマークでは、合計 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 に付属しています。Google は、ユーザーがテスト スクリプトにこの機能をどのように取り入れるかを楽しみにしています。この機能をさらに便利なものにする方法について、皆さんのご意見をお待ちしています。
プレビュー チャネルをダウンロードする
デフォルトの開発ブラウザとして、Chrome Canary、Dev、Beta の使用を検討してください。これらのプレビュー チャネルでは、最新の DevTools 機能にアクセスしたり、最先端のウェブ プラットフォーム API をテストしたり、ユーザーが事前にサイトに関する問題を発見したりすることができます。
Chrome DevTools チームへのお問い合わせ
投稿内の新機能や変更点、または DevTools に関するその他の事項について議論するには、以下のオプションを使用します。
- crbug.com からご提案やフィードバックをお寄せください。
- DevTools の [その他のオプション] > [ヘルプ] > [DevTools の問題を報告する] を使用して、DevTools の問題を報告する。
- @ChromeDevTools でツイートします。
- DevTools の新機能の YouTube 動画または DevTools のヒントの YouTube 動画で、コメントを記入してください。