Puppetaria: tập lệnh Puppeteer ưu tiên hỗ trợ tiếp cận

Johan Bay
Johan Bay

Puppeteer và phương pháp tiếp cận bộ chọn

Puppeteer là một thư viện tự động hoá trình duyệt dành cho Node: thư viện này cho phép bạn kiểm soát trình duyệt bằng một API JavaScript đơn giản và hiện đại.

Tất nhiên, nhiệm vụ nổi bật nhất của trình duyệt là duyệt các trang web. Về cơ bản, việc tự động hoá tác vụ này tương đương với việc tự động hoá các hoạt động tương tác với trang web.

Trong Puppeteer, bạn có thể thực hiện việc này bằng cách truy vấn các phần tử DOM bằng bộ chọn dựa trên chuỗi và thực hiện các thao tác như nhấp hoặc nhập văn bản trên các phần tử đó. Ví dụ: một tập lệnh mở developer.google.com, tìm hộp tìm kiếm và tìm kiếm puppetaria có thể có dạng như sau:

(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');
 })();

Do đó, cách xác định các phần tử bằng bộ chọn truy vấn là một phần quan trọng trong trải nghiệm Puppeteer. Cho đến nay, các bộ chọn trong Puppeteer chỉ được giới hạn ở các bộ chọn CSS và XPath. Mặc dù có khả năng biểu đạt rất mạnh mẽ, nhưng các bộ chọn này có thể có nhược điểm là duy trì các hoạt động tương tác với trình duyệt trong tập lệnh.

Bộ chọn cú pháp so với bộ chọn ngữ nghĩa

Bộ chọn CSS có bản chất là cú pháp; chúng liên kết chặt chẽ với hoạt động bên trong của nội dung đại diện dạng văn bản của cây DOM theo nghĩa là các bộ chọn này tham chiếu đến mã nhận dạng và tên lớp từ DOM. Do đó, các công cụ này cung cấp một công cụ không thể thiếu cho nhà phát triển web để sửa đổi hoặc thêm kiểu cho một phần tử trong trang, nhưng trong bối cảnh đó, nhà phát triển có toàn quyền kiểm soát trang và cây DOM của trang.

Mặt khác, tập lệnh Puppeteer là một trình quan sát bên ngoài của một trang, vì vậy, khi bộ chọn CSS được sử dụng trong ngữ cảnh này, tập lệnh này sẽ đưa ra các giả định ẩn về cách triển khai trang mà tập lệnh Puppeteer không kiểm soát được.

Hậu quả là các tập lệnh như vậy có thể dễ bị lỗi và dễ bị thay đổi mã nguồn. Ví dụ: giả sử bạn sử dụng tập lệnh Puppeteer để kiểm thử tự động cho một ứng dụng web chứa nút <button>Submit</button> làm phần tử con thứ ba của phần tử body. Một đoạn mã từ một trường hợp kiểm thử có thể có dạng như sau:

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

Ở đây, chúng ta đang sử dụng bộ chọn 'body:nth-child(3)' để tìm nút gửi, nhưng nút này liên kết chặt chẽ với phiên bản trang web này. Nếu sau này bạn thêm một phần tử phía trên nút, bộ chọn này sẽ không hoạt động nữa!

Đây không phải là tin tức mới đối với các nhà viết kiểm thử: người dùng Puppeteer đã cố gắng chọn bộ chọn có khả năng chống lại những thay đổi như vậy. Với Puppetaria, chúng tôi cung cấp cho người dùng một công cụ mới trong hành trình này.

Puppeteer hiện đi kèm với trình xử lý truy vấn thay thế dựa trên việc truy vấn cây hỗ trợ tiếp cận thay vì dựa vào bộ chọn CSS. Nguyên tắc cơ bản ở đây là nếu phần tử cụ thể mà chúng ta muốn chọn không thay đổi, thì nút hỗ trợ tiếp cận tương ứng cũng không được thay đổi.

Chúng tôi đặt tên cho các bộ chọn đó là "bộ chọn ARIA" và hỗ trợ truy vấn tên và vai trò có thể truy cập được đã tính toán của cây hỗ trợ tiếp cận. So với bộ chọn CSS, các thuộc tính này có bản chất ngữ nghĩa. Các thuộc tính này không liên quan đến các thuộc tính cú pháp của DOM mà là các chỉ số mô tả cách trang được quan sát thông qua các công nghệ hỗ trợ như trình đọc màn hình.

