Puppeteer i jego podejście do 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. Automatyzowanie tego zadania polega zasadniczo na automatyzacji interakcji ze stroną internetową.
W aplikacji Puppeteer osiąga się to przez wysyłanie zapytań o elementy DOM za pomocą selektorów opartych na ciągach znaków i wykonywanie działań takich jak klikanie lub wpisanie tekstu tych elementów. Na przykład skrypt, który otwiera stronę developer.google.com, znajduje pole wyszukiwania i wyszukuje hasło 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 identyfikacji elementów za pomocą selektorów zapytań stanowi więc kluczowy element interfejsu 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 składowe a semantyczne
Selektory CSS mają charakter składniowy. są ściśle związane z wewnętrznym funkcjonowaniem tekstowych 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 deweloper ma pełną kontrolę nad stroną i jej drzewem DOM.
Z kolei skrypt Puppeteer jest zewnętrznym obserwatorem strony, więc użycie selektorów arkusza CSS w tym kontekście powoduje wprowadzenie ukrytych założeń dotyczących sposobu implementacji strony, na które 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 ktoś używa skryptów Puppeteer do automatycznego testowania aplikacji internetowej zawierającej węzeł <button>Submit</button>
jako trzeci element podrzędny elementu body
. Fragment z danego przypadku testowego może wyglądać tak:
const button = await page.$('body:nth-child(3)'); // problematic selector
await button.click();
Tutaj używamy selektora 'body:nth-child(3)'
, aby znaleźć przycisk przesyłania, ale jest on ściśle powiązany z tą wersją strony. Jeśli jakiś element zostanie później dodany nad przyciskiem, ten selektor przestanie działać.
To nie jest wiadomość dla autorów – użytkownicy Puppeteer już teraz próbują wybierać selektory, które dobrze odnajdą się w takich zmianach. Puppetaria udostępnia użytkownikom nowe narzędzie.
Puppeteer używa teraz alternatywnego modułu obsługi zapytań, który wysyła zapytania do drzewa ułatwień dostępu, zamiast korzystać z selektorów arkusza CSS. Podstawowa zasada jest taka, że jeśli konkretny element, który chcemy wybrać, nie uległ zmianie, odpowiadający mu węzeł ułatwień dostępu też nie powinien się zmienić.
Takie selektory nazywamy „selektorami ARIA”. oraz obsługuje zapytania o obliczoną 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 deskryptorami dotyczącymi odczytywania strony przez technologie wspomagające, np. czytniki ekranu.
W powyższym przykładzie ze skryptem testowym możemy zamiast tego użyć selektora aria/Submit[role="button"]
, aby wybrać żądany przycisk. Submit
oznacza dostępną nazwę elementu:
const button = await page.$('aria/Submit[role="button"]');
await button.click();
Jeśli później zdecydujesz się zmienić tekst przycisku z Submit
na Done
, test znów się nie powiedzie, ale w tym przypadku jest to zalecane. Zmieniając nazwę przycisku, powodujemy zmianę zawartości strony w odróżnieniu od jej wizualnej prezentacji czy jej struktury w DOM. Nasze testy powinny ostrzegać nas o takich zmianach, 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:
- Zwiększyć selektory w skryptach testowych na zmiany kodu źródłowego.
- Zadbaj o czytelność skryptów testowych (dostępne nazwy to deskryptory semantyczne).
- Motywuj sprawdzone metody przypisywania właściwości ułatwień dostępu do elementów.
W pozostałej części tego artykułu znajdziesz szczegółowe informacje o tym, jak wdrożyliśmy projekt Puppetaria.
Proces projektowania
Tło
Jak motywowaliśmy powyżej, chcemy włączyć możliwość wysyłania zapytań według dostępnych nazw i roli. Są to właściwości drzewa ułatwień dostępu – podwójnego drzewa DOM, które jest używane przez urządzenia takie jak czytniki ekranu do wyświetlania stron internetowych.
Specyfikacja obliczania nazwy na potrzeby ułatwień dostępu pokazuje, że obliczenie nazwy elementu nie jest proste, więc od początku zdecydowaliśmy, że chcemy wykorzystać w tym celu istniejącą infrastrukturę Chromium.
Jak podeszliśmy do wdrożenia
Nawet ograniczając się do drzewa ułatwień dostępu w Chromium, można zaimplementować zapytania ARIA w Puppeteer na kilka sposobów. 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). Ujawnia to dostęp do funkcji, takich jak „przeładuj stronę” lub „wykonaj ten fragment kodu JavaScript na stronie i przekaż wynik” za pomocą interfejsu niewymagającego znajomości języka.
Zarówno interfejs narzędzi deweloperskich, jak i Puppeteer używają CDP do komunikacji z przeglądarką. Aby wdrożyć polecenia CDP, we wszystkich komponentach Chrome – w przeglądarce, w mechanizmie renderowania itd., znajduje się infrastruktura Narzędzi deweloperskich. CDP dba o kierowanie poleceń we właściwe miejsce.
Działania twórcy marionetek, takie jak zapytania, klikanie i ocenianie wyrażeń, są wykonywane przy użyciu poleceń CDP (np. Runtime.evaluate
), które oceniają JavaScript bezpośrednio w kontekście strony i przekazują 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.
Mamy już 2 ścieżki wdrożenia funkcji zapytań: możemy:
- 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:
- Poruszanie się po interfejsie JS DOM – opiera się na wstawieniu na stronie JavaScriptu
- Omijanie Puppeteer AXTree – opiera się na wykorzystaniu istniejącego dostępu do CDP do drzewa ułatwień dostępu.
- Omijanie DOM CDP – z wykorzystaniem nowego punktu końcowego CDP przeznaczonego do wysyłania zapytań do drzewa ułatwień dostępu.
Przemierzanie DOM JS
Ten prototyp wykonuje pełne przemierzenie DOM i używa element.computedName
i element.computedRole
, które są zablokowane przy użyciu flagi uruchomienia ComputedAccessibilityInfo
, aby pobrać nazwę i rolę każdego elementu podczas przemierzania.
Przemierzanie AXTree przez Puppeteer
Tutaj zamiast tego pobieramy pełne drzewo ułatwień dostępu za pomocą CDP i przemierzamy je w Puppeteer. Otrzymane w ten sposób węzły ułatwień dostępu są następnie zmapowane na węzły DOM.
Przemierzanie CDP DOM
W tym prototypie wdrożyliśmy nowy punkt końcowy CDP do wysyłania zapytań dotyczących drzewa ułatwień dostępu. Dzięki temu zapytania mogą odbyć się w ramach implementacji w języku C++, a nie w kontekście strony poprzez JavaScript.
Test porównawczy testu jednostkowego
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óżnymi rozmiarami strony i o tym, czy włączono buforowanie elementów ułatwień dostępu.
Wyraźnie widać, że występuje duża rozbieżność w wydajności mechanizmu wysyłania zapytań wspieranego przez CDP a 2 innymi mechanizmami zaimplementowanymi wyłącznie w Puppeteer. Różnica ta wydaje się jednak gwałtownie rosnąć wraz z rozmiarem strony. Co ciekawe, prototyp przemierzania JS DOM działa tak dobrze na włączenie buforowania ułatwień dostępu. Gdy buforowanie jest wyłączone, drzewo ułatwień dostępu jest obliczane na żądanie i jeśli domena jest wyłączona, drzewo ułatwień dostępu jest odrzucane po każdej interakcji. Włączenie domeny powoduje, że Chromium buforuje obliczone drzewo.
W przypadku przemierzania DOM w języku JS prosimy o udostępnienie nazwy i roli każdego elementu podczas przemierzania. Jeśli buforowanie jest wyłączone, Chromium oblicza i odrzuca drzewo ułatwień dostępu w przypadku 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.
Mimo że włączenie buforowania w pamięci podręcznej wydaje się tutaj korzystne, niesie to za sobą koszt dodatkowego wykorzystania pamięci. Może to być problematyczne w przypadku skryptów Puppeteer, które np.rejestrują pliki śledzenia. W związku z tym postanowiliśmy domyślnie nie włączać buforowania drzewa ułatwień dostępu. Użytkownicy mogą samodzielnie włączyć buforowanie, włączając domenę ułatwień dostępu CDP.
Test porównawczy pakietu testowego Narzędzi deweloperskich
Poprzednie testy porównawcze wykazały, że wdrożenie mechanizmu zapytań w warstwie CDP poprawia wydajność w klinicznym scenariuszu testów jednostkowych.
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 poprawkę kompleksowego zestawu testów w Narzędziach deweloperskich, aby wykorzystać prototypy oparte na JavaScript i CDP i porównać środowiska wykonawcze. W testach porównawczych zmieniliśmy w sumie 43 selektory z [aria-label=…]
na niestandardowy moduł obsługi zapytań aria/…
, który potem wdrożyliśmy przy użyciu każdego z prototypów.
Niektóre selektory są używane wiele razy w skryptach testowych, więc rzeczywista liczba wykonań modułu obsługi zapytania aria
wynosiła 113 na uruchomienie pakietu. Łączna liczba wybranych zapytań wyniosła 2253, więc tylko część z nich została wybrana w prototypach.
Jak widać na ilustracji powyżej, całkowity czas działania jest zauważalna. Dane są zbyt zaszumione, aby wywnioskować coś konkretnego, ale jasne jest, że w tym scenariuszu także występuje różnica w wydajności między dwoma prototypami.
Nowy punkt końcowy CDP
W świetle powyższych porównań, a podejście oparte na flagach było ogólnie niepożądane, zdecydowaliśmy się przejść do wdrożenia nowego polecenia CDP do wysyłania zapytań dotyczących drzewa ułatwień dostępu. Musieliśmy teraz opracować interfejs 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 podejść do tego interfejsu. Stwierdziliśmy, że rozmiar zwróconych obiektów, tj.tego, czy zwróciliśmy pełne węzły ułatwień dostępu, czy tylko obiekt backendNodeIds
, nie ma znaczącej różnicy. Z drugiej strony okazało się, że wykorzystanie istniejącego NextInPreOrderIncludingIgnored
nie było dobrym rozwiązaniem do wdrożenia logiki przemierzania, ponieważ skutkowało to zauważalnym spowolnieniem działania.
Podsumowanie
Teraz, mając punkt końcowy CDP, 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
udostępniany 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.
Pobierz kanały podglądu
Zastanów się, czy nie ustawić Chrome w wersji Canary, Dev lub beta jako domyślnej przeglądarki do programowania. Te kanały wersji testowej dają dostęp do najnowszych funkcji Narzędzi deweloperskich, umożliwiają testowanie najnowocześniejszych interfejsów API platformy internetowej i wykrywanie problemów w witrynie, zanim użytkownicy ją zobaczą.
Kontakt z zespołem ds. Narzędzi deweloperskich w Chrome
Użyj poniższych opcji, aby porozmawiać o nowych funkcjach i zmianach w poście lub o innych kwestiach związanych z Narzędziami deweloperskimi.
- Prześlij nam sugestię lub opinię na crbug.com.
- Aby zgłosić problem z Narzędziami deweloperskimi, kliknij Więcej opcji
> Pomoc > Zgłoś problemy z Narzędziami deweloperskimi w Narzędziach deweloperskich.
- Opublikuj tweeta na stronie @ChromeDevTools.
- Napisz komentarz pod filmem dotyczącym nowości w Narzędziach deweloperskich w Narzędziach deweloperskich w YouTube lub filmach w YouTube ze wskazówkami dotyczącymi Narzędzi deweloperskich.