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