Trong ví dụ về tập lệnh kiểm thử ở trên, chúng ta có thể sử dụng bộ chọn aria/Submit[role="button"] để chọn nút mong muốn, trong đó Submit tham chiếu đến tên có thể truy cập của phần tử:

const button = await page.$('aria/Submit[role="button"]');
await button.click();

Bây giờ, nếu sau này chúng ta quyết định thay đổi nội dung văn bản của nút từ Submit thành Done, thì kiểm thử sẽ lại không thành công, nhưng trong trường hợp này, điều đó là mong muốn; bằng cách thay đổi tên của nút, chúng ta thay đổi nội dung của trang, thay vì cách trình bày trực quan hoặc cách nút được cấu trúc trong DOM. Các thử nghiệm của chúng tôi sẽ cảnh báo chúng tôi về những thay đổi như vậy để đảm bảo rằng những thay đổi đó là có chủ ý.

Quay lại ví dụ lớn hơn với thanh tìm kiếm, chúng ta có thể tận dụng trình xử lý aria mới và thay thế

const search = await page.$('devsite-search > form > div.devsite-search-container');

với

const search = await page.$('aria/Open search[role="button"]');

để tìm thanh tìm kiếm!

Nói chung, chúng tôi tin rằng việc sử dụng các bộ chọn ARIA như vậy có thể mang lại những lợi ích sau cho người dùng Puppeteer:

  • Giúp bộ chọn trong tập lệnh kiểm thử linh hoạt hơn trước các thay đổi về mã nguồn.
  • Giúp tập lệnh kiểm thử dễ đọc hơn (tên có thể truy cập là chỉ số ngữ nghĩa).
  • Thúc đẩy các phương pháp hay để chỉ định thuộc tính hỗ trợ tiếp cận cho các phần tử.

Phần còn lại của bài viết này sẽ đi sâu vào thông tin chi tiết về cách chúng tôi triển khai dự án Puppetaria.

Quy trình thiết kế

Thông tin khái quát

Như đã nêu ở trên, chúng ta muốn cho phép truy vấn các phần tử theo tên và vai trò có thể truy cập. Đây là các thuộc tính của cây hỗ trợ tiếp cận, một cây kép của cây DOM thông thường, được các thiết bị như trình đọc màn hình sử dụng để hiển thị trang web.

Khi xem xét thông số kỹ thuật để tính toán tên có thể truy cập, rõ ràng là việc tính toán tên cho một phần tử là một nhiệm vụ không hề đơn giản. Vì vậy, ngay từ đầu, chúng tôi đã quyết định muốn sử dụng lại cơ sở hạ tầng hiện có của Chromium cho việc này.

Cách chúng tôi triển khai

Ngay cả khi chỉ sử dụng cây hỗ trợ tiếp cận của Chromium, chúng ta vẫn có thể triển khai truy vấn ARIA trong Puppeteer theo một số cách. Để biết lý do, trước tiên, hãy xem cách Puppeteer kiểm soát trình duyệt.

Trình duyệt hiển thị một giao diện gỡ lỗi thông qua một giao thức có tên là Giao thức Công cụ của Chrome cho nhà phát triển (CDP). Thao tác này hiển thị chức năng như "tải lại trang" hoặc "thực thi đoạn mã JavaScript này trong trang và trả về kết quả" thông qua giao diện không phân biệt ngôn ngữ.

Cả giao diện người dùng DevTools và Puppeteer đều đang sử dụng CDP để giao tiếp với trình duyệt. Để triển khai các lệnh CDP, có cơ sở hạ tầng DevTools bên trong tất cả các thành phần của Chrome: trong trình duyệt, trong trình kết xuất, v.v. CDP sẽ xử lý việc định tuyến các lệnh đến đúng vị trí.

Các thao tác của Puppeteer như truy vấn, nhấp và đánh giá biểu thức được thực hiện bằng cách tận dụng các lệnh CDP như Runtime.evaluate để đánh giá JavaScript ngay trong ngữ cảnh trang và trả về kết quả. Các thao tác khác của Puppeteer như mô phỏng chứng thiếu thị lực màu, chụp ảnh màn hình hoặc ghi lại dấu vết sử dụng CDP để giao tiếp trực tiếp với quy trình kết xuất Blink.

CDP

Điều này đã cho chúng ta hai cách để triển khai chức năng truy vấn; chúng ta có thể:

  • Viết logic truy vấn của chúng ta bằng JavaScript và chèn logic đó vào trang bằng Runtime.evaluate, hoặc
  • Sử dụng điểm cuối CDP có thể truy cập và truy vấn trực tiếp cây hỗ trợ tiếp cận trong quy trình Blink.

