Puppetaria: script di Puppeteer orientati all'accessibilità

Johan Bay
Johan Bay

Puppeteer e il suo approccio ai selettori

Puppeteer è una libreria di automazione del browser per Node: ti consente di controllare un browser utilizzando un'API JavaScript semplice e moderna.

L'attività più importante del browser è, ovviamente, la navigazione nelle pagine web. Automatizzare questa attività equivale essenzialmente ad automatizzare le interazioni con la pagina web.

In Puppeteer, questo viene ottenuto eseguendo query sugli elementi DOM utilizzando selettori basati su stringhe ed eseguendo azioni come fare clic o digitare testo sugli elementi. Ad esempio, uno script che apre developer.google.com, trova la casella di ricerca e cerca puppetaria potrebbe avere il seguente aspetto:

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

La modalità di identificazione degli elementi utilizzando i selettori di query è quindi una parte fondamentale dell'esperienza Puppeteer. Finora, i selettori in Puppeteer erano limitati ai selettori CSS e XPath che, sebbene molto potenti a livello espressivo, possono avere svantaggi per le interazioni persistenti con il browser negli script.

Selettori sintattici e semantici

I selettori CSS hanno una natura sintattica: sono strettamente legati ai meccanismi interni della rappresentazione testuale dell'albero DOM, nel senso che fanno riferimento a ID e nomi di classi del DOM. Pertanto, forniscono uno strumento integrale per gli sviluppatori web per modificare o aggiungere stili a un elemento in una pagina, ma in questo contesto lo sviluppatore ha il controllo completo sulla pagina e sulla relativa struttura DOM.

D'altra parte, uno script Puppeteer è un osservatore esterno di una pagina, quindi quando i selettori CSS vengono utilizzati in questo contesto, vengono introdotte ipotesi nascoste su come viene implementata la pagina su cui lo script Puppeteer non ha alcun controllo.

Il risultato è che questi script possono essere fragili e suscettibili di modifiche al codice sorgente. Supponiamo, ad esempio, che vengano utilizzati script Puppeteer per i test automatici di un'applicazione web contenente il nodo <button>Submit</button> come terzo elemento secondario dell'elemento body. Uno snippet di uno scenario di test potrebbe avere il seguente aspetto:

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

Qui utilizziamo il selettore 'body:nth-child(3)' per trovare il pulsante di invio, ma questo è strettamente legato esattamente a questa versione della pagina web. Se un elemento viene aggiunto in un secondo momento sopra il pulsante, questo selettore non funziona più.

Non è una novità per gli autori di test: gli utenti di Puppeteer tentano già di scegliere selettori resistenti a queste modifiche. Con Puppetaria, offriamo agli utenti un nuovo strumento in questa missione.

Puppeteer ora include un handler delle query alternativo basato su query sull'albero di accessibilità anziché sui selettori CSS. La filosofia di fondo è che se l'elemento concreto che vogliamo selezionare non è cambiato, nemmeno il nodo di accessibilità corrispondente dovrebbe essere cambiato.

Definiamo questi selettori "selettori ARIA" e supportiamo le query per il nome e il ruolo accessibili calcolati dell'albero dell'accessibilità. Rispetto ai selettori CSS, queste proprietà sono di natura semantica. Non sono legati alle proprietà sintattiche del DOM, ma descrivono invece il modo in cui la pagina viene osservata tramite tecnologie per la disabilità come gli screen reader.

Nell'esempio di script per il test riportato sopra, potremmo utilizzare il selettore aria/Submit[role="button"] per selezionare il pulsante desiderato, dove Submit si riferisce al nome accessibile dell'elemento:

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

Ora, se in un secondo momento decidiamo di modificare il contenuto testuale del pulsante da Submit a Done, il test non andrà a buon fine, ma in questo caso ciò sarebbe auspicabile: cambiando il nome del pulsante cambieremo i contenuti della pagina, anziché la sua presentazione visiva o il modo in cui è strutturato nel DOM. I nostri test dovrebbero avvisarci di queste modifiche per garantire che siano intenzionali.

Tornando all'esempio più grande con la barra di ricerca, potremmo sfruttare il nuovo gestore aria e sostituire

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

con

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

per trovare la barra di ricerca.

In generale, riteniamo che l'utilizzo di questi selettori ARIA possa offrire agli utenti di Puppeteer i seguenti vantaggi:

  • Rendi i selettori negli script di test più resilienti alle modifiche al codice sorgente.
  • Rendi più leggibili gli script di test (i nomi accessibili sono descrittori semantici).
  • Fornisci le best practice per l'assegnazione delle proprietà di accessibilità agli elementi.

Il resto di questo articolo illustra i dettagli su come abbiamo implementato il progetto Puppetaria.

