Puppetaria: poppenspelerscripts waarbij toegankelijkheid voorop staat

Johan Baai
Johan Bay

Poppenspeler en zijn benadering van selectors

Puppeteer is een browserautomatiseringsbibliotheek voor Node: hiermee kunt u een browser besturen met behulp van een eenvoudige en moderne JavaScript-API.

De meest prominente browsertaak is natuurlijk het bladeren door webpagina's. Het automatiseren van deze taak komt in wezen neer op het automatiseren van interacties met de webpagina.

In Puppeteer wordt dit bereikt door te zoeken naar DOM-elementen met behulp van op tekenreeksen gebaseerde selectors en door acties uit te voeren zoals klikken of tekst typen op de elementen. Een script dat bijvoorbeeld open developer.google.com opent, het zoekvak vindt en naar puppetaria zoekt, kan er als volgt uitzien:

(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');
 })();

De manier waarop elementen worden geïdentificeerd met behulp van query-selectors is daarom een ​​bepalend onderdeel van de Puppeteer-ervaring. Tot nu toe waren selectors in Puppeteer beperkt tot CSS- en XPath-selectors die, hoewel ze expressief zeer krachtig zijn, nadelen kunnen hebben voor het volhouden van browserinteracties in scripts.

Syntactische versus semantische selectors

CSS-selectors zijn syntactisch van aard; ze zijn nauw verbonden met de innerlijke werking van de tekstuele representatie van de DOM-boom, in de zin dat ze verwijzen naar ID's en klassenamen uit de DOM. Als zodanig bieden ze een integraal hulpmiddel voor webontwikkelaars voor het wijzigen of toevoegen van stijlen aan een element op een pagina, maar in die context heeft de ontwikkelaar volledige controle over de pagina en de DOM-boomstructuur.

Aan de andere kant is een Puppeteer-script een externe waarnemer van een pagina, dus wanneer CSS-selectors in deze context worden gebruikt, introduceert het verborgen aannames over hoe de pagina wordt geïmplementeerd, waarover het Puppeteer-script geen controle heeft.

Het effect is dat dergelijke scripts broos kunnen zijn en vatbaar voor wijzigingen in de broncode. Stel bijvoorbeeld dat men Puppeteer-scripts gebruikt voor het geautomatiseerd testen van een webapplicatie die het knooppunt <button>Submit</button> als derde kind van het body element bevat. Eén fragment uit een testcase kan er als volgt uitzien:

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

Hier gebruiken we de selector 'body:nth-child(3)' om de verzendknop te vinden, maar deze is nauw verbonden met precies deze versie van de webpagina. Als er later een element boven de knop wordt toegevoegd, werkt deze selector niet meer!

Dit is geen nieuws voor testschrijvers: poppenspelergebruikers proberen al selectors te kiezen die robuust zijn tegen dergelijke veranderingen. Met Puppetaria geven we gebruikers een nieuw hulpmiddel bij deze zoektocht.

Puppeteer wordt nu geleverd met een alternatieve query-handler die gebaseerd is op het bevragen van de toegankelijkheidsboom in plaats van te vertrouwen op CSS-selectors . De onderliggende filosofie hier is dat als het concrete element dat we willen selecteren niet is veranderd, het bijbehorende toegankelijkheidsknooppunt ook niet mag zijn veranderd.

We noemen dergelijke selectors " ARIA- selectors" en ondersteunen het opvragen van de berekende toegankelijke naam en rol van de toegankelijkheidsboom. Vergeleken met de CSS-selectors zijn deze eigenschappen semantisch van aard. Ze zijn niet gebonden aan syntactische eigenschappen van de DOM, maar beschrijven in plaats daarvan hoe de pagina wordt waargenomen via ondersteunende technologieën zoals schermlezers.

In het bovenstaande testscriptvoorbeeld kunnen we in plaats daarvan de selector aria/Submit[role="button"] gebruiken om de gewenste knop te selecteren, waarbij Submit verwijst naar de toegankelijke naam van het element:

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

