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.js. Damit können Sie einen Browser mit einer einfachen und modernen JavaScript API steuern.

Die wichtigste Aufgabe eines Browsers ist natürlich das Surfen im Web. Die Automatisierung dieser Aufgabe entspricht im Wesentlichen der Automatisierung von Interaktionen mit der Webseite.

In Puppeteer geschieht dies, indem mithilfe von stringbasierten Selektoren nach DOM-Elementen gesucht und Aktionen wie das Klicken auf die Elemente oder das Eintippen von Text ausgeführt werden. Ein Script, das beispielsweise 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');
 })();

Wie Elemente mithilfe von Abfrageselektiven identifiziert werden, ist daher ein entscheidender Bestandteil der Puppeteer-Umgebung. Bisher waren Selektoren in Puppeteer auf CSS- und XPath-Selektoren beschränkt, die zwar sehr leistungsfähig sind, aber Nachteile bei der Beibehaltung von Browserinteraktionen in Scripts haben können.

Syntaktische und semantische Selektoren

CSS-Selektoren sind syntaktisch. Sie sind eng mit der internen Funktionsweise der textbasierten Darstellung des DOM-Baums verbunden, da sie auf IDs und Klassennamen aus dem DOM verweisen. Daher sind sie ein unverzichtbares Tool für Webentwickler, um Elemente auf einer Seite zu ändern oder ihnen Stile hinzuzufügen. In diesem Kontext hat der Entwickler jedoch die vollständige Kontrolle über die Seite und ihr DOM-Baum.

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

Das hat zur Folge, dass solche Scripts empfindlich und anfällig für Änderungen am Quellcode sind. Angenommen, Sie verwenden Puppeteer-Scripts 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. Diese ist jedoch genau an diese Version der Webseite gebunden. Wenn später ein Element über der Schaltfläche hinzugefügt wird, funktioniert diese Auswahl nicht mehr.

Für Testautoren ist das nichts Neues: Puppeteer-Nutzer versuchen bereits, Auswahlen auszuwählen, die robust gegen solche Änderungen sind. Mit Puppetaria bieten wir Nutzern ein neues Tool für diese Herausforderung.

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

Wir nennen solche Auswahlschaltflächen ARIA-Auswahlschaltflächen und unterstützen Abfragen nach dem berechneten barrierefreien Namen und der Rolle des Baums für Barrierefreiheit. Im Vergleich zu den CSS-Selektoren sind diese Properties semantisch. Sie sind nicht an syntaktische Eigenschaften des DOM gebunden, sondern beschreiben, wie die Seite über Hilfstechnologien wie Screenreader betrachtet wird.

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

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

Wenn wir später den Textinhalt unserer Schaltfläche von Submit in Done ändern, schlägt der Test wieder fehl. In diesem Fall ist das aber wünschenswert: Durch Ändern des Namens der Schaltfläche ändern wir den Inhalt der Seite, nicht ihre visuelle Darstellung oder die Struktur im DOM. Unsere Tests sollten uns vor solchen Änderungen warnen, damit wir sicher sein können, dass sie beabsichtigt sind.

Zurück zum größeren Beispiel mit der Suchleiste: Wir könnten 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"]');

zu finden.

Im Allgemeinen sind wir der Meinung, dass die Verwendung solcher ARIA-Selektoren für Puppeteer-Nutzer folgende Vorteile bietet:

  • Erhöhen Sie die Widerstandsfähigkeit von Auswahlelementen in Testscripts gegenüber Quellcodeänderungen.
  • Testscripts lesbarer gestalten (barrierefreie Namen sind semantische Beschreibungen).
  • Erläutern Sie Best Practices für die Zuweisung von Bedienungshilfeneigenschaften zu Elementen.

Im Rest dieses Artikels erfahren Sie, wie wir das Puppetaria-Projekt implementiert haben.

Der Designprozess

Hintergrund

Wie oben erläutert, möchten wir es ermöglichen, Elemente anhand ihres zugänglichen Namens und ihrer Rolle abzufragen. Dies sind Eigenschaften des Bedienungshilfen-Baums, der dem üblichen DOM-Baum entspricht und von Geräten wie Screenreadern zum Anzeigen von Webseiten verwendet wird.

Die Spezifikation zum Berechnen des barrierefreien Namens zeigt, dass das Berechnen des Namens für ein Element keine triviale Aufgabe ist. Daher haben wir von Anfang an beschlossen, die vorhandene Infrastruktur von Chromium dafür wiederzuverwenden.

So haben wir die Funktion implementiert

Selbst wenn wir uns auf den Chromium-Zugänglichkeitsbaum 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 Debug-Oberfläche über ein Protokoll namens Chrome DevTools Protocol (CDP) bereit. So können 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 aufgerufen werden.

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

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 nutzen CDP, um direkt mit dem Blink-Rendering-Prozess zu kommunizieren.

CDP

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

  • Die Abfragelogik in JavaScript schreiben und mit Runtime.evaluate in die Seite einfügen oder
  • Verwenden Sie einen CDP-Endpunkt, der direkt im Blink-Prozess auf den Bedienungshilfen-Baum zugreifen und ihn abfragen kann.

Wir haben drei Prototypen implementiert:

  • JS-DOM-Durchlauf: JavaScript wird in die Seite eingefügt.
  • Puppeteer AXTree-Durchlauf: Basiert auf der Verwendung des vorhandenen CDP-Zugriffs auf den Bedienungshilfen-Baum
  • CDP-DOM-Durchlauf: Ein neuer CDP-Endpunkt, der speziell für die Abfrage des Baums für Barrierefreiheit entwickelt wurde

JS-DOM-Durchlauf

