Puppetaria: skrypty Puppeteer stworzone z myślą o ułatwieniach dostępu

Zatoka Johana
Johan Bay

Lalkarz i jego podejście do selektorów

Puppeteer to biblioteka automatyzacji przeglądarki dla Node: umożliwia sterowanie przeglądarką przy użyciu prostego i nowoczesnego interfejsu API JavaScript.

Najważniejszym zadaniem przeglądarki jest oczywiście przeglądanie stron internetowych. Automatyzacja tego zadania polega zasadniczo na automatyzacji interakcji ze stroną internetową.

W Puppeteer można to osiągnąć, wysyłając zapytania o elementy DOM za pomocą selektorów opartych na ciągach znaków oraz wykonując takie czynności jak klikanie lub wpisywanie tekstu w elemencie. Na przykład skrypt, który otwiera stronę developer.google.com i znajduje pole wyszukiwania, a wyszukiwanie hasła puppetaria wygląda tak:

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

Sposób identyfikowania elementów za pomocą selektorów zapytań jest więc kluczowym elementem interfejsu Puppeteer. Do tej pory selektory w Puppeteer ograniczały się do selektorów CSS i XPath. Te selektory, chociaż są bardzo skuteczne, mają wady związane z trwałością interakcji przeglądarki w skryptach.

Selektory składniowe i semantyczne

Selektory CSS mają charakter składniowy – są ściśle powiązane z wewnętrznym działaniem tekstowej reprezentacji drzewa DOM w tym sensie, że odwołują się do identyfikatorów i nazw klas z DOM. Stanowią więc integralne narzędzie dla programistów stron internetowych do modyfikowania i dodawania stylów do elementu na stronie, ale w tym kontekście ma pełną kontrolę nad stroną i jej drzewem DOM.

Z drugiej strony skrypt Puppeteer jest zewnętrznym obserwatorem strony, więc jeśli w tym kontekście używane są selektory CSS, przyjmuje on ukryte założenia dotyczące sposobu implementacji strony, nad którymi skrypt Puppeteer nie ma kontroli.

W efekcie takie skrypty mogą być delikatne i podatne na zmiany kodu źródłowego. Załóżmy na przykład, że jeden ze skryptów Puppeteer służy do automatycznego testowania aplikacji internetowej zawierającej węzeł <button>Submit</button> jako trzecie elementy podrzędne elementu body. Jeden fragment kodu ze przypadku testowego może wyglądać tak:

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

Używamy tutaj selektora 'body:nth-child(3)', by znaleźć przycisk przesyłania, ale jest on ściśle powiązany z tą wersją strony. Jeśli element zostanie później dodany nad przyciskiem, selektor przestanie działać.

Nie jest to wiadomość dla autorów testów: użytkownicy plagiatów już teraz próbują wybierać selektory odporne na takie zmiany. Puppetaria zapewnia użytkownikom nowe narzędzie w tej misji.

Puppeteer udostępnia teraz alternatywny moduł obsługi zapytań oparty na wysyłaniu zapytań do drzewa ułatwień dostępu, a nie na selektorach CSS. Zasadnicza filozofia zakłada, że jeśli konkretny element, który chcemy wybrać, nie uległ zmianie, to odpowiedni węzeł ułatwień dostępu również nie powinien się zmienić.

Te selektory nazywamy „selektorami ARIA” i obsługują zapytania o obliczoną na potrzeby ułatwień dostępu nazwę i rolę drzewa ułatwień dostępu. W porównaniu z selektorami arkusza CSS właściwości te mają charakter semantyczny. Nie są one powiązane z właściwościami składniowymi DOM, ale z opisem sposobu obserwowania strony za pomocą technologii wspomagających osoby z niepełnosprawnością, takich jak czytniki ekranu.

W powyższym przykładzie skryptu testowego możemy zamiast tego użyć selektora aria/Submit[role="button"], aby wybrać odpowiedni przycisk, gdzie Submit odnosi się do dostępnej nazwy elementu:

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