Als we nu later besluiten om de tekstinhoud van onze knop te wijzigen van Submit naar Done , zal de test opnieuw mislukken, maar in dit geval is dat wenselijk; door de naam van de knop te wijzigen, veranderen we de inhoud van de pagina, in tegenstelling tot de visuele presentatie of hoe deze in de DOM is gestructureerd. Onze tests moeten ons waarschuwen voor dergelijke veranderingen om ervoor te zorgen dat dergelijke veranderingen opzettelijk zijn.

Als we teruggaan naar het grotere voorbeeld met de zoekbalk, kunnen we de nieuwe aria handler gebruiken en vervangen

const search = await page.$('devsite-search > form > div.devsite-search-container');

met

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

om de zoekbalk te vinden!

Meer in het algemeen zijn we van mening dat het gebruik van dergelijke ARIA-selectors de volgende voordelen kan bieden aan Puppeteer-gebruikers:

  • Maak selectors in testscripts beter bestand tegen wijzigingen in de broncode.
  • Maak testscripts leesbaarder (toegankelijke namen zijn semantische descriptoren).
  • Motiveer goede praktijken voor het toekennen van toegankelijkheidseigenschappen aan elementen.

In de rest van dit artikel gaan we dieper in op de details van hoe we het Puppetaria-project hebben geïmplementeerd.

Het ontwerpproces

Achtergrond

Zoals hierboven gemotiveerd, willen we het opvragen van elementen mogelijk maken op basis van hun toegankelijke naam en rol. Dit zijn eigenschappen van de toegankelijkheidsboom , een dubbele aan de gebruikelijke DOM-boom, die door apparaten zoals schermlezers wordt gebruikt om webpagina's weer te geven.

Als we kijken naar de specificatie voor het berekenen van de toegankelijke naam , wordt het duidelijk dat het berekenen van de naam voor een element een niet-triviale taak is, dus vanaf het begin hebben we besloten dat we hiervoor de bestaande infrastructuur van Chromium wilden hergebruiken.

Hoe we de implementatie ervan benaderden

Zelfs als we ons beperken tot het gebruik van de toegankelijkheidsboom van Chromium, zijn er nogal wat manieren waarop we ARIA-query's in Puppeteer kunnen implementeren. Om te zien waarom, laten we eerst kijken hoe Puppeteer de browser bestuurt.

De browser stelt een foutopsporingsinterface beschikbaar via een protocol dat het Chrome DevTools Protocol (CDP) wordt genoemd. Hierdoor wordt functionaliteit zichtbaar zoals "de pagina opnieuw laden" of "dit stukje JavaScript op de pagina uitvoeren en het resultaat teruggeven" via een taalonafhankelijke interface.

Zowel de front-end van DevTools als Puppeteer gebruiken CDP om met de browser te praten. Om CDP-opdrachten te implementeren, is er een DevTools-infrastructuur in alle componenten van Chrome: in de browser, in de renderer, enzovoort. CDP zorgt ervoor dat de commando’s naar de juiste plek worden gerouteerd.

Poppenspeleracties zoals het opvragen, klikken en evalueren van expressies worden uitgevoerd door gebruik te maken van CDP-opdrachten zoals Runtime.evaluate , die JavaScript rechtstreeks in de paginacontext evalueert en het resultaat teruggeeft. Andere poppenspeleracties, zoals het emuleren van een tekort aan kleurenzicht, het maken van schermafbeeldingen of het vastleggen van sporen, gebruiken CDP om rechtstreeks te communiceren met het Blink-weergaveproces.

CDP

Dit laat ons al twee paden over voor het implementeren van onze queryfunctionaliteit; we kunnen:

  • Schrijf onze querylogica in JavaScript en laat die in de pagina injecteren met behulp van Runtime.evaluate , of
  • Gebruik een CDP-eindpunt dat rechtstreeks toegang heeft tot de toegankelijkheidsboom en deze kan opvragen in het Blink-proces.

