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 選取器) 為依據。這裡的基礎原則是,如果我們要選取的具體元素沒有變更,則對應的無障礙性節點也不應變更。
我們將這類選取器稱為「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 轉譯程序通訊。
已經得到實作查詢功能的兩個路徑;我們可以:
- 使用 JavaScript 編寫查詢邏輯,然後使用
Runtime.evaluate
將其插入網頁,或 - 使用 CDP 端點,即可直接在 Blink 程序中存取及查詢無障礙樹狀結構。
我們實作了 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 端點。這樣一來,查詢就能透過 C++ 實作在後端執行,而非透過 JavaScript 在網頁內容中執行。
單元測試基準
下圖比較了 3 個原型設計中,查詢 4 個元素 1000 次的總執行時間。基準測試在 3 種不同的設定中執行,因網頁大小和是否已啟用無障礙元素快取功能而有差異。
很明顯,CDP 支援的查詢機制與僅在 Puppeteer 中實作的其他兩種機制之間存在相當大的效能差距,而且隨著網頁大小增加,相對差異似乎也大幅增加。有趣的是,JS DOM 檢視原型在啟用無障礙快取功能時,反應良好。停用快取後,系統會視需要計算無障礙樹狀結構,並在每個互動後捨棄樹狀結構 (如果網域已停用)。啟用網域後,Chromium 會改為快取已計算的樹狀結構。
針對 JS DOM 檢查,我們會在檢查期間要求每個元素的可存取名稱和角色,因此如果快取功能已停用,Chromium 會計算並捨棄每個造訪元素的可存取樹狀結構。另一方面,如果採用 CDP 為基礎的方法,樹狀結構只會在每次呼叫 CDP (也就是每次查詢) 之間捨棄。這些方法也受益於啟用快取功能,因為系統隨後會在 CDP 呼叫中保留無障礙樹狀結構,但效能的提升會相對較小。
雖然啟用快取功能似乎是個不錯的做法,但這會導致額外的記憶體用量。對於記錄追蹤記錄檔的 Puppeteer 指令碼,這可能會造成問題。因此,我們決定不預設啟用無障礙樹狀結構快取功能。使用者可以啟用 CDP 無障礙網域,自行開啟快取功能。
開發人員工具測試套件基準
先前的基準測試顯示,在 CDP 層實作查詢機制可在臨床單元測試情境中提升效能。
為了在執行完整測試套件的真實情境中,查看差異的讀數是否足以讓使用者註意到兩者的差異,我們修補了開發人員工具的端對端測試套件,以便利用 JavaScript 和 CDP 式的原型並比較執行階段。在這個基準測試中,我們將總共 43 個選取器從 [aria-label=…]
變更為自訂查詢處理常式 aria/…
,然後使用各個原型實作。
部分選取器會在測試指令碼中多次使用,因此 aria
查詢處理常式的實際執行次數為每次套件執行 113 次。查詢選取項目總數為 2253,因此只有部分查詢選取項目是透過原型設計產生。
如上圖所示,總放送時間有明顯差異。資料雜訊過多,無法得出任何具體結論,但很明顯的是,這兩個原型版本在這個情境中也顯示出效能差異。
新的 CDP 端點
基於上述基準,再加上以啟動旗標為基礎的做法,整體來說並不理想,因此我們決定往後實作新的 CDP 指令來查詢無障礙樹狀結構。我們現在必須找出這個新端點的介面。
在 Puppeteer 中,我們需要端點使用所謂的 RemoteObjectIds
做為引數,並在之後找出對應的 DOM 元素,因此端點應傳回物件清單,其中包含 DOM 元素的 backendNodeIds
。
如下圖所示,我們嘗試了許多方法來滿足這個介面。從這項測試,我們發現傳回物件的大小 (也就是傳回完整無障礙性節點或僅傳回 backendNodeIds
) 並沒有明顯差異。另一方面,我們發現使用現有的 NextInPreOrderIncludingIgnored
並不是實作遍歷邏輯的理想選擇,因為這會明顯減緩。
總結
有了 CDP 端點,我們便可在 Puppeteer 端實作查詢處理程序。這項工作的重點在於重新建構查詢處理程式碼,讓查詢能夠直接透過 CDP 解析,而非透過在網頁內容中評估的 JavaScript 進行查詢。
後續步驟
新的 aria
處理程序已隨 Puppeteer 5.4.0 版一併提供,做為內建查詢處理程序。我們期待看到使用者如何將這項功能納入測試指令碼,也期待聽到你對如何讓這項功能更實用的想法!
下載預覽管道
建議您將 Chrome Canary、開發人員版或Beta 版設為預設開發人員版瀏覽器。這些預覽版本可讓您存取開發人員工具中的最新功能、測試先進的網路平台 API,並協助您事先找出網站的問題!
與 Chrome 開發人員工具團隊聯絡
請使用下列選項討論新功能、更新或任何與開發人員工具相關的內容。
- 請前往 crbug.com 提交意見回饋和功能要求。
- 在開發人員工具中,依序按一下「更多選項」>「說明」>「回報開發人員工具的問題」,然後使用 回報開發人員工具的問題。
- 前往 @ChromeDevTools 張貼 Tweet。
- 在 YouTube 影片「What's new in DevTools」或「DevTools 提示」YouTube 影片中留言。