Puppeteer und sein Ansatz für Selektoren
Puppeteer ist eine Browser-Automatisierungsbibliothek für Node. Sie ermöglicht es Ihnen, einen Browser mithilfe einer einfachen und modernen JavaScript API zu steuern.
Die wichtigste Browseraufgabe ist natürlich das Surfen auf Webseiten. Die Automatisierung dieser Aufgabe ähnelt im Wesentlichen der Automatisierung von Interaktionen mit der Webseite.
In Puppeteer wird dies durch Abfragen von DOM-Elementen mithilfe von Zeichenfolgen-basierten Selektoren und Ausführen von Aktionen wie Klicken oder Eingeben von Text in den Elementen erreicht. Ein Skript, das developer.google.com öffnet, das Suchfeld findet und nach puppetaria
sucht, könnte beispielsweise wie folgt aussehen:
(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');
})();
Die Identifizierung von Elementen mithilfe von Abfrageselektoren ist daher ein wesentlicher Bestandteil der Puppeteer-Erfahrung. Bisher waren Selektoren in Puppeteer auf CSS- und XPath-Selektoren beschränkt. Diese sind zwar ausdrucksstark, können aber Nachteile für dauerhafte Browserinteraktionen in Skripts haben.
Syntax- und semantische Auswahl
CSS-Selektoren sind syntaktischer Natur. Sie sind eng an das Innere der Textdarstellung des DOM-Baums gebunden, da sie auf IDs und Klassennamen aus dem DOM verweisen. Daher stellen sie ein integrales Tool für Webentwickler dar, mit dem sie Stile für ein Element auf einer Seite ändern oder hinzufügen können. In diesem Kontext hat der Entwickler jedoch die volle Kontrolle über die Seite und ihre DOM-Struktur.
Ein Puppeteer-Skript hingegen ist ein externer Beobachter einer Seite. Wenn CSS-Selektoren in diesem Kontext verwendet werden, führt es also versteckte Annahmen über die Implementierung der Seite ein, über die das Puppeteer-Skript keine Kontrolle hat.
Solche Skripts können anfällig für Änderungen am Quellcode sein. Angenommen, jemand verwendet Puppeteer-Skripts für automatisierte Tests einer Webanwendung, die den Knoten <button>Submit</button>
als drittes untergeordnetes Element des body
-Elements enthält. Ein Snippet aus einem Testfall könnte so aussehen:
const button = await page.$('body:nth-child(3)'); // problematic selector
await button.click();
Hier verwenden wir den Selektor 'body:nth-child(3)'
, um die Schaltfläche „Senden“ zu finden. Dieser ist jedoch eng mit dieser Version der Webseite verbunden. Wenn später ein Element über der Schaltfläche hinzugefügt wird, funktioniert dieser Selektor nicht mehr.
Das ist keine Neuigkeit für Testautoren: Puppeteer-Nutzer versuchen bereits, Selektoren auszuwählen, die solche Änderungen problemlos bewältigen können. Mit Puppetaria erhalten Nutzer ein neues Tool für diese Aufgabenreihe.
Puppeteer wird jetzt mit einem alternativen Abfrage-Handler ausgeliefert, der auf der Abfrage des Barrierefreiheitsbaums statt auf CSS-Selektoren basiert. Wenn sich das konkrete Element, das wir auswählen möchten, nicht geändert hat, sollte sich auch der entsprechende Barrierefreiheitsknoten nicht geändert haben.
Diese Selektoren nennen wir ARIA-Selektoren und unterstützen die Abfrage des berechneten barrierefreien Namens und der Rolle des Baums für Barrierefreiheit. Im Vergleich zu den CSS-Selektoren sind diese Eigenschaften semantisch. Sie sind nicht an die syntaktischen Eigenschaften des DOMs gebunden, sondern Deskriptoren dafür, wie die Seite durch Hilfstechnologien wie Screenreader beobachtet wird.
Im Testskript oben könnten wir stattdessen den Selektor aria/Submit[role="button"]
zur Auswahl der gewünschten Schaltfläche verwenden, wobei sich Submit
auf den zugänglichen Namen des Elements bezieht:
const button = await page.$('aria/Submit[role="button"]');
await button.click();
Wenn wir uns später entscheiden, den Textinhalt unserer Schaltfläche von Submit
in Done
zu ändern, schlägt der Test wieder fehl, aber in diesem Fall ist das wünschenswert. Wenn wir den Namen der Schaltfläche ändern, ändern wir auch den Inhalt der Seite – im Gegensatz zu ihrer visuellen Darstellung oder ihrer Struktur im DOM. Unsere Tests sollten uns vor solchen Änderungen warnen, damit wir sicher sein können, dass diese Änderungen beabsichtigt sind.
Um zum größeren Beispiel mit der Suchleiste zurückzukehren, könnten wir den neuen aria
-Handler verwenden und
const search = await page.$('devsite-search > form > div.devsite-search-container');
mit
const search = await page.$('aria/Open search[role="button"]');
um die Suchleiste zu finden.
Im Allgemeinen gehen wir davon aus, dass die Verwendung solcher ARIA-Selektoren Puppeteer-Nutzern die folgenden Vorteile bieten kann:
- Selektoren in Testskripts widerstandsfähiger gegen Quellcodeänderungen
- Die Lesbarkeit von Testskripts wird verbessert (barrierefreie Namen sind semantische Deskriptoren).
- Best Practices für das Zuweisen von Barrierefreiheitseigenschaften zu Elementen zu fördern
Im weiteren Verlauf dieses Artikels wird die Implementierung des Puppetaria-Projekts ausführlich beschrieben.
Der Designprozess
Hintergrund
Wie oben erwähnt, möchten wir Abfrageelemente nach ihrem barrierefreien Namen und ihrer Rolle aktivieren. Dies sind die Eigenschaften des Baum für Barrierefreiheit, der identisch mit dem üblichen DOM-Baum ist und von Geräten wie Screenreadern verwendet wird, um Webseiten anzuzeigen.
Aus der Spezifikation zur Berechnung des barrierefreien Namens geht hervor, dass die Berechnung des Namens eines Elements keine leichte Aufgabe ist. Daher haben wir von Anfang an beschlossen, die vorhandene Infrastruktur von Chromium für diesen Zweck weiterzuverwenden.
Vorgehensweise bei der Implementierung
Selbst wenn wir uns darauf beschränken, den Barrierefreiheitsbaum von Chromium zu verwenden, gibt es zahlreiche Möglichkeiten, ARIA-Abfragen in Puppeteer zu implementieren. Um zu verstehen, warum, sehen wir uns zuerst an, wie Puppeteer den Browser steuert.
Eine Debugging-Oberfläche wird im Browser über das Protokoll Chrome DevTools Protocol (CDP) bereitgestellt. Dadurch werden Funktionen wie „Seite neu laden“ oder „diesen JavaScript-Code auf der Seite ausführen und das Ergebnis zurückgeben“ über eine sprachunabhängige Benutzeroberfläche.
Sowohl das DevTools-Frontend als auch Puppeteer kommunizieren über CDP mit dem Browser. Zur Implementierung von CDP-Befehlen ist die Entwicklertools-Infrastruktur in allen Komponenten von Chrome vorhanden: im Browser, im Renderer usw. CDP leitet die Befehle an die richtige Stelle weiter.
Puppeteer-Aktionen wie das Abfragen, Klicken und Auswerten von Ausdrücken werden mithilfe von CDP-Befehlen wie Runtime.evaluate
ausgeführt, die JavaScript direkt im Seitenkontext auswertet und das Ergebnis übergibt. Andere Puppeteer-Aktionen wie die Emulierung von Farbblindheit, das Erstellen von Screenshots oder das Aufzeichnen von Traces kommunizieren direkt mit dem Blink-Renderingprozess.
Damit haben wir bereits zwei Möglichkeiten, unsere Abfragefunktion zu implementieren: können wir:
- Schreiben Sie unsere Abfragelogik in JavaScript und fügen Sie diese mit
Runtime.evaluate
in die Seite ein. - Verwenden Sie einen CDP-Endpunkt, der direkt im Blink-Prozess auf den Baum für Barrierefreiheit zugreifen und ihn abfragen kann.
Wir haben drei Prototypen implementiert:
- JS DOM Traversal: basierend auf dem Einfügen von JavaScript in die Seite
- Puppeteer AXTree-Durchlauf – basierend auf der Nutzung des vorhandenen CDP-Zugriffs auf den Baum für Barrierefreiheit
- CDP DOM Traversal: mit einem neuen CDP-Endpunkt, der speziell für die Abfrage der Barrierefreiheitsstruktur entwickelt wurde
JS-DOM-Durchlauf
Dieser Prototyp führt einen vollständigen Durchlauf des DOMs durch und verwendet element.computedName
und element.computedRole
, die mit dem ComputedAccessibilityInfo
Launch-Flag verknüpft sind, um den Namen und die Rolle für jedes Element während des Durchlaufs abzurufen.
Puppeteer AXTree-Durchlauf
Hier wird stattdessen der vollständige Baum für die Barrierefreiheit über CDP abgerufen und in Puppeteer durchlaufen. Die resultierenden Bedienungshilfen-Knoten werden dann DOM-Knoten zugeordnet.
CDP-DOM-Durchlauf
Für diesen Prototyp haben wir einen neuen CDP-Endpunkt speziell für die Abfrage der Barrierefreiheitsstruktur implementiert. Auf diese Weise kann die Abfrage im Back-End über eine C++-Implementierung statt im Seitenkontext über JavaScript erfolgen.
Benchmark für Einheitentest
In der folgenden Abbildung wird die Gesamtlaufzeit der 1.000-maligen Abfrage von vier Elementen für die drei Prototypen verglichen. Die Benchmark wurde in drei verschiedenen Konfigurationen durchgeführt, bei denen die Seitengröße unterschiedlich war und ob das Caching von Elementen der Barrierefreiheit aktiviert war.
Es ist deutlich erkennbar, dass zwischen dem CDP-gestützten Abfragemechanismus und den beiden anderen, ausschließlich in Puppeteer implementierten Leistungsunterschieden eine erhebliche Leistungslücke besteht, und dass der relative Unterschied mit der Seitengröße dramatisch zunimmt. Es ist ziemlich interessant zu sehen, dass der Prototyp des JS DOM Traversal so gut reagiert, um Caching für Bedienungshilfen zu ermöglichen. Ist das Caching deaktiviert, wird der Baum für die Barrierefreiheit bei Bedarf berechnet und verwirft ihn nach jeder Interaktion, wenn die Domain deaktiviert ist. Wenn Sie die Domain aktivieren, speichert Chromium stattdessen die berechnete Baumstruktur im Cache.
Beim JS DOM-Durchlauf fragen wir nach dem barrierefreien Namen und der Rolle für jedes Element während des Durchlaufs. Wenn Caching deaktiviert ist, berechnet und verwirft Chromium die Barrierefreiheitsstruktur für jedes aufgerufene Element. Bei den CDP-basierten Ansätzen wird der Baum jedoch nur zwischen den CDP-Aufrufen, also bei jeder Abfrage, verworfen. Diese Ansätze profitieren auch von der Aktivierung des Cachings, da der Baum für die Barrierefreiheit dann über alle CDP-Aufrufe hinweg beibehalten wird. Die Leistungssteigerung ist daher vergleichsweise geringer.
Obwohl das Aktivieren von Caching hier wünschenswert erscheint, ist dies mit Kosten für zusätzliche Arbeitsspeichernutzung verbunden. Bei Puppeteer-Skripts, die z. B. Trace-Dateien aufzeichnen, könnte dies problematisch sein. Aus diesem Grund haben wir uns entschieden, das Caching der Baumstruktur für Barrierefreiheit standardmäßig nicht zu aktivieren. Nutzer können das Caching selbst aktivieren, indem sie die CDP-Domain für Barrierefreiheit aktivieren.
Benchmark der Entwicklertools-Testsuite
Die vorherige Benchmark hat gezeigt, dass die Implementierung unseres Abfragemechanismus auf der CDP-Ebene in einem klinischen Einheitentestszenario zu einer Leistungssteigerung führt.
Um zu sehen, ob der Unterschied so ausgeprägt ist, dass er in einem realistischeren Szenario bei der Ausführung einer vollständigen Testsuite erkennbar ist, haben wir die End-to-End-Testsuite der Entwicklertools gepatcht, um die JavaScript- und CDP-basierten Prototypen zu verwenden und die Laufzeiten zu vergleichen. In dieser Benchmark haben wir insgesamt 43 Selektoren von [aria-label=…]
in einen benutzerdefinierten Abfrage-Handler aria/…
geändert, den wir dann mit jedem der Prototypen implementiert haben.
Einige der Selektoren werden mehrmals in Testskripts verwendet. Die tatsächliche Anzahl der Ausführungen des aria
-Abfrage-Handlers betrug also 113 pro Ausführung der Suite. Insgesamt wurden 2.253 Abfragen ausgewählt, sodass nur ein Bruchteil der Abfrageauswahl durch die Prototypen erfolgte.
Wie in der Abbildung oben zu sehen ist, besteht ein deutlicher Unterschied bei der Gesamtlaufzeit. Die Daten sind zu ungenau, um eine spezifische Schlussfolgerung zu ziehen, aber es ist klar, dass der Leistungsunterschied zwischen den beiden Prototypen auch in diesem Szenario deutlich wird.
Einen neuen CDP-Endpunkt
Angesichts der oben genannten Benchmarks und da der auf Start-Flags basierende Ansatz im Allgemeinen unerwünscht war, haben wir uns entschieden, einen neuen CDP-Befehl zur Abfrage des Barrierefreiheitsbaums zu implementieren. Jetzt mussten wir die Schnittstelle dieses neuen Endpunkts herausfinden.
Für unseren Anwendungsfall in Puppeteer muss der Endpunkt das sogenannte RemoteObjectIds
als Argument verwenden. Damit wir die entsprechenden DOM-Elemente anschließend finden können, sollte er eine Liste von Objekten zurückgeben, die das backendNodeIds
für die DOM-Elemente enthalten.
Wie in der folgenden Tabelle dargestellt, haben wir eine Reihe von Ansätzen ausprobiert, die für diese Schnittstelle geeignet sind. Daraus haben wir herausgefunden, dass die Größe der zurückgegebenen Objekte, d. h. ob wir vollständige Barrierefreiheitsknoten zurückgegeben haben oder nur die backendNodeIds
, keinen erkennbaren Unterschied machte. Andererseits haben wir festgestellt, dass die Verwendung des vorhandenen NextInPreOrderIncludingIgnored
eine schlechte Wahl war, um die Durchlauflogik hier zu implementieren, da dies zu einer deutlichen Verlangsamung führte.
Zusammenfassung
Mit dem vorhandenen CDP-Endpunkt haben wir nun den Abfrage-Handler auf der Puppeteer-Seite implementiert. Der Hauptteil der Arbeit bestand darin, den Code für die Abfrageverarbeitung neu zu strukturieren, sodass Abfragen direkt über CDP aufgelöst werden konnten, anstatt über JavaScript, das im Seitenkontext ausgewertet wird.
Nächste Schritte
Der neue aria
-Handler, der mit Puppeteer v5.4.0 als integrierten Abfrage-Handler geliefert wurde Wir sind schon gespannt darauf, wie unsere Nutzer diese Funktion in ihre Testskripts einbauen.
Vorschaukanäle herunterladen
Sie können Canary, Dev oder Beta als Standardbrowser für die Entwicklung verwenden. Über diese Vorschaukanäle erhalten Sie Zugriff auf die neuesten Entwicklertools, können innovative Webplattform-APIs testen und Probleme auf Ihrer Website erkennen, bevor Ihre Nutzer es tun.
Kontaktaufnahme mit dem Team für Chrome-Entwicklertools
Mit den folgenden Optionen kannst du die neuen Funktionen und Änderungen in dem Beitrag oder andere Aspekte der Entwicklertools besprechen.
- Senden Sie uns über crbug.com einen Vorschlag oder Feedback.
- Problem mit den Entwicklertools über Weitere Optionen melden
> Hilfe > Hier kannst du Probleme mit den Entwicklertools in den Entwicklertools melden.
- Twittern Sie unter @ChromeDevTools.
- Hinterlasse Kommentare in den YouTube-Videos mit den Neuerungen in den Entwicklertools oder in YouTube-Videos mit Tipps zu den Entwicklertools.