We hebben 3 prototypes geïmplementeerd:

  • JS DOM-traversal - gebaseerd op het injecteren van JavaScript in de pagina
  • Poppenspeler AXTree traversal - gebaseerd op het gebruik van de bestaande CDP-toegang tot de toegankelijkheidsboom
  • CDP DOM-traversal - met behulp van een nieuw CDP-eindpunt dat speciaal is gebouwd voor het bevragen van de toegankelijkheidsboom

JS DOM-traversal

Dit prototype voert een volledige doorloop van de DOM uit en gebruikt element.computedName en element.computedRole , gepoort op de ComputedAccessibilityInfo startvlag , om de naam en rol voor elk element op te halen tijdens de doorgang.

Poppenspeler AXTreen doorkruist

Hier halen we in plaats daarvan de volledige toegankelijkheidsboom op via CDP en doorkruisen deze in Puppeteer. De resulterende toegankelijkheidsknooppunten worden vervolgens toegewezen aan DOM-knooppunten.

CDP DOM-doorgang

Voor dit prototype hebben we een nieuw CDP-eindpunt geïmplementeerd, specifiek voor het bevragen van de toegankelijkheidsboom. Op deze manier kan de bevraging plaatsvinden op de back-end via een C++-implementatie in plaats van in de paginacontext via JavaScript.

Eenheidstestbenchmark

De volgende afbeelding vergelijkt de totale looptijd van het 1000 keer opvragen van vier elementen voor de drie prototypes. De benchmark werd uitgevoerd in 3 verschillende configuraties, variërend van de paginagrootte en of het cachen van toegankelijkheidselementen al dan niet was ingeschakeld.

Benchmark: totale looptijd van het 1000 keer opvragen van vier elementen

Het is vrij duidelijk dat er een aanzienlijke prestatiekloof bestaat tussen het door CDP ondersteunde zoekmechanisme en de twee andere die uitsluitend in Puppeteer zijn geïmplementeerd, en het relatieve verschil lijkt dramatisch toe te nemen met de paginagrootte. Het is enigszins interessant om te zien dat het JS DOM traversal-prototype zo goed reageert op het mogelijk maken van toegankelijkheidscaching. Als caching is uitgeschakeld, wordt de toegankelijkheidsboom op aanvraag berekend en wordt de boom na elke interactie verwijderd als het domein is uitgeschakeld. Als u het domein inschakelt, wordt in plaats daarvan de berekende boom in Chromium in de cache opgeslagen.

Voor de JS DOM-traversal vragen we om de toegankelijke naam en rol voor elk element tijdens de traversal, dus als caching is uitgeschakeld, berekent Chromium de toegankelijkheidsboom voor elk element dat we bezoeken en verwijdert deze. Bij de op CDP gebaseerde benaderingen wordt de boom daarentegen alleen verwijderd tussen elke oproep aan CDP, dat wil zeggen voor elke vraag. Deze benaderingen profiteren ook van het inschakelen van caching, omdat de toegankelijkheidsboom dan behouden blijft bij alle CDP-oproepen, maar de prestatieverbetering is daarom relatief kleiner.

Hoewel het inschakelen van caching hier wenselijk lijkt, brengt dit wel kosten met zich mee voor extra geheugengebruik. Voor Puppeteer-scripts die bijvoorbeeld traceerbestanden registreren , kan dit problematisch zijn. We hebben daarom besloten om caching van toegankelijkheidsbomen niet standaard in te schakelen. Gebruikers kunnen caching zelf inschakelen door het CDP Accessibility-domein in te schakelen.

Benchmark van DevTools-testsuite

De vorige benchmark liet zien dat de implementatie van ons bevragingsmechanisme op de CDP-laag een prestatieverbetering oplevert in een klinisch unit-testscenario.

