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

Johan Bay
Johan Bay

Puppeteer và cách tiếp cận của Puppeteer đối với bộ chọn

Puppeteer là một thư viện tự động hoá trình duyệt 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. Việc tự động hoá nhiệm vụ này về cơ bản là 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 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ỉ giới hạn ở 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 khi duy trì các hoạt động tương tác của 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 cú pháp; chúng liên kết chặt chẽ với hoạt động bên trong của bản trình bày văn bản của cây DOM theo nghĩa là chúng tham chiếu mã nhận dạng và tên lớp từ DOM. Do đó, các thuộc tính 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 vào 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à trình quan sát bên ngoài của một trang, vì vậy, khi bộ chọn CSS được dùng trong bối 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ệ quả là các tập lệnh như vậy có thể không ổn định và dễ bị ảnh hưởng bởi các thay đổi về mã nguồn. Ví dụ: giả sử một người 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ã trong 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 bộ chọn này liên kết chặt chẽ với chính phiên bản này của trang web. Nếu sau đó một phần tử được thêm vào phía trên nút, thì bộ chọn này sẽ không còn hoạt động nữa!

Đây không phải là tin mới đối với người viết kiểm thử: Người dùng Puppeteer đã cố gắng chọn các bộ chọn mạnh mẽ trước 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 một 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. Triết lý 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 thay đổi.

Chúng tôi đặt tên cho các bộ chọn như vậy là "bộ chọn ARIA" và hỗ trợ truy vấn tên thành phần hỗ trợ tiếp cận và vai trò đượ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ó tính ngữ nghĩa. Các vai trò 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 là tên thành phần hỗ trợ tiếp cận:

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ì thử nghiệm 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, trái ngược với cách trình bày trực quan hoặc cách cấu trúc của nút trong DOM. Các thử nghiệm của chúng tôi sẽ cảnh báo cho chúng tôi về những thay đổi đó để đả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ử có khả năng thích ứng tốt hơn với các thay đổi về mã nguồn.
  • Giúp tập lệnh kiểm thử dễ đọc hơn (tên thành phần hỗ trợ tiếp cận là các chỉ báo ngữ nghĩa).
  • Khuyến khích 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 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 tôi muốn cho phép truy vấn các phần tử theo tên thành phần hỗ trợ tiếp cận và vai trò của chúng. Đây là các thuộc tính của cây hỗ trợ tiếp cận, một cây kép cho 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 thông số kỹ thuật để tính toán tên thành phần hỗ trợ tiếp cận, rõ ràng là việc tính toán tên cho một phần tử không phải là một nhiệm vụ đơn giản. Vì vậy, ngay từ đầu, chúng tôi đã quyết định 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ó một số cách để triển khai truy vấn ARIA trong Puppeteer. Để 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ị giao diện gỡ lỗi thông qua một giao thức có tên là Giao thức Chrome DevTools (CDP). Thao tác này cho thấy các chức năng như "tải lại trang" hoặc "thực thi đoạn JavaScript này trong trang và trả về kết quả" thông qua một giao diện không phụ thuộc vào ngôn ngữ.

Cả giao diện người dùng Công cụ cho nhà phát triển 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 Công cụ cho nhà phát triển 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 đảm nhận việc định tuyến các lệnh đến đúng nơi.

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 trực tiếp 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 tình trạng mù màu, chụp ảnh màn hình hoặc ghi lại dấu vết đều sử dụng CDP để giao tiếp trực tiếp với tiến trình kết xuất Blink.

CDP

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

  • Viết logic truy vấn 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:

  • Duyệt qua DOM bằng JavaScript – dựa trên việc chèn JavaScript vào trang
  • Duyệt 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
  • Duyệt qua DOM CDP – sử dụng một điểm cuối CDP mới được xây dựng riêng để truy vấn cây hỗ trợ tiếp cận

Duyệt qua DOM JS

Nguyên mẫu này duyệt qua toàn bộ DOM và sử dụng element.computedName và element.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.

Duyệt qua AXTree của Puppeteer

Tại đây, chúng tôi truy xuất toàn bộ cây hỗ trợ tiếp cận thông qua CDP và truyền tải 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.

Truyền tải DOM 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, việc truy vấn có thể diễn ra ở phần phụ trợ thông qua quá trình 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ử 1.000 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à liệu có bật tính năng lưu vào bộ nhớ đệm của các phần tử hỗ trợ tiếp cận 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ử 1.000 lần

Rõ ràng là có sự chênh lệch đáng kể về hiệu suất giữa cơ chế truy vấn dựa trên CDP và hai cơ chế còn lại chỉ được triển khai trong Puppeteer, và sự khác biệt tương đối dường như tăng đáng kể theo kích thước trang. Thật thú vị khi thấy nguyên mẫu truyền tải 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ần tương tác nếu miền bị tắt. Khi bạn bật miền, Chromium sẽ lưu cây đã tính toán vào bộ nhớ đệm.

Đối với việc truyền tải DOM JS, chúng tôi yêu cầu tên thành phần hỗ trợ tiếp cận và vai trò cho mọi phần tử trong quá trình truyền tải. 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 tôi 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. Những 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ì cây hỗ trợ tiếp cận sau đó sẽ được duy trì trong các lệnh gọi CDP, nhưng do đó, mức tăng hiệu suất sẽ 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ẻ phù hợp trong trường hợp này, nhưng việc này 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ộ kiểm thử Công cụ cho nhà phát triển

Đ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 một tình huống 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 đã vá bộ kiểm thử toàn diện của Công cụ cho nhà phát triển để 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 đ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 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 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 cho 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ỉ có một phần nhỏ các 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ệt về tổng thời gian chạy. Dữ liệu quá nhiễu nên không thể đưa ra kết luận cụ thể, nhưng rõ ràng là khoảng cách về hiệu suất giữa hai nguyên mẫu cũng thể 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 thường không mong muố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 tôi phải tìm ra 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 này sẽ trả về 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 rằng 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ề các nút hỗ trợ tiếp cận đầy đủ hay chỉ backendNodeIds) không tạo ra sự khác biệt rõ rệt. Mặt khác, chúng tôi nhận thấy rằng 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 truyền tải ở đây, vì điều đó khiến tốc độ chậm lại đáng kể.

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

Tổng kết

Giờ đây, khi đã có điểm cuối CDP, chúng tôi đã triển khai trình xử lý truy vấn ở phía Puppeteer. Công việc khó khăn nhất ở đây là tái cấu trúc mã xử lý truy vấn để cho phép các 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 cung cấp cùng với Puppeteer phiên bản 5.4.0 dưới dạng trình xử lý truy vấn tích hợp sẵn. Chúng tôi rất mong được thấy 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ọ và rất mong được nghe ý kiến của bạn về cách chúng tôi có thể cải thiện tính năng này 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 truy cập vào các tính năng mới nhất của Công cụ cho nhà phát triển, cho phép bạn thử nghiệm các API nền tảng web tiên tiến và giúp bạn tìm thấy các vấn đề trên trang web của mình trước khi người dùng gặp phải!

Liên hệ với nhóm Chrome DevTools

Hãy sử dụng các lựa chọn sau để thảo luận về các tính năng mới, bản cập nhật 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.