Chúng tôi đã triển khai 3 nguyên mẫu:

  • Di chuyển qua DOM JS – dựa trên việc chèn JavaScript vào trang
  • Di chuyển qua Puppeteer AXTree – dựa trên việc sử dụng quyền truy cập CDP hiện có vào cây hỗ trợ tiếp cận
  • Di chuyển qua DOM CDP – sử dụng một điểm cuối CDP mới được thiết kế riêng để truy vấn cây hỗ trợ tiếp cận

Di chuyển qua DOM JS

Nguyên mẫu này thực hiện việc duyệt qua toàn bộ DOM và sử dụng element.computedNameelement.computedRole, được kiểm soát bằng cờ khởi chạy ComputedAccessibilityInfo, để truy xuất tên và vai trò của từng phần tử trong quá trình duyệt qua.

Di chuyển qua Puppeteer AXTree

Thay vào đó, chúng ta truy xuất toàn bộ cây hỗ trợ tiếp cận thông qua CDP và duyệt qua cây đó trong Puppeteer. Sau đó, các nút hỗ trợ tiếp cận thu được sẽ được liên kết với các nút DOM.

Di chuyển qua DOM của CDP

Đối với nguyên mẫu này, chúng tôi đã triển khai một điểm cuối CDP mới dành riêng cho việc truy vấn cây hỗ trợ tiếp cận. Bằng cách này, quá trình truy vấn có thể diễn ra ở phần phụ trợ thông qua việc triển khai C++ thay vì trong ngữ cảnh trang thông qua JavaScript.

Điểm chuẩn kiểm thử đơn vị

Hình sau đây so sánh tổng thời gian chạy của việc truy vấn 4 phần tử 1000 lần cho 3 nguyên mẫu. Điểm chuẩn được thực thi trong 3 cấu hình khác nhau, thay đổi kích thước trang và việc có bật tính năng lưu các thành phần hỗ trợ tiếp cận vào bộ nhớ đệm hay không.

Điểm chuẩn: Tổng thời gian chạy của việc truy vấn 4 phần tử 1000 lần

Rõ ràng là có một khoảng cách hiệu suất đáng kể giữa cơ chế truy vấn do CDP hỗ trợ và hai cơ chế khác chỉ được triển khai trong Puppeteer, và sự khác biệt tương đối dường như tăng lên đáng kể theo kích thước trang. Thật thú vị khi thấy nguyên mẫu duyệt DOM JS phản hồi rất tốt khi bật tính năng lưu vào bộ nhớ đệm hỗ trợ tiếp cận. Khi bạn tắt tính năng lưu vào bộ nhớ đệm, cây hỗ trợ tiếp cận sẽ được tính toán theo yêu cầu và loại bỏ cây sau mỗi lượt tương tác nếu miền bị tắt. Khi bạn bật miền, Chromium sẽ lưu cây được tính toán vào bộ nhớ đệm.

Đối với việc duyệt qua DOM JS, chúng ta yêu cầu tên và vai trò có thể truy cập cho mọi phần tử trong quá trình duyệt qua. Vì vậy, nếu tính năng lưu vào bộ nhớ đệm bị tắt, Chromium sẽ tính toán và loại bỏ cây hỗ trợ tiếp cận cho mọi phần tử mà chúng ta truy cập. Mặt khác, đối với các phương pháp dựa trên CDP, cây chỉ bị loại bỏ giữa mỗi lệnh gọi đến CDP, tức là cho mỗi truy vấn. Các phương pháp này cũng được hưởng lợi từ việc bật tính năng lưu vào bộ nhớ đệm, vì sau đó, cây hỗ trợ tiếp cận sẽ được duy trì trên các lệnh gọi CDP, nhưng mức tăng hiệu suất tương đối nhỏ hơn.

Mặc dù việc bật tính năng lưu vào bộ nhớ đệm có vẻ như là điều nên làm ở đây, nhưng việc này cũng sẽ làm tăng mức sử dụng bộ nhớ. Đối với các tập lệnh Puppeteer, chẳng hạn như ghi lại tệp theo dõi, điều này có thể gây ra vấn đề. Do đó, chúng tôi quyết định không bật tính năng lưu cây hỗ trợ tiếp cận vào bộ nhớ đệm theo mặc định. Người dùng có thể tự bật tính năng lưu vào bộ nhớ đệm bằng cách bật Miền hỗ trợ tiếp cận của CDP.

Điểm chuẩn của bộ thử nghiệm DevTools