Om te zien of het verschil duidelijk genoeg is om het merkbaar te maken in een realistischer scenario van het uitvoeren van een volledige testsuite, hebben we de end-to-end testsuite van DevTools gepatcht om gebruik te maken van de op JavaScript en CDP gebaseerde prototypes en de runtimes vergeleken. . In deze benchmark hebben we in totaal 43 selectors gewijzigd van [aria-label=…] naar een aangepaste queryhandler aria/… , die we vervolgens hebben geïmplementeerd met behulp van elk van de prototypes.

Sommige selectors worden meerdere keren gebruikt in testscripts, dus het werkelijke aantal uitvoeringen van de aria queryhandler was 113 per run van de suite. Het totale aantal zoekopdrachtselecties was 2253, dus slechts een fractie van de zoekopdrachtselecties gebeurde via de prototypes.

Benchmark: e2e-testsuite

Zoals u in de bovenstaande afbeelding kunt zien, is er een waarneembaar verschil in de totale looptijd. De gegevens zijn te luidruchtig om iets specifieks te concluderen, maar het is duidelijk dat de prestatiekloof tussen de twee prototypes ook in dit scenario zichtbaar is.

Een nieuw CDP-eindpunt

In het licht van de bovenstaande benchmarks, en omdat de op de lanceringsvlag gebaseerde aanpak in het algemeen onwenselijk was, hebben we besloten om verder te gaan met het implementeren van een nieuw CDP-commando voor het bevragen van de toegankelijkheidsboom. Nu moesten we de interface van dit nieuwe eindpunt uitzoeken.

Voor ons gebruik in Puppeteer hebben we het eindpunt nodig om zogenaamde RemoteObjectIds als argument te nemen en om ons in staat te stellen de overeenkomstige DOM-elementen achteraf te vinden, moet het een lijst met objecten retourneren die de backendNodeIds voor de DOM-elementen bevat.

Zoals te zien is in het onderstaande diagram, hebben we een flink aantal benaderingen geprobeerd die aan deze interface voldeden. Hieruit hebben we ontdekt dat de grootte van de geretourneerde objecten, dwz of we wel of niet volledige toegankelijkheidsknooppunten of alleen de backendNodeIds hebben geretourneerd, geen waarneembaar verschil maakte. Aan de andere kant ontdekten we dat het gebruik van de bestaande NextInPreOrderIncludingIgnored een slechte keuze was voor het implementeren van de traversal-logica hier, omdat dat een merkbare vertraging opleverde.

Benchmark: vergelijking van op CDP gebaseerde AXTree traversal-prototypes

Het allemaal inpakken

Nu het CDP-eindpunt aanwezig is, hebben we de queryhandler aan de Puppeteer-kant geïmplementeerd. Het grootste deel van het werk hier was het herstructureren van de code voor het verwerken van query's, zodat query's rechtstreeks via CDP konden worden opgelost in plaats van query's via JavaScript, geëvalueerd in de paginacontext.

Wat is het volgende?

De nieuwe aria handler wordt geleverd met Puppeteer v5.4.0 als ingebouwde query-handler. We kijken ernaar uit om te zien hoe gebruikers het in hun testscripts opnemen, en we kunnen niet wachten om uw ideeën te horen over hoe we dit nog nuttiger kunnen maken!

Download de voorbeeldkanalen

Overweeg om Chrome Canary , Dev of Beta te gebruiken als uw standaard ontwikkelingsbrowser. Deze preview-kanalen geven u toegang tot de nieuwste DevTools-functies, testen geavanceerde webplatform-API's en ontdekken problemen op uw site voordat uw gebruikers dat doen!

Neem contact op met het Chrome DevTools-team

Gebruik de volgende opties om de nieuwe functies en wijzigingen in het bericht te bespreken, of iets anders gerelateerd aan DevTools.

  • Stuur ons een suggestie of feedback via crbug.com .
  • Rapporteer een DevTools-probleem met behulp van de opties MeerMeer > Help > Rapporteer een DevTools-probleem in DevTools.
  • Tweet op @ChromeDevTools .
  • Laat reacties achter op onze Wat is er nieuw in DevTools YouTube-video's of DevTools Tips YouTube-video's .