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

Johan Bay
Johan Bay

Puppeteer i sposób jego działania w przypadku selektorów

Puppeteer to biblioteka automatyzacji przeglądarki dla Node. Umożliwia ona sterowanie przeglądarką za pomocą prostego i nowoczesnego interfejsu JavaScript API.

Najważniejszym zadaniem przeglądarki jest oczywiście przeglądanie stron internetowych. Automatyzacja tego zadania sprowadza się do automatyzacji interakcji ze stroną internetową.

W Puppeteer uzyskuje się to, wysyłając zapytanie o elementy DOM za pomocą selektorów opartych na ciągach znaków i wykonując działania takie jak klikanie lub wpisywanie tekstu na elementach. Na przykład skrypt, który otwiera stronę developer.google.com, znajduje pole wyszukiwania i wyszukuje puppetaria, może 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 działania Puppeteer. Do tej pory selektory w Puppeteer ograniczały się do selektorów CSS i XPath, które, choć są bardzo skuteczne pod względem ekspresji, mogą mieć wady utrwalania interakcji przeglądarki w skryptach.

Selektory syntaktyczne a selektory semantyczne

Selektory CSS mają charakter syntaktyczny. Są ściśle powiązane z wewnętrzną reprezentacją tekstową drzewa DOM w tym sensie, że odwołują się do identyfikatorów i nazwy klasy z DOM. Stanowią one integralne narzędzie dla programistów stron internetowych do modyfikowania lub dodawania stylów do elementu na stronie, ale w tym kontekście deweloper ma pełną kontrolę nad stroną i jej drzewem DOM.

Z drugiej strony skrypt Puppeteer jest zewnętrznym obserwatorem strony, więc gdy w tym kontekście używa się selektorów CSS, wprowadza on ukryte założenia dotyczące sposobu implementacji strony, nad którymi skrypt Puppeteer nie ma kontroli.

Efektem jest to, że skrypty takie są delikatne i podatne na zmiany w kodzie źródłowym. Załóżmy na przykład, że do automatycznego testowania aplikacji internetowej używasz skryptów Puppeteer, a węzeł <button>Submit</button> jest trzecim elementem potomnym elementu body. Oto przykładowy fragment kodu z przypadku testowego:

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

W tym przykładzie używamy selektora 'body:nth-child(3)', aby znaleźć przycisk przesyłania, ale jest on ściśle powiązany z tą wersją strony internetowej. Jeśli później nad przyciskiem dodasz element, selektor przestanie działać.

To nie jest nowość dla autorów testów: użytkownicy Puppeteer już teraz próbują wybierać selektory, które są odporne na takie zmiany. Puppetaria udostępnia użytkownikom nowe narzędzie.

Puppeteer zawiera teraz alternatywny moduł obsługi zapytań, który wysyła zapytania do drzewa ułatwień dostępu zamiast korzystać z selektorów CSS. Zasada jest taka, że jeśli konkretny element, który chcemy wybrać, się nie zmienił, to odpowiedni węzeł dostępności też nie powinien się zmienić.

Nazywamy takie selektory „ARIA” i obsługujemy zapytania o obliczony widoczny element oraz rolę w drzewie ułatwień dostępu. W porównaniu z selektorami arkusza CSS te właściwości mają charakter semantyczny. Nie są one powiązane z właściwościami syntaktycznymi DOM, ale są opisami sposobu, w jaki strona jest obserwowana przez technologie wspomagające, takie jak czytniki ekranu.

W przykładowym skrypcie testowym powyżej do wybrania odpowiedniego przycisku możemy użyć selektora aria/Submit[role="button"], gdzie Submit odnosi się do nazwy elementu:

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

Jeśli później zdecydujesz się zmienić treść tekstową przycisku z Submit na Done, test ponownie zakończy się niepowodzeniem, ale w tym przypadku jest to pożądane. Zmiana nazwy przycisku powoduje zmianę treści strony, a nie jej wyglądu czy struktury w DOM. Testy powinny nas ostrzec przed takimi zmianami, aby mieć pewność, że są one zamierzone.

Wracając do większego przykładu z paskiem wyszukiwania, możemy 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.

Uważamy, że korzystanie z takich selektorów ARIA może przynieść użytkownikom Puppeteer następujące korzyści:

  • Ułatwienie selekcji w skryptach testowych dzięki zwiększeniu odporności na zmiany w kodzie źródłowym.
  • Ułatwić czytelność skryptów testowych (nazwy na potrzeby ułatwień dostępu to deskryptory semantyczne).
  • Zachęcaj do stosowania sprawdzonych metod przypisywania elementom właściwości ułatwień dostępu.

Z dalszej części tego artykułu dowiesz się, jak wdrożone zostały rozwiązania z projektu Puppetaria.

Proces projektowania

Tło

Jak już wspomnieliśmy, chcemy umożliwić wysyłanie zapytań o elementy według ich nazwy i odpowiedniej roli. Są to właściwości drzewa ułatwień dostępu, które jest odpowiednikiem zwykłego drzewa DOM i jest używane przez urządzenia takie jak czytniki ekranu do wyświetlania stron internetowych.

