Puppetaria: Puppeteer-Skripte, bei denen die Barrierefreiheit im Mittelpunkt steht

Johan Bay
Johan Bay

Puppeteer und sein Ansatz für Selectors

Puppeteer ist eine Browserautomatisierungsbibliothek für Node. Sie ermöglicht die Steuerung eines Browsers über eine einfache und moderne JavaScript-API.

Die auffälligste Aufgabe des Browsers ist natürlich das Durchsuchen von Webseiten. Das Automatisieren dieser Aufgabe entspricht im Wesentlichen der Automatisierung von Interaktionen mit der Webseite.

In Puppeteer erfolgt dies durch Abfragen von DOM-Elementen mithilfe von stringbasierten Selektoren und Ausführen von Aktionen wie Klicken oder Tippen auf die Elemente. Beispiel: Ein Script, das developer.google.com öffnet, das Suchfeld findet und nach puppetaria sucht, könnte so 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, die, obwohl sie ausdrucksstark sehr leistungsstark sind, Nachteile bei der dauerhaften Browserinteraktion in Skripts haben können.

Syntaktische vs. semantische Selectors

CSS-Selektoren sind syntaktischer Natur. Sie sind eng an die Funktionsweise der Textdarstellung der DOM-Baumstruktur gebunden, da sie auf IDs und Klassennamen aus dem DOM verweisen. Sie stellen also ein wichtiges Tool für Webentwickler dar, mit dem sie Stile zu einem Element auf einer Seite ändern oder hinzufügen können. In diesem Kontext hat der Entwickler jedoch die volle Kontrolle über die Seite und ihren DOM-Baum.

Ein Puppeteer-Skript hingegen ist ein externer Beobachter einer Seite. Wenn also CSS-Selektoren in diesem Kontext verwendet werden, bringt es versteckte Annahmen zur Implementierung der Seite auf sich, über die das Puppeteer-Skript keine Kontrolle hat.

Dies hat zur Folge, dass solche Skripts anfällig für Quellcodeänderungen sein können. Angenommen, in einem Beispiel werden Puppeteer-Skripts für automatisierte Tests für eine Webanwendung verwendet, die den Knoten <button>Submit</button> als drittes untergeordnetes Element des body-Elements enthält. Ein Snippet aus einem Testlauf könnte so aussehen:

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

Hier verwenden wir die Auswahl 'body:nth-child(3)', um die Schaltfläche zum Senden zu finden, aber diese ist eng an genau diese Version der Webseite gebunden. Wenn später ein Element über der Schaltfläche hinzugefügt wird, funktioniert die Auswahl nicht mehr.

Für Testautoren ist das nichts Neues: Puppeteer-Nutzer versuchen bereits, Selektoren auszuwählen, die solchen Änderungen standhalten können. Mit Puppetaria erhalten Nutzer in dieser Mission ein neues Tool.

Puppeteer wird jetzt mit einem alternativen Abfrage-Handler ausgeliefert, der auf der Abfrage des Baums für Barrierefreiheit basiert und nicht auf CSS-Selektoren basiert. Die zugrunde liegende Philosophie besagt, dass sich, wenn sich das konkrete Element, das wir auswählen möchten, nicht geändert hat, der entsprechende Bedienungshilfen-Knoten auch nicht geändert haben sollte.

Wir nennen solche Selektoren ARIA-Selektoren und unterstützen die Abfrage des berechneten Namens und der Rolle der Barrierefreiheitsstruktur. Im Vergleich zu CSS-Selektoren sind diese Eigenschaften semantisch. Sie sind nicht an syntaktische Eigenschaften des DOMs gebunden, sondern an Deskriptoren dafür, wie die Seite durch Hilfstechnologien wie Screenreader beobachtet wird.

Im Testskriptbeispiel oben könnten wir stattdessen die Auswahl aria/Submit[role="button"] verwenden, um die gewünschte Schaltfläche auszuwählen, wobei Submit auf den zugänglichen Namen des Elements verweist:

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

