Puppetaria:無障礙優先的 Puppeteer 腳本

約翰灣
約翰灣

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 中的呈現方式,也不會改變圖片在 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 元件中都有 DevTools 基礎架構:瀏覽器、轉譯器等。CDP 會將指令轉送至正確位置。

實際操作 (例如查詢、點擊和評估運算式) 會使用 CDP 指令 (例如 Runtime.evaluate) 直接在網頁結構定義中評估 JavaScript,然後將結果放回結果。其他 Puppeter 動作 (例如模擬色覺障礙、擷取螢幕截圖或擷取追蹤記錄) 會使用 CDP 直接與 Blink 轉譯程序通訊。

客戶資料平台

此時,我們將只提供兩個實作查詢功能的路徑;我們可以:

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

我們導入了 3 種原型:

  • JS DOM 週遊 - 根據在網頁中插入 JavaScript 的做法
  • Puppeteer AXTree 遍歷 - 以現有的無障礙樹狀結構存取權為基礎
  • CDP DOM 週遊 - 使用專為查詢無障礙樹狀結構而建構的新 CDP 端點

JS DOM 週遊

這個原型會完整掃遍 DOM,並使用上列於 ComputedAccessibilityInfo 啟動標記element.computedNameelement.computedRole,在周遊期間擷取每個元素的名稱和角色。

布佩特 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 層實作我們的查詢機制可大幅提升臨床單元測試情境的效能。

為了瞭解不同差異的發音是否足以在執行完整測試套件的情境中顯而易見,我們修補了 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 v5.4.0 做為內建查詢處理常式提供。我們很期待看到使用者如何將這項工具納入測試指令碼,也迫不及待想知道如何讓這些更實用!

下載預覽頻道

建議您使用 Chrome Canary開發人員版Beta 版做為預設開發瀏覽器。這些預覽管道可讓您使用最新的開發人員工具、測試最先進的網路平台 API,以及在使用者操作之前在網站上找出問題!

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

使用下列選項,在文章中討論新功能和異動,或與開發人員工具相關的任何其他內容。

  • 透過 crbug.com 提供建議或意見。
  • 如要回報開發人員工具問題,請在開發人員工具中依序點選「更多選項」更多   >「說明」 >「回報開發人員工具的問題」
  • @ChromeDevTools 張貼推文。
  • 歡迎前往開發人員工具的 YouTube 影片或開發人員工具的 YouTube 影片提供新功能留言。