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

Johan Bay
Johan Bay

Puppeteer und sein Ansatz für Selektoren

Puppeteer ist eine Browserautomatisierungsbibliothek für Node, mit der Sie einen Browser über eine einfache und moderne JavaScript API steuern können.

Die wichtigste Aufgabe des Browsers ist natürlich das Aufrufen von Webseiten. Die Automatisierung dieser Aufgabe läuft im Wesentlichen auf die Automatisierung von Interaktionen mit der Webseite hinaus.

In Puppeteer wird dies erreicht, indem DOM-Elemente mithilfe von stringbasierten Selektoren abgefragt und Aktionen wie Klicken oder Texteingabe für die Elemente ausgeführt werden. Ein Skript, das developer.google.com öffnet, das Suchfeld findet und nach puppetaria sucht, könnte beispielsweise 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 entscheidender Teil der Puppeteer-Nutzung. Bisher waren Selektoren in Puppeteer auf CSS- und XPath-Selektoren beschränkt. Diese sind zwar sehr ausdrucksstark, können aber Nachteile für die Beibehaltung von Browserinteraktionen in Skripts haben.

Syntaktische und semantische Selektoren

CSS-Selektoren sind syntaktischer Natur. Sie sind eng mit der Funktionsweise der Textdarstellung des DOM-Baums verbunden, da sie auf IDs und Klassennamen aus dem DOM verweisen. Sie sind daher ein wichtiges Tool für Webentwickler, um Stile für ein Element auf einer Seite zu ändern oder hinzuzufügen. In diesem Kontext hat der Entwickler jedoch die volle Kontrolle über die Seite und ihren DOM-Baum.

Ein Puppeteer-Script ist dagegen ein externer Beobachter einer Seite. Wenn CSS-Selektoren in diesem Kontext verwendet werden, werden versteckte Annahmen darüber eingeführt, wie die Seite implementiert ist, auf die das Puppeteer-Script keinen Einfluss hat.

Das hat zur Folge, dass solche Skripts fehleranfällig und anfällig für Änderungen am Quellcode sein können. Angenommen, Sie verwenden 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 Testlauf könnte so aussehen:

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

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

Das ist für Testautoren nichts Neues: Puppeteer-Nutzer versuchen bereits, Selektoren auszuwählen, die gegenüber solchen Änderungen robust sind. Mit Puppetaria geben wir Nutzern ein neues Tool für diese Suche an die Hand.

Puppeteer wird jetzt mit einem alternativen Abfrage-Handler ausgeliefert, der auf der Abfrage der Baumstruktur für Barrierefreiheit im Internet basiert, anstatt sich auf CSS-Selektoren zu verlassen. Die zugrunde liegende Philosophie ist, dass sich der entsprechende Knoten für Bedienungshilfen nicht geändert haben sollte, wenn sich das konkrete Element, das wir auswählen möchten, nicht geändert hat.

Diese Selektoren werden als ARIA-Selektoren bezeichnet. Sie unterstützen Abfragen für den berechneten barrierefreien Namen und die Rolle des Barrierefreiheitsbaums. Im Vergleich zu den CSS-Selektoren sind diese Attribute semantischer Natur. Sie sind nicht an syntaktische Eigenschaften des DOM gebunden, sondern beschreiben, wie die Seite durch Hilfstechnologien wie Screenreader wahrgenommen wird.

Im obigen Beispiel für ein Testskript könnten wir stattdessen den Selektor aria/Submit[role="button"] verwenden, um die gewünschte Schaltfläche auszuwählen, wobei sich Submit auf den barrierefreien Namen des Elements bezieht:

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

Wenn wir später den Textinhalt unseres Buttons von Submit in Done ändern, schlägt der Test wieder fehl. In diesem Fall ist das aber erwünscht, denn durch die Änderung des Namens des Buttons ändern wir den Inhalt der Seite, nicht ihre visuelle Darstellung oder ihre Struktur im DOM. Unsere Tests sollten uns vor solchen Änderungen warnen, damit wir sicherstellen können, dass sie beabsichtigt sind.

Wenn wir zum größeren Beispiel mit der Suchleiste zurückkehren, könnten wir den neuen aria-Handler nutzen und Folgendes ersetzen:

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.

Allgemein sind wir der Meinung, dass die Verwendung solcher ARIA-Selektoren Puppeteer-Nutzern die folgenden Vorteile bieten kann:

  • Selektoren in Testskripts widerstandsfähiger gegen Änderungen am Quellcode machen.
  • Testskripts besser lesbar machen (barrierefreie Namen sind semantische Deskriptoren)
  • Motivieren Sie zu Best Practices für das Zuweisen von Eigenschaften für Barrierefreiheit im Internet zu Elementen.

Im weiteren Verlauf dieses Artikels wird detailliert beschrieben, wie wir das Puppetaria-Projekt implementiert haben.

Der Designprozess

Hintergrund

Wie oben beschrieben, möchten wir Elemente nach ihrem barrierefreien Namen und ihrer Rolle abfragen können. Dies sind Eigenschaften der Baumstruktur für Bedienungshilfen, die ein Gegenstück zur üblichen DOM-Baumstruktur ist und von Geräten wie Screenreadern verwendet wird, um Webseiten anzuzeigen.