Wenn wir jetzt den Textinhalt unserer Schaltfläche von Submit in Done ändern, schlägt der Test wieder fehl, in diesem Fall ist das aber wünschenswert. Wenn wir den Namen der Schaltfläche ändern, ändern wir den Inhalt der Seite und nicht die visuelle Darstellung oder die Struktur im DOM. Durch unsere Tests sollten wir vor solchen Änderungen gewarnt werden, um sicherzustellen, dass die Änderungen beabsichtigt sind.

Wenn wir zum größeren Beispiel mit der Suchleiste zurückkehren, 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 sind wir der Meinung, dass die Verwendung solcher ARIA-Selektoren für Puppeteer-Nutzer folgende Vorteile bietet:

  • Selektoren in Testskripts widerstandsfähiger gegenüber Quellcodeänderungen machen
  • Lesbarkeit von Testskripts verbessern (zugängliche Namen sind semantische Deskriptoren).
  • Animieren zu Best Practices für das Zuweisen von Bedienungshilfen zu Elementen

Im weiteren Verlauf dieses Artikels werden die Details zur Implementierung des Puppetaria-Projekts ausführlich beschrieben.

Der Designprozess

Hintergrund

Wie oben bereits erwähnt, möchten wir das Abfragen von Elementen über ihren zugänglichen Namen und ihre zugängliche Rolle ermöglichen. Dies sind Eigenschaften des Baum für Barrierefreiheit, eine Gemeinsamkeit des DOM-Baums, der von Geräten wie Screenreadern zum Anzeigen von Webseiten verwendet wird.

Aus der Spezifikation zur Berechnung des barrierefreien Namens geht deutlich hervor, dass die Berechnung des Namens für ein Element eine nicht triviale Aufgabe ist. Daher haben wir uns von Anfang an entschieden, die vorhandene Infrastruktur von Chromium dafür zu nutzen.

Unsere Herangehensweise

Auch wenn wir uns nur auf die Verwendung des Baums für Barrierefreiheit von Chromium beschränken, gibt es zahlreiche Möglichkeiten, ARIA-Abfragen in Puppeteer zu implementieren. Um den Grund dafür zu erfahren, sehen wir uns zuerst an, wie Puppeteer den Browser steuert.

Der Browser stellt über das Protokoll Chrome DevTools Protocol (CDP) eine Debugging-Oberfläche bereit. Dadurch werden Funktionen wie „Seite neu laden“ oder „Diesen JavaScript-Code auf der Seite ausführen und das Ergebnis über eine sprachunabhängige Benutzeroberfläche zurückgeben“ angezeigt.

Sowohl das DevTools-Front-End als auch Puppeteer verwenden CDP, um mit dem Browser zu kommunizieren. Zur Implementierung von CDP-Befehlen befindet sich die DevTools-Infrastruktur in allen Chrome-Komponenten: im Browser, im Renderer usw. CDP sorgt dafür, dass die Befehle an die richtige Stelle weitergeleitet werden.

Puppeteer-Aktionen wie Abfragen, Klicks und Auswertung von Ausdrücken werden mit CDP-Befehlen wie Runtime.evaluate ausgeführt, die JavaScript direkt im Seitenkontext bewerten und das Ergebnis zurückgeben. Andere Puppeteer-Aktionen wie die Emulation von Farbblindheit, das Erstellen von Screenshots oder das Aufzeichnen von Traces verwenden CDP, um direkt mit dem Blink-Renderingprozess zu kommunizieren.

CDP

Damit haben wir bereits zwei Möglichkeiten zur Implementierung der Abfragefunktion:

  • Schreiben Sie unsere Abfragelogik in JavaScript und fügen Sie sie mit Runtime.evaluate in die Seite ein.
  • Verwenden Sie einen CDP-Endpunkt, der direkt im Blink-Prozess auf die Struktur der Bedienungshilfen zugreifen und diese abfragen kann.