Il processo di progettazione

Sfondo

Come spiegato sopra, vogliamo consentire l'esecuzione di query sugli elementi in base al nome e al ruolo accessibili. Si tratta di proprietà dell'albero di accessibilità, un doppio della normale struttura ad albero DOM, che viene utilizzata da dispositivi come gli screen reader per mostrare le pagine web.

Dall'esame della specifica per il calcolo del nome accessibile, è chiaro che calcolare il nome di un elemento non è un'operazione banale, quindi fin dall'inizio abbiamo deciso di riutilizzare l'infrastruttura esistente di Chromium per questo scopo.

Qual è l'approccio da noi adottato per implementarlo

Anche se ci limitiamo a utilizzare l'albero di accessibilità di Chromium, esistono diversi modi per implementare le query ARIA in Puppeteer. Per capire il perché, vediamo prima come Puppeteer controlla il browser.

Il browser espone un'interfaccia di debug tramite un protocollo chiamato Chrome DevTools Protocol (CDP). In questo modo vengono esposte funzionalità come "Ricarica la pagina" o "Esegui questo codice JavaScript nella pagina e restituisci il risultato" tramite un'interfaccia indipendente dal linguaggio.

Sia il front-end di DevTools sia Puppeteer utilizzano CDP per comunicare con il browser. Per implementare i comandi CDP, è presente l'infrastruttura di DevTools in tutti i componenti di Chrome: nel browser, nel renderer e così via. Il CDP si occupa di instradare i comandi al posto giusto.

Le azioni di Puppeteer come query, clic e valutazione delle espressioni vengono eseguite sfruttando i comandi CDP come Runtime.evaluate che valutano JavaScript direttamente nel contesto della pagina e restituiscono il risultato. Altre azioni di Puppeteer, come l'emulazione della deficienza della visione dei colori, l'acquisizione di screenshot o la cattura di tracce, utilizzano CDP per comunicare direttamente con il processo di rendering di Blink.

CDP

Abbiamo già due percorsi per implementare la funzionalità di query:

  • Scrivi la nostra logica di query in JavaScript e inseriscila nella pagina utilizzando Runtime.evaluate oppure
  • Utilizza un endpoint CDP che può accedere e eseguire query sull'albero di accessibilità direttamente nel processo Blink.

Abbiamo implementato tre prototipi:

  • Percorso del DOM JS: basato sull'inserimento di JavaScript nella pagina
  • Percorso dell'albero AXTree di Puppeteer: basato sull'utilizzo dell'accesso CDP esistente all'albero di accessibilità
  • Percorso DOM del CDP: utilizzo di un nuovo endpoint CDP appositamente progettato per eseguire query sull'albero di accessibilità

Attraversamento DOM JS

Questo prototipo esegue un attraversamento completo del DOM e utilizza element.computedName e element.computedRole, individuati in base al flag di lancio ComputedAccessibilityInfo, per recuperare il nome e il ruolo di ogni elemento durante l'attraversamento.

Esplorazione di AXTree di Puppeteer

Qui, invece, recuperiamo l'albero dell'accessibilità completo tramite CDP e lo esploriamo in Puppeteer. I nodi di accessibilità risultanti vengono poi mappati ai nodi DOM.

Attraversamento DOM CDP

Per questo prototipo, abbiamo implementato un nuovo endpoint CDP specifico per eseguire query sull'albero dell'accessibilità. In questo modo, la query può essere eseguita sul back-end tramite un'implementazione C++ anziché nel contesto della pagina tramite JavaScript.

Benchmark del test delle unità

La figura seguente confronta il tempo di esecuzione totale della query di quattro elementi 1000 volte per i tre prototipi. Il benchmark è stato eseguito in tre configurazioni diverse variando le dimensioni della pagina e l'attivazione o meno della memorizzazione nella cache degli elementi di accessibilità.

Benchmark: tempo di esecuzione totale della query di quattro elementi 1000 volte

È abbastanza chiaro che esiste un notevole divario di prestazioni tra il meccanismo di query basato su CDP e gli altri due implementati esclusivamente in Puppeteer e la differenza relativa sembra aumentare notevolmente con le dimensioni della pagina. È piuttosto interessante notare che il prototipo di attraversamento del DOM JS risponde così bene all'attivazione della memorizzazione nella cache dell'accessibilità. Se la memorizzazione nella cache è disattivata, l'albero di accessibilità viene calcolato su richiesta e viene ignorato dopo ogni interazione se il dominio è disattivato. Se attivi il dominio, Chromium memorizza nella cache la struttura ad albero calcolata.