Điểm chuẩn trước đó cho thấy việc triển khai cơ chế truy vấn của chúng tôi ở lớp CDP giúp tăng hiệu suất trong trường hợp kiểm thử đơn vị lâm sàng.

Để xem liệu sự khác biệt có đủ rõ ràng để nhận thấy trong một tình huống thực tế hơn khi chạy một bộ kiểm thử đầy đủ hay không, chúng tôi đã sửa bộ kiểm thử toàn diện của DevTools để sử dụng các nguyên mẫu dựa trên JavaScript và CDP, đồng thời so sánh thời gian chạy. Trong phép đo điểm chuẩn này, chúng tôi đã thay đổi tổng cộng 43 bộ chọn từ [aria-label=…] thành một trình xử lý truy vấn tuỳ chỉnh aria/…, sau đó chúng tôi triển khai bằng cách sử dụng từng nguyên mẫu.

Một số bộ chọn được sử dụng nhiều lần trong tập lệnh kiểm thử, vì vậy, số lần thực thi thực tế của trình xử lý truy vấn aria là 113 lần mỗi lần chạy bộ kiểm thử. Tổng số lựa chọn truy vấn là 2253, vì vậy, chỉ một phần nhỏ lựa chọn truy vấn xảy ra thông qua các nguyên mẫu.

Điểm chuẩn: bộ kiểm thử toàn diện

Như trong hình trên, có sự khác biệt rõ ràng về tổng thời gian chạy. Dữ liệu quá nhiễu để kết luận bất kỳ điều gì cụ thể, nhưng rõ ràng là khoảng cách hiệu suất giữa hai nguyên mẫu cũng xuất hiện trong trường hợp này.

Điểm cuối CDP mới

Dựa trên các điểm chuẩn ở trên và vì phương pháp dựa trên cờ khởi chạy nói chung là không mong muốn, nên chúng tôi quyết định tiếp tục triển khai một lệnh CDP mới để truy vấn cây hỗ trợ tiếp cận. Bây giờ, chúng ta phải tìm hiểu giao diện của điểm cuối mới này.

Đối với trường hợp sử dụng trong Puppeteer, chúng ta cần điểm cuối lấy RemoteObjectIds làm đối số và để cho phép chúng ta tìm các phần tử DOM tương ứng sau đó, điểm cuối sẽ trả về một danh sách các đối tượng chứa backendNodeIds cho các phần tử DOM.

Như trong biểu đồ bên dưới, chúng tôi đã thử một số phương pháp để đáp ứng giao diện này. Từ đó, chúng tôi nhận thấy kích thước của các đối tượng được trả về, tức là việc chúng tôi trả về toàn bộ nút hỗ trợ tiếp cận hay chỉ backendNodeIds không tạo ra sự khác biệt rõ ràng. Mặt khác, chúng tôi nhận thấy việc sử dụng NextInPreOrderIncludingIgnored hiện có là một lựa chọn không phù hợp để triển khai logic duyệt qua ở đây, vì điều đó làm chậm đáng kể.

Điểm chuẩn: So sánh các nguyên mẫu truy cập AXTree dựa trên CDP

Tóm tắt

Giờ đây, với điểm cuối CDP, chúng ta đã triển khai trình xử lý truy vấn ở bên Puppeteer. Công việc chính ở đây là tái cấu trúc mã xử lý truy vấn để cho phép truy vấn phân giải trực tiếp thông qua CDP thay vì truy vấn thông qua JavaScript được đánh giá trong ngữ cảnh trang.

Tiếp theo là gì?

Trình xử lý aria mới được vận chuyển cùng với Puppeteer v5.4.0 dưới dạng trình xử lý truy vấn tích hợp. Chúng tôi rất mong được xem cách người dùng áp dụng tính năng này vào tập lệnh kiểm thử của họ. Chúng tôi cũng rất mong được nghe ý kiến của bạn về cách chúng tôi có thể làm cho tính năng này trở nên hữu ích hơn nữa!

Tải các kênh xem trước xuống

Hãy cân nhắc sử dụng Chrome Canary, Dev hoặc Beta làm trình duyệt phát triển mặc định. Các kênh xem trước này cho phép bạn sử dụng các tính năng mới nhất của DevTools, kiểm thử các API nền tảng web tiên tiến và giúp bạn tìm thấy vấn đề trên trang web của mình trước khi người dùng phát hiện ra!

Liên hệ với nhóm Công cụ của Chrome cho nhà phát triển

Hãy sử dụng các lựa chọn sau để thảo luận về các tính năng, bản cập nhật mới hoặc bất kỳ nội dung nào khác liên quan đến Công cụ cho nhà phát triển.