Wir implementierten 3 Prototypen:

  • JS-DOM-Durchlauf – basierend auf der JavaScript-Einschleusung in die Seite
  • Puppeteer AXTree-Durchlauf – basierend auf der Nutzung des vorhandenen CDP-Zugriffs auf den Accessibility Tree
  • CDP-DOM-Durchlauf: Verwendung eines neuen CDP-Endpunkts, der speziell für die Abfrage der Barrierefreiheitsstruktur entwickelt wurde

JS-DOM-Durchlauf

Dieser Prototyp führt einen vollständigen Durchlauf des DOM durch und verwendet element.computedName und element.computedRole, die durch das ComputedAccessibilityInfo-Start-Flag gesteuert werden, um den Namen und die Rolle für jedes Element während des Durchlaufs abzurufen.

Puppeteer AXTree Traversal

Hier rufen wir stattdessen den vollständigen Baum für Barrierefreiheit über CDP ab und durchlaufen ihn in Puppeteer. Die resultierenden Knoten für Bedienungshilfen werden dann DOM-Knoten zugeordnet.

CDP-DOM-Durchlauf

Für diesen Prototyp haben wir einen neuen CDP-Endpunkt implementiert, der speziell für die Abfrage des Baums für Barrierefreiheit im Internet vorgesehen ist. Auf diese Weise kann die Abfrage im Back-End über eine C++-Implementierung und nicht über JavaScript im Seitenkontext erfolgen.

Unittest-Benchmark

In der folgenden Abbildung wird die Gesamtlaufzeit der Abfrage von vier Elementen 1.000-mal für die drei Prototypen verglichen. Der Benchmark wurde in drei verschiedenen Konfigurationen durchgeführt, wobei die Seitengröße variiert und das Caching von Bedienungshilfen aktiviert wurde.

Benchmark: Gesamtlaufzeit von 1.000 Abfragen von vier Elementen

Es ist klar, dass es eine erhebliche Leistungsdifferenz zwischen dem CDP-gestützten Abfragemechanismus und den beiden anderen Mechanismen gibt, die ausschließlich in Puppeteer implementiert sind, und der relative Unterschied scheint mit der Seitengröße drastisch zuzunehmen. Es ist einigermaßen interessant zu sehen, dass der Prototyp für den JS-DOM-Durchlauf so gut auf das Aktivieren von Barrierefreiheits-Caching reagiert. Ist Caching deaktiviert, wird der Baum für Barrierefreiheit bei Bedarf berechnet und verwirft ihn nach jeder Interaktion, wenn die Domain deaktiviert ist. Wenn die Domain aktiviert wird, speichert Chromium stattdessen den berechneten Baum im Cache.

Für den 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 den Baum für die Barrierefreiheit für jedes besuchte Element. Bei CDP-basierten Ansätzen wird der Baum hingegen nur zwischen den einzelnen CDP-Aufrufen verworfen, d.h. für jede Abfrage. Diese Ansätze profitieren auch von der Aktivierung von Caching, da der Baum für Barrierefreiheit dann über CDP-Aufrufe hinweg beibehalten wird, aber die Leistungssteigerung damit vergleichsweise kleiner ist.

Obwohl das Aktivieren des Caching hier wünschenswert erscheint, geht es mit Kosten zusätzlicher Arbeitsspeichernutzung einher. Bei Puppeteer-Skripts, die beispielsweise Trace-Dateien aufzeichnen, könnte dies problematisch sein. Daher haben wir uns entschieden, das Baum-Caching für Barrierefreiheit nicht standardmäßig zu aktivieren. Nutzer können das Caching selbst aktivieren, indem sie die CDP-Domain für Barrierefreiheit aktivieren.

Entwicklertools-Testsuite-Benchmark

Die vorherige Benchmark zeigte, dass die Implementierung unseres Abfragemechanismus auf der CDP-Ebene in einem Szenario mit klinischen Einheitentests zu einer Leistungssteigerung führt.

