Puppetaria: ユーザー補助重視の Puppeteer のスクリプト

Johan Bay
Johan Bay

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 スクリプトを使用するとします。テストケースの 1 つのスニペットは、次のようになります。

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 ツリーと重複するアクセシビリティ ツリーのプロパティで、スクリーン リーダーなどのデバイスでウェブページの表示に使用されます。

ユーザー補助機能用の名前の計算の仕様を見ると、要素の名前の計算が簡単なタスクではないことは明らかです。そのため、Google は最初から、Chromium の既存のインフラストラクチャをこの目的のために再利用することにしました。

実装のアプローチ

Chromium のアクセシビリティ ツリーを使うだけでも、Puppeteer で ARIA クエリを実装する方法はたくさんあります。その理由を知るために、まず Puppeteer がブラウザを制御する仕組みを見てみましょう。

ブラウザには、Chrome DevTools Protocol(CDP)と呼ばれるプロトコルを介したデバッグ インターフェースが表示されます。これにより、言語に依存しないインターフェースを通じて、「ページを再読み込みする」や「この JavaScript をページで実行して結果を返す」などの機能を公開できます。

DevTools のフロントエンドと Puppeteer は、どちらも CDP を使用してブラウザと通信しています。CDP コマンドを実装するために、Chrome のすべてのコンポーネント(ブラウザ、レンダラなど)内に DevTools インフラストラクチャがあります。CDP はコマンドを適切な場所にルーティングします。

Puppeteer のアクション(クエリ、クリック、式の評価など)は、Runtime.evaluate などの CDP コマンドを活用して実行されます。CDP コマンドは、ページのコンテキストで JavaScript を直接評価し、結果を返します。Puppeteer のその他のアクション(色覚異常のエミュレーション、スクリーンショットの撮影、トレースのキャプチャなど)は、CDP を使用して Blink のレンダリング プロセスと直接通信します。

CDP

これにより、クエリ機能を実装するための 2 つのパスが残ります。

  • JavaScript でクエリロジックを記述し、Runtime.evaluate を使用してページに挿入する。
  • Blink プロセス内で直接ユーザー補助ツリーにアクセスしてクエリを実行できる CDP エンドポイントを使用します。

以下の 3 つのプロトタイプを実装しました。

  • JS DOM トラバーサル - ページへの JavaScript の挿入に基づく
  • Puppeteer AXTree トラバーサル - アクセシビリティ ツリーへの既存の CDP アクセスに基づく
  • CDP DOM トラバーサル - アクセシビリティ ツリーのクエリに特化した新しい CDP エンドポイントを使用

JS DOM 走査

このプロトタイプは DOM のフル走査を行い、ComputedAccessibilityInfo 起動フラグで制限された element.computedNameelement.computedRole を使用して、走査中に各要素の名前とロールを取得します。

Puppeteer AXTree トラバーサル

ここでは、代わりに CDP を通じてアクセシビリティ ツリー全体を取得し、Puppeteer でトラバースします。結果として得られるアクセシビリティ ノードは、DOM ノードにマッピングされます。

CDP DOM 走査

このプロトタイプでは、ユーザー補助ツリーのクエリ専用の新しい CDP エンドポイントを実装しました。こうすることで、JavaScript によるページ コンテキストではなく、C++ 実装を介してバックエンドでクエリを実行できます。

単体テストのベンチマーク

次の図は、3 つのプロトタイプで 4 つの要素を 1,000 回クエリした場合の合計実行時間を比較したものです。ベンチマークは、ページサイズとユーザー補助要素のキャッシュが有効かどうかを変えた 3 つの異なる設定で実行しました。

ベンチマーク: 4 つの要素を 1,000 回クエリした場合の合計実行時間

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 だったので、プロトタイプによって行われたのは、クエリ選択のごく一部にすぎません。

ベンチマーク: e2e テストスイート

上の図に示すように、合計ランタイムには目に見える違いがあります。データはノイズが多く、具体的な結論を出すことはできませんが、2 つのプロトタイプ間のパフォーマンス ギャップがこのシナリオでも明らかです。

新しい CDP エンドポイント

上記のベンチマークを考慮し、またリリースフラグに基づくアプローチは一般的に望ましくないものであったため、アクセシビリティ ツリーをクエリする新しい CDP コマンドの実装を進めることにしました。そこで、この新しいエンドポイントのインターフェースを見つける必要がありました。

Puppeteer のユースケースでは、エンドポイントでいわゆる RemoteObjectIds を引数として受け取り、対応する DOM 要素を後で見つけられるように、DOM 要素の backendNodeIds を含むオブジェクトのリストを返す必要があります。

下の表に示すように、このインターフェースの要件を満たす方法はいろいろ試されました。このことから、返されたオブジェクトのサイズ、つまり、完全なアクセシビリティ ノードを返したのか、backendNodeIds のみを返したのかによって、識別できる違いがないことがわかりました。その一方で、走査ロジックの実装には既存の NextInPreOrderIncludingIgnored の使用は適切でないことがわかりました。明らかな速度低下が発生しています。

ベンチマーク: CDP ベースの AXTree 走査プロトタイプの比較

まとめ

現在、CDP エンドポイントを使用して、Puppeteer 側にクエリハンドラを実装しました。この取り組みの大きな課題は、ページ コンテキストで評価される JavaScript を使用してクエリを実行する代わりに、CDP を介してクエリを直接解決できるように、クエリ処理コードを再構成することでした。

次のステップ

新しい aria ハンドラは、Puppeteer v5.4.0 に組み込みクエリハンドラとして付属しています。テスト スクリプトに取り入れていただくのを楽しみにしています。さらに便利になるように改善する方法について、皆様からのアイデアをお待ちしています。

プレビュー チャンネルをダウンロードする

デフォルトの開発ブラウザとして Chrome の CanaryDev、または Beta を使用することを検討してください。これらのプレビュー チャンネルを使用すると、DevTools の最新機能にアクセスしたり、最先端のウェブ プラットフォーム API をテストしたり、ユーザーに先駆けてサイトの問題を検出したりできます。

Chrome DevTools チームへのお問い合わせ

以下のオプションを使用して、投稿の新機能や変更点、または DevTools に関連するその他のことについて話し合います。

  • ご提案やフィードバックは、crbug.com からお送りください。
  • DevTools の [More options] その他   > [Help] > [Report a DevTools issues] を使用して、DevTools の問題を報告します。
  • @ChromeDevTools でツイートしてください。
  • DevTools の新機能に関する YouTube 動画または DevTools のヒントの YouTube 動画にコメントを残してください。