Ze specyfikacji obliczania nazwy dostępnej wynika, że obliczenie nazwy elementu nie jest trywialnym zadaniem, dlatego od początku chcieliśmy użyć do tego celu istniejącej infrastruktury Chromium.

Jak podeszliśmy do wdrożenia

Nawet ograniczając się do korzystania z drzewa ułatwień dostępu w Chromium, istnieje wiele sposobów, by wdrożyć zapytania ARIA w aplikacji Puppeteer. Aby przekonać się dlaczego, najpierw zobaczmy, jak Puppeteer steruje przeglądarką.

Przeglądarka udostępnia interfejs debugowania za pomocą protokołu Chrome DevTools Protocol (CDP). Dzięki temu można udostępniać funkcje takie jak „odśwież stronę” lub „wykonaj ten fragment kodu JavaScript na stronie i zwracaj wynik” za pomocą interfejsu niezależnego od języka.

Zarówno interfejs narzędzi deweloperskich, jak i Puppeteer używają CDP do komunikacji z przeglądarką. Aby zaimplementować polecenia CDP, we wszystkich komponentach Chrome (w przeglądarce, w renderowaniu itp.) jest infrastruktura Narzędzi deweloperskich. CDP zajmuje się kierowaniem poleceń do właściwego miejsca.

Działania Puppeteer, takie jak wysyłanie zapytań, klikanie i interpretowanie wyrażeń, są wykonywane za pomocą poleceń CDP, np. Runtime.evaluate, które interpretuje JavaScript bezpośrednio w kontekście strony i zwraca wynik. Inne działania w aplikacji Puppeter, takie jak emulowanie zaburzeń rozpoznawania barw, robienie zrzutów ekranu czy rejestrowanie śladów, wykorzystują platformę CDP do bezpośredniej komunikacji z procesem renderowania Blink.

CDP

Mamy już 2 ścieżki wdrożenia funkcji zapytań:

  • Napisz naszą logikę zapytań w języku JavaScript i umieść ją na stronie za pomocą atrybutu Runtime.evaluate lub
  • Użyj punktu końcowego CDP, który może uzyskiwać dostęp do drzewa ułatwień dostępu i wysyłać do niego zapytania bezpośrednio w procesie Blink.

Wprowadziliśmy 3 prototypy:

  • Przeglądanie DOM w JavaScriptzie – polega na wstrzykiwaniu JavaScriptu na stronie.
  • Przeglądanie za pomocą Puppeteer AXTree – na podstawie użycia istniejącego dostępu CDP do drzewa ułatwień dostępu.
  • Przeszukiwanie DOM w CDP – korzystanie z nowego punktu końcowego CDP, który został stworzony specjalnie do wykonywania zapytań do drzewa dostępności.

Przeszukiwanie DOM w JS

Ten prototyp wykonuje pełne przejście przez DOM i korzysta z elementów element.computedNameelement.computedRole, które są ograniczone flagą ComputedAccessibilityInfo, aby pobierać nazwę i rolę każdego elementu podczas przechodzenia.

Przeszukiwanie AXTree za pomocą Puppeteer

W tym przypadku zamiast tego pobieramy pełne drzewo ułatwień dostępu za pomocą CDP i przechodzimy przez nie w Puppeteer. Uzyskane w ten sposób węzły ułatwień dostępu są następnie mapowane na węzły modelu DOM.

Przemierzanie CDP DOM

W tym prototypie wdrożyliśmy nowy punkt końcowy CDP specjalnie do zapytań dotyczących drzewa dostępności. Dzięki temu zapytania mogą być wysyłane na zapleczu za pomocą implementacji w C++, a nie w kontekście strony za pomocą JavaScriptu.

Test jednostkowy – test porównawczy

Poniższy rysunek porównuje łączny czas wykonywania zapytań 1000 razy w przypadku 3 prototypów. Test porównawczy został przeprowadzony w 3 różnych konfiguracjach z różnym rozmiarem strony i z włączonym lub wyłączonym buforowaniem elementów ułatwień dostępu.

Wskaźnik: łączny czas wykonywania zapytań do 4 elementów 1000 razy

Wyraźnie widać, że mechanizm wysyłania zapytań oparty na CDP i dwa inne mechanizmy zaimplementowane wyłącznie w Puppeteer różnią się znacznie pod względem wydajności. Różnica ta wydaje się zwiększać wraz z wzrostem rozmiaru strony. Ciekawe jest to, że prototyp JS DOM traversal reaguje tak dobrze na włączanie pamięci podręcznej ułatwień dostępu. Gdy buforowanie jest wyłączone, drzewo ułatwień dostępu jest obliczane na żądanie i odrzucane po każdej interakcji, jeśli domena jest wyłączona. Włączenie domeny powoduje, że Chromium zamiast tego przechowuje w pamięci podręcznej wyliczoną strukturę.