Ein Blick auf die Spezifikation für die Berechnung des barrierefreien Namens zeigt, dass die Berechnung des Namens für ein Element keine einfache Aufgabe ist. Daher haben wir uns von Anfang an entschieden, die vorhandene Infrastruktur von Chromium dafür wiederzuverwenden.

Wie wir die Implementierung angegangen sind

Selbst wenn wir uns auf die Verwendung des Chromium-Baums für Barrierefreiheit im Internet beschränken, gibt es einige Möglichkeiten, ARIA-Abfragen in Puppeteer zu implementieren. Sehen wir uns zuerst an, wie Puppeteer den Browser steuert.

Der Browser stellt eine Debugging-Schnittstelle über ein Protokoll namens Chrome-Entwicklertools-Protokoll (CDP) bereit. Dadurch werden Funktionen wie „Seite neu laden“ oder „Dieses JavaScript-Element auf der Seite ausführen und das Ergebnis zurückgeben“ über eine sprachunabhängige Schnittstelle verfügbar gemacht.

Sowohl das DevTools-Frontend als auch Puppeteer verwenden CDP, um mit dem Browser zu kommunizieren. Zur Implementierung von CDP-Befehlen gibt es in allen Komponenten von Chrome eine DevTools-Infrastruktur: im Browser, im Renderer usw. Das CDP leitet die Befehle an die richtige Stelle weiter.

Puppeteer-Aktionen wie Abfragen, Klicken und Auswerten von Ausdrücken werden mithilfe von CDP-Befehlen wie Runtime.evaluate ausgeführt, die JavaScript direkt im Seitenkontext auswerten und das Ergebnis zurückgeben. Andere Puppeteer-Aktionen wie das Emulieren von Farbfehlsichtigkeiten, das Erstellen von Screenshots oder das Erfassen von Traces verwenden CDP, um direkt mit dem Blink-Rendering-Prozess zu kommunizieren.

CDP

Damit haben wir bereits zwei Möglichkeiten, unsere Abfragefunktion zu implementieren:

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

Wir haben drei Prototypen implementiert:

  • JS-DOM-Durchlauf: basiert auf dem Einfügen von JavaScript in die Seite
  • Puppeteer AXTree-Durchlauf – basiert auf dem vorhandenen CDP-Zugriff auf die Bedienungshilfen-Baumstruktur
  • CDP-DOM-Durchlauf: Verwendung eines neuen CDP-Endpunkts, der speziell für die Abfrage der Baumstruktur für Barrierefreiheit im Internet entwickelt wurde

JS-DOM-Durchlauf

Dieser Prototyp durchläuft das gesamte DOM 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-Durchlauf

Hier rufen wir stattdessen die vollständige Baumstruktur für Bedienungshilfen über CDP ab und durchlaufen sie in Puppeteer. Die resultierenden Knoten für die Barrierefreiheit im Internet werden dann DOM-Knoten zugeordnet.

CDP-DOM-Durchlauf

Für diesen Prototyp haben wir einen neuen CDP-Endpunkt speziell für die Abfrage der Baumstruktur für Bedienungshilfen implementiert. So kann die Abfrage im Backend über eine C++-Implementierung statt im Seitenkontext über JavaScript erfolgen.

Benchmark für Unit-Tests

In der folgenden Abbildung wird die Gesamtlaufzeit für die Abfrage von vier Elementen 1.000 Mal für die drei Prototypen verglichen. Der Benchmark wurde in drei verschiedenen Konfigurationen ausgeführt, wobei die Seitengröße und die Aktivierung des Cachings von Elementen für Barrierefreiheit im Internet variierten.

Benchmark: Gesamtlaufzeit für die 1.000-malige Abfrage von vier Elementen

Es ist ganz offensichtlich, dass es einen erheblichen Leistungsunterschied zwischen dem CDP-gestützten Abfragemechanismus und den beiden anderen, ausschließlich in Puppeteer implementierten Mechanismen gibt. Der relative Unterschied scheint mit der Seitengröße dramatisch zuzunehmen. Es ist interessant, dass der JS-DOM-Durchlauf-Prototyp so gut auf die Aktivierung des Caching für Bedienungshilfen reagiert. Wenn das Caching deaktiviert ist, wird die Baumstruktur für Barrierefreiheit bei Bedarf berechnet und nach jeder Interaktion verworfen, wenn die Domain deaktiviert ist. Wenn die Domain aktiviert ist, speichert Chromium stattdessen die berechnete Struktur im Cache.

Bei der JS-DOM-Durchsuchung fragen wir für jedes Element den barrierefreien Namen und die Rolle ab. Wenn das Caching deaktiviert ist, berechnet Chromium den Baum für Barrierefreiheit für jedes besuchte Element und verwirft ihn dann. Bei den CDP-basierten Ansätzen wird der Baum hingegen nur zwischen den einzelnen Aufrufen von CDP verworfen, d.h. für jede Abfrage. Diese Ansätze profitieren auch von der Aktivierung des Cachings, da die Baumstruktur für Barrierefreiheit dann über CDP-Aufrufe hinweg beibehalten wird. Die Leistungssteigerung ist jedoch vergleichsweise geringer.

