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 動画にコメントを残してください。