Puppeteer i sposób jego działania w przypadku selektorów
Puppeteer to biblioteka automatyzacji przeglądarki dla Node.js, która umożliwia 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 polega w istocie na 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 były ograniczone do selektorów CSS i XPath, które, choć bardzo wydajne, mogą mieć wady w przypadku trwałych interakcji z przeglądarką 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.
W efekcie takie skrypty mogą być niestabilne i wrażliwe 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 zostanie dodany element, selektor przestanie działać.
Nie jest to nowość dla autorów testów: użytkownicy Puppeteer już teraz próbują wybierać selektory, które są odporne na takie zmiany. Puppetaria to nowe narzędzie, które pomoże użytkownikom w realizacji tego zadania.
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 odpowiadający mu węzeł dostępności też nie powinien się zmienić.
Nazywamy takie selektory „selektorami 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 zdecydujemy się zmienić tekst przycisku z Submit
na Done
, test znów się nie powiedzie, ale w tym przypadku jest to pożądane. Zmiana nazwy przycisku powoduje zmianę zawartości strony, a nie jej wizualnej prezentacji ani sposobu jej ustrukturyzowania 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 użyć nowego modułu 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 takich selektorów ARIA może przynieść użytkownikom Puppeteer następujące korzyści:
- Ułatwienie selekcji w skryptach testowych w sytuacji, gdy zmienia się kod źródłowy.
- Ułatwić odczytywanie 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 dostępności w Chromium, można zaimplementować zapytania ARIA w Puppeteer na kilka sposobów. Aby to zrozumieć, najpierw sprawdź, jak Puppeteer kontroluje 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 DevTools, jak i Puppeteer korzystają z interfejsu 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 Puppeteer, takie jak emulowanie niedoboru widzenia kolorów, robienie zrzutów ekranu czy rejestrowanie śladów, korzystają z CDP do bezpośredniej komunikacji z procesem renderowania Blink.
Pozostają nam 2 ścieżki implementacji funkcji zapytań:
- Napisać logikę zapytań w JavaScriptzie i wstrzyknąć ją do strony za pomocą funkcji
Runtime.evaluate
lub - Użyj punktu końcowego CDP, który może uzyskać dostęp do drzewa ułatwień dostępu i wysyłać do niego zapytania bezpośrednio w ramach procesu Blink.
Wdrożyliśmy 3 prototypy:
- Przeszukiwanie DOM w JS – polega na wstrzyknięciu JavaScriptu do strony.
- Przeglądanie za pomocą Puppeteer AXTree – na podstawie używania istniejącego dostępu CDP do drzewa ułatwień dostępu.
- Przeszukiwanie DOM w CDP – korzystanie z nowego punktu końcowego CDP stworzonego specjalnie do zapytań dotyczących drzewa dostępności.
Przeszukiwanie DOM w JS
Ten prototyp wykonuje pełne przejście po DOM i wykorzystuje funkcje element.computedName
oraz element.computedRole
, które są dostępne po spełnieniu warunku ComputedAccessibilityInfo
flagi uruchomienia. Dzięki temu podczas przejścia po DOM można pobrać nazwę i rolę każdego elementu.
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.
Przeszukiwanie CDP w DOM-ie
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ć wykonywane po stronie serwera za pomocą implementacji w C++, a nie w kontekście strony za pomocą JavaScriptu.
Test jednostkowy – test porównawczy
Na poniższym rysunku porównano łączny czas wykonywania zapytań do 4 elementów 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.
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, ż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 usuwane 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 nazwę i rolę ułatwień dostępu dla 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 podejścia opartego na CDP drzewo jest odrzucane tylko między każdym wywołaniem CDP, czyli po każdym zapytaniu. Te podejścia również korzystają z włączenia buforowania, ponieważ drzewo dostępności jest wtedy przechowywane w ramach wywołań CDP, ale wzrost wydajności jest w tym przypadku stosunkowo mniejszy.
Chociaż włączenie pamięci podręcznej wydaje się tutaj pożądane, wiąże się z dodatkowym wykorzystaniem pamięci. W przypadku skryptów Puppeteer, które na przykład nagrywają pliki śledzenia, może to być problematyczne. Dlatego postanowiliśmy domyślnie nie włączać buforowania drzewa ułatwień dostępu. Użytkownicy mogą sami włączyć buforowanie, aktywując domena Dostępność w CDP.
Zestaw testów DevTools
Poprzedni test porównawczy wykazał, że implementacja naszego mechanizmu zapytań na poziomie CDP zwiększa wydajność w sytuacji testu jednostkowego w klinice.
Aby sprawdzić, czy różnica jest wystarczająco wyraźna, aby była zauważalna w bardziej realistycznym scenariuszu uruchamiania pełnego zestawu testów, wprowadziliśmy poprawki w całościowym zestawie testów DevTools, aby wykorzystać prototypy oparte na JavaScript i CDP, i porównać czasy wykonywania. 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ń wynosiła 2253, więc tylko niewielka część wyborów zapytań została dokonana za pomocą prototypów.
Jak widać na rysunku powyżej, jest wyraźna różnica w łącznym czasie działania. 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 Puppeteer potrzebujemy punktu końcowego, który przyjmuje jako argument tzw. RemoteObjectIds
, a aby umożliwić nam późniejsze znajdowanie odpowiednich elementów DOM, powinien zwracać listę obiektów zawierającą backendNodeIds
dla elementów DOM.
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 znaczenia. 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.
Podsumowanie
Teraz, gdy punkt końcowy CDP jest już dostępny, wdrożyliśmy moduł obsługi zapytań po stronie Puppeteer. Głównym zadaniem było przekształcenie kodu obsługującego zapytania, aby umożliwić ich rozwiązywanie bezpośrednio przez CDP zamiast wysyłania zapytań za pomocą kodu JavaScript 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ń. Czekamy na to, jak użytkownicy wykorzystają tę funkcję w swoich skryptach testowych, i z niecierpliwością czekamy na Wasze pomysły na to, jak możemy ją jeszcze bardziej udoskonalić.
Pobieranie kanałów podglądu
Rozważ użycie jako domyślnej przeglądarki deweloperskiej wersji Canary, Dev lub Beta przeglądarki Chrome. Te kanały wersji wstępnej zapewniają dostęp do najnowszych funkcji DevTools, umożliwiają testowanie najnowocześniejszych interfejsów API platformy internetowej i pomagają znaleźć problemy w witrynie, zanim zrobią to użytkownicy.
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.
- Przesyłaj opinie i prośby o dodanie funkcji na stronie crbug.com.
- Zgłoś problem z Narzędziami deweloperskimi, klikając Więcej opcji > Pomoc > Zgłoś problem z Narzędziami deweloperskimi w Narzędziach deweloperskich.
- Wyślij tweeta do @ChromeDevTools.
- Dodaj komentarze do filmów w YouTube z serii „Co nowego w Narzędziach deweloperskich” lub Wskazówki dotyczące Narzędzi deweloperskich.