W przypadku przeglądania DOM w JS prosimy o dostępną nazwę i rolę każdego elementu podczas przeglądania, więc jeśli buforowanie jest wyłączone, Chromium oblicza i odrzuca drzewo ułatwień dostępu dla każdego odwiedzanego elementu. W przypadku metod opartych na CDP drzewo jest odrzucane tylko między każdym wywołaniem CDP, tj. dla każdego zapytania. Te rozwiązania również uzyskują korzyści z włączenia buforowania, ponieważ drzewo ułatwień dostępu jest następnie zachowywane w wywołaniach CDP, ale wzrost wydajności jest więc stosunkowo mniejszy.

Chociaż w tym przypadku włączenie buforowania wydaje się być dobrym rozwiązaniem, wiąże się to z dodatkowym wykorzystaniem pamięci. W przypadku skryptów Puppeteer, które na przykład rejestrują pliki śledzenia, może to być problematyczne. Dlatego postanowiliśmy domyślnie nie włączać buforowania drzewa ułatwień dostępu. Użytkownicy mogą włączyć buforowanie samodzielnie, aktywując domenę Dostępność w CDP.

Test porównawczy pakietu testowego Narzędzi deweloperskich

Poprzednie testy porównawcze wykazały, że implementacja naszego mechanizmu zapytań na poziomie CDP zwiększa wydajność w sytuacji testu jednostkowego w klinice.

Aby sprawdzić, czy różnica jest na tyle wyraźna, że staje się zauważalna w bardziej realistycznym scenariuszu uruchomienia pełnego zestawu testów, wprowadziliśmy w Narzędziach deweloperskich kompleksowy pakiet testów, aby wykorzystać prototypy oparte na JavaScript i CDP i porównać ś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 wdrożyliśmy za pomocą każdego z prototypów.

Niektóre selektory są używane wielokrotnie w skryptach testowych, więc rzeczywista liczba uruchomień modułu obsługi zapytania aria wynosiła 113 na każde uruchomienie zestawu. Łączna liczba wybranych zapytań wyniosła 2253, więc tylko część z nich została wybrana w prototypach.

Test porównawczy: zestaw testów E2E

Jak widać na ilustracji powyżej, całkowity czas działania jest zauważalna. Dane są zbyt niejednoznaczne, aby można było wyciągnąć konkretne wnioski, ale w tym scenariuszu też widać różnicę w skuteczności między dwoma prototypami.

Nowy punkt końcowy CDP

Z uwagi na powyższe wyniki i fakt, że podejście oparte na flagach uruchomienia było ogólnie niepożądane, zdecydowaliśmy się wdrożyć nowe polecenie CDP do zapytania drzewa ułatwień dostępu. Musieliśmy się dowiedzieć, jak wygląda interfejs tego nowego punktu końcowego.

W przypadku aplikacji Puppeteer punkt końcowy musi przyjąć tak zwane RemoteObjectIds jako argument. Aby umożliwić nam późniejsze znalezienie odpowiednich elementów DOM, powinien on zwracać listę obiektów zawierających backendNodeIds dla tych elementów.

Jak widać na wykresie poniżej, wypróbowaliśmy kilka rozwiązań, które spełniają wymagania dotyczące tego interfejsu. Na tej podstawie stwierdziliśmy, że rozmiar zwracanych obiektów, czyli to, czy zwróciliśmy pełne węzły dostępności, czy tylko węzły backendNodeIds, nie miało żadnego zauważalnego wpływu. Z drugiej strony stwierdziliśmy, że użycie dotychczasowej funkcji NextInPreOrderIncludingIgnored nie było dobrym pomysłem na implementację logiki przeszukiwania, ponieważ spowodowało to zauważalne spowolnienie.

Analiza porównawcza: porównanie prototypów AXTree przemierzających opartych na CDP

Podsumowanie

Teraz, gdy punkt końcowy CDP jest już dostępny, wdrożyliśmy moduł obsługi zapytań po stronie Puppeteer. Efektem pracy było przebudowanie kodu obsługi zapytań w taki sposób, aby zapytania rozwiązywały bezpośrednio przez platformę CDP, zamiast wykonywać zapytania za pomocą JavaScriptu ocenianego w kontekście strony.

Co dalej?

Nowy moduł obsługi aria jest dostarczany z Puppeteer w wersji 5.4.0 jako wbudowany moduł obsługi zapytań. Jesteśmy bardzo ciekawi, jak użytkownicy zastosowali je w swoich skryptach testowych. Jesteśmy bardzo ciekawi Twoich pomysłów na zwiększenie przydatności tej funkcji.

Pobieranie kanałów podglądu

Rozważ użycie przeglądarki Chrome Canary, Dev lub Beta jako domyślnej przeglądarki deweloperskiej. Te kanały wersji testowej dają dostęp do najnowszych funkcji Narzędzi deweloperskich, umożliwiają testowanie najnowocześniejszych interfejsów API platform internetowych i pomagają w wykrywaniu problemów w witrynie przed użytkownikami.

Kontakt z zespołem Narzędzi deweloperskich w Chrome

Aby omówić nowe funkcje, aktualizacje lub inne kwestie związane z Narzędziami deweloperskimi, skorzystaj z tych opcji.