Dieser Prototyp führt eine vollständige Durchlaufprüfung des DOM durch und verwendet element.computedName und element.computedRole, die vom ComputedAccessibilityInfo-Startflag abhängig sind, um während der Durchlaufprüfung den Namen und die Rolle jedes Elements abzurufen.

Puppeteer AXTree-Durchlauf

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

CDP-DOM-Durchlauf

Für diesen Prototyp haben wir einen neuen CDP-Endpunkt speziell für die Abfrage des Bedienungshilfen-Baums implementiert. So können Abfragen im Back-End über eine C++-Implementierung statt im Seitenkontext über JavaScript erfolgen.

Benchmark für Unit-Tests

In der folgenden Abbildung wird die Gesamtlaufzeit verglichen, die für die Abfrage von vier Elementen 1.000 Mal für die drei Prototypen benötigt wird. Der Benchmark wurde in drei verschiedenen Konfigurationen ausgeführt, wobei die Seitengröße und das Caching von Elementen für Barrierefreiheit variiert wurden.

Benchmark: Gesamtlaufzeit der Abfrage von vier Elementen 1.000 Mal

Es ist ganz klar, dass es einen erheblichen Leistungsunterschied zwischen dem CDP-gestützten Abfragemechanismus und den beiden anderen gibt, die ausschließlich in Puppeteer implementiert sind. Der relative Unterschied scheint mit der Seitengröße dramatisch zu zunehmen. Es ist interessant zu sehen, dass der JS-DOM-Durchlauf-Prototyp so gut auf das Aktivieren des Cachings für die Barrierefreiheit reagiert. Wenn das Caching deaktiviert ist, wird der Baum für Barrierefreiheit bei Bedarf berechnet und nach jeder Interaktion verworfen, wenn die Domain deaktiviert ist. Wenn Sie die Domain aktivieren, speichert Chromium stattdessen den berechneten Baum im Cache.

Bei der JS-DOM-Durchsuchung fragen wir während der Durchsuchung nach dem barrierefreien Namen und der Rolle jedes Elements. Wenn das Caching deaktiviert ist, berechnet und verwirft Chromium den Baum für die Barrierefreiheit für jedes Element, das wir aufrufen. Bei den CDP-basierten Ansätzen wird der Baum dagegen nur zwischen den einzelnen Aufrufen der CDP, also für jede Abfrage, verworfen. Bei diesen Ansätzen ist auch das Caching von Vorteil, da der Baum für die Barrierefreiheit dann über alle CDP-Aufrufe hinweg beibehalten wird. Der Leistungsanstieg ist jedoch vergleichsweise geringer.

Auch wenn das Aktivieren des Cachings hier wünschenswert erscheint, geht es mit einer zusätzlichen Arbeitsspeichernutzung einher. Bei Puppeteer-Scripts, die z. B. Trace-Dateien aufzeichnen, kann dies problematisch sein. Daher haben wir uns entschieden, das Caching des Bedienungshilfen-Baums standardmäßig nicht zu aktivieren. Nutzer können das Caching selbst aktivieren, indem sie die Zugänglichkeitsdomain der CDP aktivieren.

Benchmark für DevTools-Testsuite

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

Um zu sehen, ob der Unterschied ausgeprägt genug ist, um in einem realistischeren Szenario beim Ausführen einer vollständigen Testsuite bemerkbar zu werden, haben wir die End-to-End-Testsuite von DevTools gepatcht, um die JavaScript- und CDP-basierten Prototypen zu verwenden, und die Laufzeiten verglichen. In diesem Benchmark haben wir insgesamt 43 Auswählte von [aria-label=…] in einen benutzerdefinierten Abfrage-Handler aria/… geändert, den wir dann mit jedem der Prototypen implementiert haben.

Einige der Auswahlkriterien werden in Testscripts mehrmals verwendet. Die tatsächliche Anzahl der Ausführungen des aria-Abfrage-Handlers betrug daher 113 pro Ausführung der Suite. Die Gesamtzahl der Suchanfragen betrug 2.253, also wurde nur ein Bruchteil der Suchanfragen über die Prototypen ausgewählt.

Benchmark: End-to-End-Testsuite

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

Einen neuen CDP-Endpunkt

Angesichts der oben genannten Benchmarks und da der richtlinienbasierte Ansatz für die Einführung im Allgemeinen nicht wünschenswert war, haben wir uns entschieden, einen neuen CDP-Befehl zur Abfrage des Bedienungshilfen-Baums zu implementieren. Jetzt mussten wir die Benutzeroberfläche dieses neuen Endpunkts herausfinden.

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

Wie das Diagramm unten zeigt, haben wir einige Ansätze ausprobiert, die diese Anforderungen 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 oder nur die backendNodeIds zurückgegeben haben, keinen merklichen Unterschied macht. Andererseits haben wir festgestellt, dass die Verwendung der vorhandenen NextInPreOrderIncludingIgnored für die Implementierung der Durchlauflogik hier keine gute Wahl war, da dies zu einer deutlichen 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 zur Abfrageverwaltung so umzustrukturieren, dass Abfragen direkt über die CDP aufgelöst werden können, anstatt über JavaScript, das im Seitenkontext ausgewertet wird.

Nächste Schritte

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

Vorschaukanäle herunterladen

Verwenden Sie als Standard-Entwicklungsbrowser Chrome Canary, Chrome Dev oder Chrome Beta. Diese Vorabversionen bieten Ihnen Zugriff auf die neuesten DevTools-Funktionen, ermöglichen es Ihnen, innovative Webplattform-APIs zu testen, und helfen Ihnen, Probleme auf Ihrer Website zu finden, bevor Ihre Nutzer sie bemerken.

Chrome-Entwicklertools-Team kontaktieren

Mit den folgenden Optionen können Sie über neue Funktionen, Updates oder andere Themen im Zusammenhang mit den DevTools sprechen.