Jeśli później zdecydujesz się zmienić zawartość tekstową przycisku z Submit na Done, test ponownie się nie powinie, ale w tym przypadku jest to pożądane. Zmiana nazwy przycisku spowoduje zmianę treści strony, a nie jej wyglądu czy struktury w DOM. Nasze testy powinny ostrzegać nas o takich zmianach, aby mieć pewność, że są one celowe.

Wracając do większego przykładu z paskiem wyszukiwania, moglibyśmy wykorzystać nowy moduł obsługi aria i zastąpić

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

z

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

aby znaleźć pasek wyszukiwania!

Ogólnie uważamy, że korzystanie z selektorów ARIA może przynieść użytkownikom Puppeteer te korzyści:

  • Zwiększ odporność selektorów w skryptach testowych na zmiany w kodzie źródłowym.
  • Zwiększ czytelność skryptów testowych (dostępne nazwy to deskryptory semantyczne).
  • Stosuj sprawdzone metody przypisywania właściwości ułatwień dostępu do elementów.

W pozostałej części tego artykułu znajdziesz więcej informacji o tym, jak wdrożyliśmy projekt Puppetaria.

Proces projektowania

Wprowadzenie

Jak motywowaliśmy powyżej, chcemy włączyć możliwość wysyłania zapytań do elementów według ich łatwo dostępnej nazwy i roli. Są to właściwości drzewa ułatwień dostępu, działającego podwójnie, czyli drzewka DOM, które jest używane przez urządzenia takie jak czytniki ekranu do wyświetlania stron internetowych.

Patrząc na specyfikację obliczania nazwy na potrzeby ułatwień dostępu, wyraźnie widać, że obliczenie nazwy elementu nie jest proste, dlatego od samego początku zdecydowaliśmy się wykorzystać do tego istniejącą infrastrukturę Chromium.

Nasze podejście do wdrażania

Nawet jeśli ograniczyliśmy się tylko do drzewa ułatwień dostępu Chromium, można wdrożyć zapytania ARIA w Puppeteer na kilka sposobów. Aby dowiedzieć się, dlaczego tak jest, zobaczmy najpierw, jak Puppeteer kontroluje przeglądarkę.

Przeglądarka udostępnia interfejs debugowania za pomocą protokołu o nazwie Chrome DevTools Protocol (CDP). Ujawnia to takie funkcje jak „przeładuj stronę” lub „uruchom ten fragment kodu JavaScript na stronie i prześlij wynik” za pomocą interfejsu niezależnego od języka.

Zarówno interfejs DevTools, jak i Puppeteer, używają CDP do komunikacji z przeglądarką. Do zaimplementowania poleceń CDP umieszcza infrastruktura narzędzi deweloperskich we wszystkich komponentach Chrome: w przeglądarce, w mechanizmie renderowania itd. CDP zajmuje się kierowaniem poleceń we właściwe miejsce.

Działania marionetkowe, takie jak zapytania, klikanie i ocenianie wyrażeń, są wykonywane przy użyciu poleceń CDP, takich jak Runtime.evaluate, które ocenia kod JavaScript bezpośrednio w kontekście strony i zwraca wynik. Inne działania Puppeteer, takie jak emulacja zaburzeń rozpoznawania barw, robienie zrzutów ekranu czy rejestrowanie śladów, używają CDP do bezpośredniej komunikacji z procesem renderowania Blink.

CDP,

Wiąże się to już z 2 ścieżkami implementacji funkcji zapytań. Możemy:

  • Zapisz nasze zapytania w języku JavaScript i wstaw je na stronie za pomocą polecenia Runtime.evaluate lub
  • Użyj punktu końcowego CDP, który ma dostęp do drzewa ułatwień dostępu i wysyła do niego zapytania bezpośrednio w procesie Blink.

Wprowadziliśmy 3 prototypy:

  • Przemierzanie JS DOM – oparte na wstawieniu na stronie JavaScriptu.
  • Przemierzanie Puppeteer AXTree – na podstawie dostępu do drzewa ułatwień dostępu w CDP.
  • Przemierzanie DOM w CDP – przy użyciu nowego punktu końcowego CDP, stworzonego do wysyłania zapytań dotyczących drzewa ułatwień dostępu.

