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.js. Sie können damit 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 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');
 })();

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 syntaktischer Natur. Sie sind eng an die Textdarstellung des DOM-Baums gebunden, 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. 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.

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 diese Änderungen 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:

  • Selektoren in Testskripts widerstandsfähiger gegen Quellcodeänderungen
  • Testscripts lesbarer gestalten (barrierefreie Namen sind semantische Beschreibungen).
  • Erläutern Sie Best Practices für die Zuweisung von Bedienungshilfeneigenschaften zu Elementen.

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 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 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 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 zur Implementierung unserer Abfragefunktion:

  • 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 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 dem vorhandenen CDP-Zugriff 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 wird stattdessen der vollständige Baum für die Barrierefreiheit über CDP abgerufen und in Puppeteer durchlaufen. 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. Auf diese Weise kann die Abfrage 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. 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.

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. 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 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 jedoch nur zwischen den CDP-Aufrufen, also bei jeder 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, führt es zu einer zusätzlichen Arbeitsspeichernutzung. Bei Puppeteer-Scripts, die z. B. Trace-Dateien aufzeichnen, kann 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 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. Insgesamt wurden 2.253 Abfragen ausgewählt, sodass nur ein Bruchteil der Abfrageauswahl durch die Prototypen erfolgte.

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. 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.

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

Zusammenfassung

Mit dem vorhandenen CDP-Endpunkt haben wir nun 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, 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. Diese Vorabversionen bieten Zugriff auf die neuesten DevTools-Funktionen, ermöglichen den Test moderner Webplattform-APIs 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.