Auch wenn das Aktivieren des Cachings hier wünschenswert erscheint, ist es mit zusätzlichen Kosten für die Speichernutzung verbunden. Bei Puppeteer-Skripts, die z. B. Trace-Dateien aufzeichnen, kann dies problematisch sein. Daher haben wir uns entschieden, das Caching der Baumansicht für Barrierefreiheit nicht standardmäßig zu aktivieren. Nutzer können das Caching selbst aktivieren, indem sie die CDP-Domain für Bedienungshilfen aktivieren.

Benchmark für die DevTools-Test-Suite

Der vorherige Benchmark hat gezeigt, dass die Implementierung unseres Abfragemechanismus auf der CDP-Ebene in einem klinischen Unit-Test-Szenario zu einer Leistungssteigerung führt.

Um herauszufinden, ob der Unterschied groß genug ist, um in einem realistischeren Szenario mit einer vollständigen Test-Suite spürbar zu sein, haben wir die End-to-End-Test-Suite der DevTools gepatcht, um die JavaScript- und CDP-basierten Prototypen zu verwenden, und die Laufzeiten verglichen. In diesem 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 in Testskripts mehrmals verwendet. Die tatsächliche Anzahl der Ausführungen des Abfrage-Handlers aria betrug daher 113 pro Ausführung der Suite. Die Gesamtzahl der Abfrageauswahlen betrug 2.253, sodass nur ein Bruchteil der Abfrageauswahlen über die Prototypen erfolgte.

Benchmark: E2E-Test-Suite

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

Ein neuer CDP-Endpunkt

In Anbetracht der oben genannten Benchmarks und da der Ansatz mit Start-Flags im Allgemeinen nicht wünschenswert war, haben wir uns entschieden, einen neuen CDP-Befehl zum Abfragen der Baumstruktur für Bedienungshilfen zu implementieren. Jetzt mussten wir die Schnittstelle dieses neuen Endpunkts herausfinden.

Für unseren Anwendungsfall in Puppeteer benötigen wir den Endpunkt, um sogenannte RemoteObjectIds als Argument zu verwenden. Damit wir die entsprechenden DOM-Elemente danach finden können, sollte er eine Liste von Objekten zurückgeben, die die backendNodeIds für die DOM-Elemente enthält.

Wie Sie in der folgenden Tabelle sehen, haben wir verschiedene Ansätze ausprobiert, die diese Schnittstelle erfüllen. Dabei haben wir festgestellt, dass die Größe der zurückgegebenen Objekte, d. h. ob wir vollständige Knoten für die Barrierefreiheit im Internet oder nur die backendNodeIds zurückgegeben haben, keinen erkennbaren Unterschied machte. Andererseits haben wir festgestellt, dass die Verwendung des vorhandenen NextInPreOrderIncludingIgnored eine schlechte Wahl für die Implementierung der Durchlauflogik war, da dies zu einer spürbaren Verlangsamung führte.

Benchmark: Vergleich von CDP-basierten AXTree-Durchlaufprototypen

Zusammenfassung

Nachdem der CDP-Endpunkt eingerichtet war, haben wir den Abfrage-Handler auf der Puppeteer-Seite implementiert. Der Großteil der Arbeit bestand darin, den Code für die Abfrageverarbeitung so umzustrukturieren, dass Abfragen direkt über CDP aufgelöst werden können, anstatt über JavaScript, das im Seitenkontext ausgewertet wird.

Nächste Schritte

Der neue aria-Handler wurde mit Puppeteer v5.4.0 als integrierter Abfrage-Handler ausgeliefert. Wir sind gespannt, wie Nutzer die Funktion in ihre Testskripts einbinden, und freuen uns auf Ihre Ideen, wie wir sie noch nützlicher machen können.

Vorschaukanäle herunterladen

Verwenden Sie Chrome Canary, Dev oder Beta als Standardbrowser für die Entwicklung. Über diese Vorabversionen erhalten Sie Zugriff auf die neuesten Entwicklertools-Funktionen, können innovative Webplattform-APIs testen und Probleme auf Ihrer Website finden, bevor Ihre Nutzer darauf stoßen.

Chrome-Entwicklertools-Team kontaktieren

Verwenden Sie die folgenden Optionen, um über die neuen Funktionen, Updates oder andere Themen im Zusammenhang mit den Entwicklertools zu diskutieren.

  • Unter crbug.com können Sie uns Feedback und Feature Requests senden.
  • Sie können ein Problem mit den Entwicklertools melden, indem Sie in den Entwicklertools auf das Dreipunkt-Menü  Weitere Optionen > Hilfe > Problem mit den Entwicklertools melden klicken.
  • Tweeten Sie an @ChromeDevTools.
  • Hinterlassen Sie Kommentare zu den YouTube-Videos What's new in DevTools oder DevTools Tips.