Przemierzanie DOM w JS

Ten prototyp wykonuje pełne przemierzanie DOM oraz wykorzystuje element.computedName i element.computedRole ograniczone za pomocą flagi uruchamiania ComputedAccessibilityInfo, aby pobrać nazwę i rolę każdego elementu podczas przemierzania.

Przemierzanie śladu Puppeteer AXTree

Tutaj pobieramy pełne drzewo ułatwień dostępu za pomocą CDP i przemierzamy je w Puppeteer. Powstałe węzły ułatwień dostępu są mapowane na węzły DOM.

Przemierzanie DOM CDP

Na potrzeby tego prototypu zaimplementowaliśmy nowy punkt końcowy CDP, aby wysyłać zapytania dotyczące drzewa ułatwień dostępu. Dzięki temu zapytania mogą odbywać się w backendzie za pomocą implementacji C++, a nie w kontekście strony przez JavaScript.

Test porównawczy testu jednostkowego

Na ilustracji poniżej porównano łączny czas wykonywania zapytań 1000 razy w przypadku 3 prototypów. Test porównawczy został przeprowadzony w 3 różnych konfiguracjach. W zależności od rozmiaru strony i tego, czy włączono buforowanie elementów ułatwień dostępu, czy nie.

Test porównawczy: całkowite czas wykonywania zapytań 4 elementów 1000 razy

Wyraźnie widać, że między mechanizmem do wysyłania zapytań opartym na CDP a mechanizmem zapytań opartym wyłącznie na platformie Puppeteer występuje znaczna różnica w wydajności, a względna różnica wydaje się znacznie rosnąca wraz z rozmiarem strony. Ciekawe jest to, że prototyp przemierzania DOM w języku JS tak dobrze reaguje na włączanie pamięci podręcznej ułatwień dostępu. Gdy wyłączysz buforowanie, drzewo ułatwień dostępu jest obliczane na żądanie i po każdej interakcji usuwa drzewo ułatwień dostępu, jeśli domena jest wyłączona. Włączenie domeny powoduje, że pamięć podręczna Chromium stanie się drzewem obliczonym.

Na potrzeby przemierzania DOM w języku JS prosimy o podanie dostępnej nazwy i roli każdego elementu podczas przemierzania. Jeśli więc buforowanie jest wyłączone, Chromium oblicza i odrzuca drzewo ułatwień dostępu dla każdego odwiedzanego elementu. Z kolei w przypadku metod opartych na CDP drzewo jest odrzucane tylko między każdym wywołaniem CDP, czyli w przypadku każdego zapytania. Te metody również mają korzyści z włączenia pamięci podręcznej, ponieważ drzewo ułatwień dostępu jest zachowywane w wywołaniach CDP, ale wzrost wydajności jest stosunkowo mniejszy.

Włączenie buforowania w tym przypadku jest pożądane, ale wiąże się z kosztem dodatkowego wykorzystania pamięci. Może to powodować problemy w przypadku skryptów Puppeteer, np.rejestrujących pliki śledzenia. W związku z tym postanowiliśmy nie włączać domyślnie drzewa ułatwień dostępu w pamięci podręcznej. Użytkownicy mogą samodzielnie włączyć buforowanie, włączając domenę ułatwień dostępu CDP.

Test porównawczy pakietu testowego narzędzia DevTools

Poprzednia analiza porównawcza wykazała, że wdrożenie naszego mechanizmu zapytań w warstwie CDP zwiększa wydajność w scenariuszu klinicznych testów jednostkowych.

Aby sprawdzić, czy różnica jest na tyle wyraźna, że jest widoczna w bardziej realistycznym scenariuszu uruchamiania pełnego pakietu testów, dostosowaliśmy kompleksowy zestaw testów w Narzędziach deweloperskich, aby wykorzystywał prototypy oparte na języku JavaScript i CDP i porównywaliśmy środowiska wykonawcze. W ramach tego testu porównawczego zmieniliśmy łącznie 43 selektory z [aria-label=…] na niestandardowy moduł obsługi zapytań aria/…, który następnie zaimplementowaliśmy za pomocą każdego z prototypów.

