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 元素的第 3 個子項。測試案例的一個程式碼片段可能會像這樣:

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 開發人員工具通訊協定 (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.computedNameelement.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 無障礙功能網域,自行開啟快取功能。

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

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

為了瞭解差異是否明顯到在執行完整測試套件的更實際情境中可察覺,我們修補 DevTools 端對端測試套件,以便使用 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 版設為預設開發人員版瀏覽器。這些預覽管道可讓您存取最新的 DevTools 功能,測試最新的網路平台 API,並在使用者發現問題前,協助您找出網站的問題!

與 Chrome 開發人員工具團隊聯絡

請使用下列選項討論新功能、更新或任何與開發人員工具相關的內容。