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 樹狀結構文字表示法的內部運作方式緊密相關,因為它們會參照 DOM 的 ID 和類別名稱。因此,這類屬性是網頁程式開發人員修改或新增網頁元素樣式時不可或缺的工具,但在這種情況下,開發人員可以完全控制網頁及其 DOM 樹狀結構。

另一方面,Puppeteer 指令碼是網頁的外部觀察者,因此在這種情況下使用 CSS 選取器時,會對網頁的實作方式產生隱藏假設,而 Puppeteer 指令碼無法控制。

因此,這類指令碼可能不夠穩定,容易受到原始碼變更的影響。舉例來說,假設有人使用 Puppeteer 指令碼,對包含節點 <button>Submit</button> 做為 body 元素第三個子項的網頁應用程式進行自動化測試。測試案例中的程式碼片段可能如下所示:

const button = await page.$('body:nth-child(3)'); // problematic selector
await button.click();

在這裡,我們使用選取器 'body:nth-child(3)' 尋找提交按鈕,但這與這個版本的網頁緊密繫結。如果之後在按鈕上方新增元素,這個選取器就會失效!

這對測試編寫者來說並非新鮮事:Puppeteer 使用者已嘗試挑選能因應這類變更的選取器。Puppetaria 就是我們為使用者提供的全新工具。

Puppeteer 現在隨附替代查詢處理常式,可查詢無障礙樹狀結構,而不必依賴 CSS 選取器。這裡的基本原則是,如果我們要選取的具體元素未變更,則對應的無障礙節點也不應變更。

我們將這類選取器稱為「ARIAARIA 選取器」,並支援查詢無障礙樹狀結構的計算無障礙元素名稱和角色。與 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 開發人員工具通訊協定 (CDP) 公開偵錯介面。這會透過與語言無關的介面,公開「重新載入頁面」或「在頁面中執行這段 JavaScript 並傳回結果」等功能。

開發人員工具前端和 Puppeteer 都使用 CDP 與瀏覽器通訊。為了實作 CDP 指令,Chrome 的所有元件 (瀏覽器、轉譯器等) 都內建開發人員工具基礎架構。CDP 會負責將指令轉送至正確的位置。

Puppeteer 動作 (例如查詢、點擊和評估運算式) 是透過 CDP 指令 (例如 Runtime.evaluate) 執行,該指令會直接在網頁內容中評估 JavaScript 並傳回結果。其他 Puppeteer 動作 (例如模擬色覺障礙、擷取螢幕截圖或擷取追蹤記錄) 則會使用 CDP 直接與 Blink 轉譯器程序通訊。

CDP

這表示我們有兩種方式可以實作查詢功能:

  • 使用 JavaScript 編寫查詢邏輯,並透過 Runtime.evaluate 插入網頁,或
  • 使用 CDP 端點,直接在 Blink 程序中存取及查詢無障礙樹狀結構。

我們實作了 3 個原型:

  • JS DOM 遍歷:將 JavaScript 插入網頁
  • Puppeteer AXTree 遍歷:根據現有的 CDP 存取權,存取無障礙功能樹狀結構
  • CDP DOM 遍歷:使用專為查詢無障礙樹狀結構而建構的新 CDP 端點

JS DOM 遍歷

這個原型會完整遍歷 DOM,並使用 element.computedName 和 element.computedRole (受 ComputedAccessibilityInfo 啟動旗標限制),在遍歷期間擷取每個元素的名稱和角色。

Puppeteer AXTree 遍歷

在這裡,我們改為透過 CDP 擷取完整的無障礙樹狀結構,並在 Puppeteer 中周遊。產生的無障礙節點會對應至 DOM 節點。

CDP DOM 遍歷

在這個原型中,我們實作了新的 CDP 端點,專門用於查詢無障礙樹狀結構。這樣一來,查詢作業就能透過 C++ 實作在後端進行,而不是透過 JavaScript 在網頁內容中進行。

單元測試基準

下圖比較了 3 個原型查詢 4 個元素 1000 次的總執行時間。基準測試在 3 種不同的設定下執行,頁面大小和是否啟用無障礙元素快取功能皆不相同。

