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. L'automazione di questa attività equivale essenzialmente all'automazione delle 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. Fino ad ora, i selettori in Puppeteer erano limitati ai selettori CSS e XPath che, sebbene molto potenti dal punto di vista espressivo, possono avere degli svantaggi per la persistenza delle interazioni del browser negli script.

Selettori sintattici e semantici

I selettori CSS sono di natura sintattica; sono strettamente legati al funzionamento interno 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 un test case 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 in un secondo momento viene aggiunto un elemento 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 per questa ricerca.

Puppeteer ora è dotato di 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.

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

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

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

Ora, se in un secondo momento decidiamo di cambiare il contenuto del testo del pulsante da Submit a Done, il test non andrà a buon fine, ma in questo caso è auspicabile. Modificando il nome del pulsante, modifichiamo i contenuti della pagina, al contrario della sua presentazione visiva o della sua struttura 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.

Più 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 del 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.

Il nostro approccio all'implementazione

Anche se ci limitiamo a utilizzare l'albero di accessibilità di Chromium, esistono diversi modi per implementare le query ARIA in Puppeteer. Per capire 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:

  • Scrivere la logica di query in JavaScript e inserirla 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à

Esplorazione del DOM di JS

Questo prototipo esegue un'esplorazione completa del DOM e utilizza element.computedName e element.computedRole, basati sul flag di lancio ComputedAccessibilityInfo, per recuperare il nome e il ruolo di ogni elemento durante l'esplorazione.

Attraversamento 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 del DOM di CDP

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

Benchmark dei test delle unità

La figura seguente confronta il tempo di esecuzione totale della query su 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. È un po' interessante vedere 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.

Sebbene l'attivazione della memorizzazione nella cache sia auspicabile in questo caso, comporta un costo aggiuntivo per l'utilizzo della memoria. Per gli script Puppeteer che, ad esempio, registrano file di traccia, questo potrebbe essere problematico. 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, è necessario che l'endpoint accetti come argomento il cosiddetto RemoteObjectIds e, per consentirci di trovare in seguito gli elementi DOM corrispondenti, deve restituire un elenco di oggetti contenenti il 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 dei prototipi di attraversamento di 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 i tuoi utenti.

Contatta il team di Chrome DevTools

Utilizza le seguenti opzioni per discutere di nuove funzionalità, aggiornamenti o qualsiasi altro argomento relativo a DevTools.