Niektóre selektory są używane wiele razy w skryptach testowych, więc rzeczywista liczba uruchomień modułu obsługi zapytań aria wyniosła 113 na uruchomienie pakietu. Łączna liczba wybranych zapytań wyniosła 2253, więc w prototypach wykorzystano tylko część tych wyborów.

Test porównawczy: pakiet testów e2e

Jak widać na ilustracji powyżej, występuje wyraźna różnica w całkowitym czasie działania. Dane są zbyt luźne, aby można było wyciągnąć wnioski z praktyk, ale wyraźnie widać tu rozbieżność w wydajności 2 prototypów.

Nowy punkt końcowy CDP

W świetle powyższych testów porównawczych oraz jako że podejście oparte na flagi startowej było ogólnie niepożądane, dlatego postanowiliśmy wdrożyć nowe polecenie CDP do zapytań dotyczących drzewa ułatwień dostępu. Musieliśmy teraz poznać interfejs tego nowego punktu końcowego.

W naszym przypadku użycia w Puppeteer punkt końcowy musi przyjąć tzw. RemoteObjectIds jako argument. Aby później ułatwić nam znalezienie odpowiednich elementów DOM, powinien on zwracać listę obiektów, które zawierają backendNodeIds dla elementów DOM.

Jak widać na wykresie poniżej, wypróbowaliśmy kilka metod, które sprawdziły się w przypadku tego interfejsu. Na tej podstawie okazało się, że rozmiar zwracanych obiektów, tj.czy zwrócono pełne węzły ułatwień dostępu, czy też tylko backendNodeIds nie dał żadnej zauważalnej różnicy. Z drugiej strony stwierdziliśmy, że użycie istniejącego zasobu NextInPreOrderIncludingIgnored nie nadaje się do implementacji logiki przemierzania w tym miejscu, ponieważ spowodowało to zauważalne spowolnienie.

Test porównawczy: porównanie prototypów przemierzania opartych na CDP przez AXTree

Podsumowanie

Po skonfigurowaniu punktu końcowego CDP zaimplementowaliśmy moduł obsługi zapytań po stronie Puppeteer. Efektem naszych działań była zmiana struktury kodu obsługi zapytań, aby umożliwić użytkownikom rozwiązywanie zapytań bezpośrednio przez CDP, a nie wysyłanie zapytań przez JavaScript ocenianych w kontekście strony.

Co dalej?

Nowy moduł obsługi aria udostępniany z Puppeteer w wersji 5.4.0 jako wbudowanym modułem obsługi zapytań. Chętnie zobaczymy, jak użytkownicy wykorzystują ją w scenariuszu testowym. Nie możemy się też doczekać Twoich pomysłów, jak możemy jeszcze bardziej zwiększyć jej przydatność.

Pobieranie kanałów podglądu

Jako domyślnej przeglądarki programistycznej możesz użyć Chrome Canary, Dev lub Beta. Te kanały podglądu dają dostęp do najnowszych funkcji Narzędzi deweloperskich, testują nowoczesne interfejsy API platform internetowych oraz wykrywają problemy w witrynie, zanim zrobią to użytkownicy.

Kontakt z zespołem Narzędzi deweloperskich w Chrome

Użyj tych opcji, aby omówić nowe funkcje i zmiany w poście lub wszelkich innych sprawach związanych z Narzędziami dla programistów.

  • Sugestię lub opinię możesz przesłać na stronie crbug.com.
  • Aby zgłosić problem z Narzędziami deweloperskimi, kliknij Więcej opcji   Więcej   > Pomoc > Zgłoś problemy z Narzędziami deweloperskimi.
  • zatweetować na @ChromeDevTools.
  • Komentarze do filmów o narzędziach dla deweloperów w YouTube lub filmach w YouTube ze wskazówkami dotyczącymi Narzędzi deweloperskich.