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, tác vụ nổi bật nhất của trình duyệt là duyệt 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 xác định 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 cách trình bày 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 đó, chúng cung cấp một công cụ không thể thiếu cho các 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 ngữ 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 các tập lệnh Puppeteer để kiểm thử tự động cho một ứng dụng web có 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 đó 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 hoạt động nữa!
Đây không phải là tin tức để kiểm tra người viết: Người dùng Puppeteer đã cố gắng chọn các bộ chọn có khả năng chịu đượ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 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
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ì 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 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 hỗ trợ tiếp cận là chỉ số mô tả 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ư đã đề cập ở trên, chúng ta muốn bật các phần tử truy vấn theo tên và vai trò dễ tiếp cận 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 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.
Từ việc xem thông số kỹ thuật để tính toán tên thành phần hỗ trợ tiếp cận, chúng ta thấy rõ rằng việc tính toán tên cho một phần tử là một công việc không hề nhỏ. Do đó, ngay từ đầu, chúng ta đã 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úng ta chỉ sử dụng cây hỗ trợ tiếp cận của Chromium, chúng ta vẫn có khá nhiều 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ị 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 sẽ hiển thị chức năng như "tải lại trang" hoặc "thực thi đoạn mã JavaScript này trên trang và trả lại 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à Trình chỉnh sửa đều 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 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.
Điều này cho phép chúng ta triển khai chức năng truy vấn theo hai đường dẫ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
- Truyền tải DOM CDP – sử dụng một điểm cuối CDP mới được tạo ra để 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.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.
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 ánh xạ tớ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, 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 để 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à 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.
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 rằng nguyên mẫu truyền tải DOM JS đáp ứng rất tốt việc bật chức 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 tại lớp CDP giúp tăng hiệu suất trong kịch bản 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 các tập lệnh kiểm thử, do đó, 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ộ công cụ. 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.
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 đối số có tên RemoteObjectIds
làm đối số. Để có thể tìm thấy các phần tử DOM tương ứng sau đó, điểm cuối sẽ trả về danh sách đố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à liệu chúng tôi có trả về các nút hỗ trợ tiếp cận đầy đủ hay chỉ có 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ể.
Kết thúc
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.
- Gửi ý kiến phản hồi và yêu cầu về tính năng cho chúng tôi tại crbug.com.
- Báo cáo sự cố của Công cụ cho nhà phát triển bằng cách sử dụng biểu tượng Tuỳ chọn khác > Trợ giúp > Báo cáo sự cố của Công cụ cho nhà phát triển trong Công cụ cho nhà phát triển.
- Gửi tweet đến @ChromeDevTools.
- Để lại bình luận trên video YouTube về tính năng mới trong DevTools hoặc video YouTube về mẹo sử dụng DevTools.