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 選取器) 為依據。這裡的基本原則是,如果我們要選取的具體元素未變更,那麼相應的無障礙節點也不應變更。

我們將這類選取器命名為「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 會負責將指令轉送至正確位置。

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

客戶資料平台

已經得到實作查詢功能的兩個路徑;我們可以:

  • 使用 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 端點。如此一來,查詢便能透過 C++ 實作,在後端進行,而非透過 JavaScript 在網頁環境中查詢。

單元測試基準測試

下圖比較針對這 3 種原型,查詢四個元素 1000 次的總執行階段。基準測試在 3 種不同的設定中執行,因網頁大小和是否已啟用無障礙元素快取功能而有差異。

基準:查詢四個元素 1,000 次的總執行階段

顯然,在 CDP 支援的查詢機制中,另外兩個只導入 Puppeteer 的查詢機制之間存在顯著的效能落差,而相對差異似乎隨著頁面大小而大幅增加。如看到 JS DOM 週遊原型提供許多回應,以啟用無障礙快取功能,有點有趣。停用快取功能後,系統會依需求計算無障礙功能樹狀結構,並在網域停用後在每次互動後捨棄樹狀結構。啟用網域後,Chromium 會改為快取這些運算的樹狀結構。

在 JS DOM 週遊中,我們會要求每個元素的可存取名稱和角色,因此如果停用快取,Chromium 就會為我們造訪的每個元素計算並捨棄無障礙樹狀結構。另一方面,如果是以 CDP 為基礎的方法,系統只會在每次呼叫 CDP 之間 (亦即每個查詢) 捨棄樹狀結構。這些方法也受益於啟用快取功能,因為系統隨後會在 CDP 呼叫中保留無障礙樹狀結構,但效能的提升會相對較小。

您或許會想啟用快取功能,但這麼做會增加額外的記憶體用量。但對於會記錄追蹤檔的 Puppeteer 指令碼,這可能會造成問題。因此,我們決定不預設啟用無障礙樹狀結構快取功能。使用者可以啟用 CDP 無障礙網域,自行開啟快取功能。

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

先前的基準測試結果顯示,在 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 v5.4.0 做為內建查詢處理常式。我們期待能看到使用者採用這項功能做為測試指令碼,也迫不及待想聽聽您的想法,讓這項功能更實用!

下載預覽頻道

建議您使用 Chrome CanaryDevBeta 版做為預設的開發瀏覽器。透過這些預覽版本,您可以存取開發人員工具中的最新功能、測試最先進的網路平台 API,以及找出網站的問題,以免使用者發現問題。

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

請使用下列選項,討論貼文中的新功能和異動,或與開發人員工具相關的其他事項。

  • 歡迎透過 crbug.com 提出建議或意見。
  • 在開發人員工具中,依序點選「更多選項」圖示 更多   >「說明」 >「回報開發人員工具問題」,回報開發人員工具問題。
  • 前往 @ChromeDevTools 張貼 Tweet。
  • 歡迎在「開發人員工具」推出「最新消息」YouTube 影片或「開發人員工具秘訣」YouTube 影片留言。