基準:查詢四個元素 1000 次的總執行時間

很明顯,CDP 支援的查詢機制與僅在 Puppeteer 中實作的另外兩種機制之間,存在相當大的效能差距,而且相對差異似乎會隨著頁面大小而急劇增加。有趣的是,啟用無障礙快取後,JS DOM 遍歷原型的回應速度明顯提升。停用快取後,系統會視需要計算無障礙樹狀結構,並在每次互動後捨棄樹狀結構 (如果網域已停用)。啟用網域後,Chromium 會改為快取計算樹。

在 JS DOM 遍歷過程中,我們會要求每個元素的無障礙元素名稱和角色,因此如果停用快取,Chromium 會計算並捨棄我們造訪的每個元素的無障礙樹狀結構。另一方面,如果是以 CDP 為基礎的方法,則只有在每次呼叫 CDP (即每次查詢) 之間,才會捨棄樹狀結構。啟用快取功能也有助於提升這些方法的效能,因為無障礙樹狀結構會在 CDP 呼叫之間保留,但效能提升幅度相對較小。

雖然啟用快取似乎是理想的做法,但會耗用額外記憶體用量。如果 Puppeteer 指令碼會記錄追蹤檔案,這可能會造成問題。因此,我們決定不預設啟用無障礙功能樹狀結構快取。使用者可以啟用 CDP Accessibility 網域,自行開啟快取功能。

開發人員工具測試套件基準

先前的基準測試顯示,在 CDP 層實作查詢機制,可提升臨床單元測試情境的效能。

為了確認差異是否足夠明顯,在更實際的情況下執行完整測試套件時,我們修補了開發人員工具端對端測試套件,以便使用 JavaScript 和 CDP 原型,並比較執行階段。在這項基準測試中,我們將總共 43 個選取器從 [aria-label=…] 變更為自訂查詢處理常式 aria/…,然後使用每個原型實作。

測試指令碼會多次使用部分選取器,因此每次執行套件時,aria 查詢處理常式的實際執行次數為 113 次。查詢選項總數為 2253,因此只有一小部分查詢選項是透過原型產生。

基準:e2e 測試套件

如上圖所示,總執行時間有明顯差異。資料雜訊過多,無法得出任何具體結論,但很明顯,兩個原型的效能差距在這個情境中也同樣存在。

新的 CDP 端點

根據上述基準,且由於一般不建議採用啟動旗標的做法,我們決定繼續實作新的 CDP 指令,用於查詢無障礙樹狀結構。接下來,我們必須找出這個新端點的介面。

在 Puppeteer 的用途中,端點需要採用所謂的 RemoteObjectIds 做為引數,且為了讓我們之後能找到對應的 DOM 元素,端點應傳回包含 DOM 元素 backendNodeIds 的物件清單。

如下圖所示,我們嘗試了多種方法來滿足這個介面。我們發現,傳回物件的大小 (即是否傳回完整的無障礙節點,或僅傳回 backendNodeIds) 並沒有明顯差異。另一方面,我們發現使用現有的 NextInPreOrderIncludingIgnored 實作周遊邏輯並非理想選擇,因為這會導致速度明顯變慢。

基準:比較以 CDP 為基礎的 AXTree 遍歷原型

總結

現在,有了 CDP 端點,我們在 Puppeteer 端實作了查詢處理常式。這裡的主要工作是重構查詢處理程式碼,讓查詢能直接透過 CDP 解析,而不是透過在網頁內容中評估的 JavaScript 查詢。

後續步驟

新的 aria 處理常式隨附於 Puppeteer 5.4.0 版,做為內建查詢處理常式。我們很期待看到使用者如何將這項功能融入測試指令碼,也迫不及待想聽聽你的想法,瞭解如何讓這項功能更實用!

下載預覽管道

建議使用 Chrome Canary、開發人員版或 Beta 版做為預設開發瀏覽器。您可以透過這些預先發布版管道,使用最新的開發人員工具功能、測試先進的網路平台 API,並在使用者發現網站問題前,搶先找出問題!

聯絡 Chrome 開發人員工具團隊

您可以透過下列管道,討論新功能、更新或任何與開發人員工具相關的事項。