Um herauszufinden, ob der Unterschied in einem realistischeren Szenario der Ausführung einer vollständigen Test-Suite so ausgeprägt ist, dass er sichtbar ist, haben wir die End-to-End-Testsuite der Entwicklertools gepatcht, um JavaScript- und CDP-basierte 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, sodass die tatsächliche Anzahl der Ausführungen des aria-Abfrage-Handlers pro Ausführung der Suite 113 betrug. Es wurden insgesamt 2.253 Abfragen ausgewählt, sodass nur ein Bruchteil der Abfrageauswahl durch die Prototypen erfolgte.

Benchmark: e2e-Testsuite

Wie in der Abbildung oben zu sehen ist, gibt es einen erkennbaren Unterschied bei der Gesamtlaufzeit. Die Daten sind zu ungenau, um irgendetwas Spezifisches zu schlussfolgern, aber es ist auch in diesem Szenario klar, dass die Leistungslücke zwischen den beiden Prototypen zeigt.

Ein neuer CDP-Endpunkt

Angesichts der oben genannten Benchmarks und da der auf Flags basierende Ansatz allgemein unerwünscht war, haben wir uns entschieden, mit der Implementierung eines neuen CDP-Befehls zum Abfragen des Baums für Barrierefreiheit weiterzumachen. Jetzt mussten wir die Benutzeroberfläche des neuen Endpunkts herausfinden.

Für unseren Anwendungsfall in Puppeteer muss der Endpunkt das sogenannte RemoteObjectIds als Argument verwenden. Damit wir anschließend die entsprechenden DOM-Elemente finden können, sollte er eine Liste von Objekten zurückgeben, die den backendNodeIds für die DOM-Elemente enthält.

Wie in der nachfolgenden Tabelle dargestellt, haben wir einige Ansätze ausprobiert, die für diese Benutzeroberfläche geeignet sind. Hier haben wir festgestellt, dass die Größe der zurückgegebenen Objekte keinen erkennbaren Unterschied macht, d. h., ob wir vollständige Bedienungshilfen-Knoten oder nur backendNodeIds zurückgegeben haben. Andererseits haben wir festgestellt, dass die Verwendung des vorhandenen NextInPreOrderIncludingIgnored keine gute Wahl für die Implementierung der Durchlauflogik hier war, da dies zu einer deutlichen Verlangsamung geführt hat.

Benchmark: Vergleich von CDP-basierten AXTree-Traversal-Prototypen

Zusammenfassung

Mit dem vorhandenen CDP-Endpunkt haben wir nun den Abfrage-Handler auf der Puppeteer-Seite implementiert. Der größte Teil der Arbeit bestand darin, den Abfrageverarbeitungscode so umzustrukturieren, dass Abfragen direkt über CDP aufgelöst werden konnten, anstatt Abfragen über JavaScript im Seitenkontext auszuführen.

Nächste Schritte

Der neue aria-Handler, der mit der Puppeteer-Version 5.4.0 als integriertem Abfrage-Handler ausgeliefert wurde. Wir freuen uns darauf, zu sehen, wie Nutzer das Tool in ihre Testskripts aufnehmen, und sind gespannt auf eure Ideen, wie wir dies noch nützlicher machen können!

Vorschaukanäle herunterladen

Du kannst Chrome Canary, Dev oder Beta als Standardbrowser für die Entwicklung verwenden. Mit diesen Vorschaukanälen erhalten Sie Zugriff auf die neuesten Funktionen der Entwicklertools, können bahnbrechende Webplattform-APIs testen und Probleme auf Ihrer Website erkennen, noch bevor Ihre Nutzer dies tun.

Chrome-Entwicklertools-Team kontaktieren

Verwende die folgenden Optionen, um die neuen Funktionen und Änderungen im Beitrag oder andere Themen im Zusammenhang mit den Entwicklertools zu besprechen.

  • Sende uns über crbug.com einen Vorschlag oder Feedback.
  • Wenn du ein Problem mit den Entwicklertools melden möchtest, klicke in den Entwicklertools auf Weitere Optionen   Mehr   > Hilfe > Probleme mit Entwicklertools melden.
  • Senden Sie einen Tweet an @ChromeDevTools.
  • Hinterlasse Kommentare unter YouTube-Videos oder YouTube-Videos mit Tipps zu DevTools.