Per l'esplorazione del DOM JS, chiediamo il nome e il ruolo accessibili per ogni elemento durante l'esplorazione, quindi se la memorizzazione nella cache è disattivata, Chromium calcola e ignora l'albero di accessibilità per ogni elemento visitato. Per gli approcci basati su CDP, invece, l'albero viene ignorato solo tra ogni chiamata al CDP, ovvero per ogni query. Questi approcci beneficiano anche dell'attivazione della memorizzazione nella cache, poiché l'albero di accessibilità viene mantenuto nelle chiamate CDP, ma l'aumento delle prestazioni è quindi relativamente minore.

Anche se in questo caso è preferibile abilitare la memorizzazione nella cache, comporta un costo aggiuntivo per la memoria utilizzata. Questo potrebbe essere problematico per gli script Puppeteer che, ad esempio, registrano i file di traccia. Di conseguenza, abbiamo deciso di non attivare la memorizzazione nella cache dell'albero di accessibilità per impostazione predefinita. Gli utenti possono attivare la memorizzazione nella cache attivando il dominio Accessibilità di CDP.

Benchmark della suite di test DevTools

Il benchmark precedente ha dimostrato che l'implementazione del nostro meccanismo di query a livello di livello CDP offre un aumento delle prestazioni in uno scenario di test di unità clinica.

Per verificare se la differenza è sufficientemente pronunciata da essere percepita in uno scenario più realistico di esecuzione di una suite di test completa, abbiamo apportato una patch alla suite di test end-to-end di DevTools per utilizzare i prototipi basati su JavaScript e CDP e abbiamo confrontato i runtime. In questo benchmark, abbiamo modificato un totale di 43 selettori da [aria-label=…] a un gestore delle query personalizzate aria/…, che abbiamo poi implementato utilizzando ciascuno dei prototipi.

Alcuni dei selettori vengono utilizzati più volte negli script di test, pertanto il numero effettivo di esecuzioni dell'handler delle query aria è stato pari a 113 per ogni esecuzione della suite. Il numero totale di selezioni di query è stato 2253, quindi solo una parte delle selezioni di query è avvenuta tramite i prototipi.

Benchmark: suite di test e2e

Come mostrato nella figura sopra, esiste una differenza percepibile nel tempo di esecuzione totale. I dati sono troppo rumorosi per trarre conclusioni specifiche, ma è chiaro che il divario di rendimento tra i due prototipi si manifesta anche in questo scenario.

Un nuovo endpoint CDP

Alla luce dei benchmark riportati sopra e poiché l'approccio basato sui flag di lancio non era auspicabile in generale, abbiamo deciso di procedere con l'implementazione di un nuovo comando CDP per eseguire query sull'albero di accessibilità. Ora dovevamo capire l'interfaccia di questo nuovo endpoint.

Per il nostro caso d'uso in Puppeteer, abbiamo bisogno che l'endpoint prenda il cosiddetto RemoteObjectIds come argomento e, per consentirci di trovare in seguito gli elementi DOM corrispondenti, dovrebbe restituire un elenco di oggetti che contiene backendNodeIds per gli elementi DOM.

Come mostrato nel grafico seguente, abbiamo provato diversi approcci per soddisfare questa interfaccia. Da ciò è emerso che le dimensioni degli oggetti restituiti, ovvero se abbiamo restituito o meno nodi di accessibilità completi o solo backendNodeIds, non hanno fatto alcuna differenza. D'altra parte, abbiamo riscontrato che l'utilizzo di NextInPreOrderIncludingIgnored esistente non era una scelta ottimale per implementare la logica di attraversamento qui, poiché ha prodotto un rallentamento significativo.

Benchmark: confronto tra prototipi di attraversamento AXTree basati su CDP

Riepilogo

Ora, con l'endpoint CDP implementato, abbiamo implementato il gestore delle query sul lato di Puppeteer. La parte più impegnativa del lavoro è stata ristrutturare il codice di gestione delle query per consentire la risoluzione delle query direttamente tramite CDP anziché tramite JavaScript valutato nel contesto della pagina.

Passaggi successivi

Il nuovo gestore aria è stato fornito con Puppeteer 5.4.0 come gestore delle query integrato. Non vediamo l'ora di scoprire come gli utenti la adotteranno nei loro script di test e non vediamo l'ora di ascoltare le tue idee su come renderla ancora più utile.

Scaricare i canali di anteprima

Valuta la possibilità di utilizzare Chrome Canary, Dev o Beta come browser di sviluppo predefinito. Questi canali di anteprima ti consentono di accedere alle funzionalità più recenti di DevTools, di testare API di piattaforme web all'avanguardia e di trovare i problemi sul tuo sito prima che lo facciano gli utenti.

Contatta il team di Chrome DevTools

Utilizza le seguenti opzioni per discutere delle nuove funzionalità, degli aggiornamenti o di qualsiasi altra cosa relativa a DevTools.