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 delle pagine web. Automatizzare questa attività equivale essenzialmente ad automatizzare le interazioni con la pagina web.

In Puppeteer, questo si ottiene 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 le ricerche di puppetaria potrebbero 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');
 })();

Il modo in cui gli elementi vengono identificati utilizzando i selettori di query è quindi un aspetto determinante dell'esperienza Puppeteer. Finora, i selettori in Puppeteer erano limitati ai selettori CSS e XPath che, anche se espressivamente molto potenti, 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. In quanto tali, 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 pieno controllo sulla pagina e sulla sua struttura DOM.

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

Il risultato è che tali script possono essere fragili e suscettibili di modifiche al codice sorgente. Supponiamo, ad esempio, che utilizzi gli 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();

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

Questa non è una novità per testare gli autori: gli utenti Puppeteer tentano già di scegliere selettori solidi per queste modifiche. Con Puppetaria, offriamo agli utenti un nuovo strumento in questa missione.

Puppeteer ora viene fornito con un gestore di query alternativo basato sulle query all'albero dell'accessibilità anziché su selettori CSS. Secondo la filosofia di base, se l'elemento concreto che vogliamo selezionare non è cambiato, neanche 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 avvisarti in merito a tali modifiche per garantire che siano intenzionali.

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

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

grazie a

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 i seguenti vantaggi agli utenti di Puppeteer:

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

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

Il processo di progettazione

Contesto

Come spiegato in precedenza, vogliamo consentire l'esecuzione di query sugli elementi in base al loro nome e ruolo accessibili. Si tratta di proprietà dell'albero dell'accessibilità, un doppio rispetto al solito albero DOM, utilizzato da dispositivi come gli screen reader per visualizzare le pagine web.

Osservando le specifiche per il calcolo del nome accessibile, è chiaro che calcolare il nome di un elemento è un'attività non 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 limitandoci all'utilizzo dell'albero dell'accessibilità di Chromium, ci sono diversi modi in cui potremmo 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 protocollo DevTools di Chrome (CDP). In questo modo, vengono esposte funzionalità quali "ricarica la pagina" o "esegui questa porzione di JavaScript nella pagina e restituisci il risultato" tramite un'interfaccia indipendente dalla lingua.

Sia il front-end di DevTools che Puppeteer utilizzano CDP per comunicare con il browser. Per implementare i comandi CDP, è presente l'infrastruttura DevTools all'interno di tutti i componenti di Chrome: nel browser, nel renderer e così via. CDP si occupa di indirizzare i comandi nella posizione corretta.

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

CDP

Questo ci lascia già due percorsi per l'implementazione della nostra funzionalità di query: possiamo:

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

Abbiamo implementato 3 prototipi:

  • Traversal DOM JS: basato sull'inserimento di JavaScript nella pagina
  • Traversal Puppeteer AXTree: basato sull'uso dell'accesso CDP esistente all'albero dell'accessibilità.
  • Attraversamento DOM CDP: utilizzando un nuovo endpoint CDP creato appositamente per eseguire query sull'albero dell'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.

Attraversamento AXTree dei burattini

Qui recuperiamo l'albero dell'accessibilità completa tramite CDP e lo attraversiamo in Puppeteer. I nodi di accessibilità risultanti vengono quindi 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, le query possono avvenire sul back-end attraverso un'implementazione C++ invece che nel contesto della pagina tramite JavaScript.

Benchmark del test delle unità

La figura seguente confronta il tempo di esecuzione totale delle query su quattro elementi 1000 volte per i 3 prototipi. Il benchmark è stato eseguito in tre diverse configurazioni, che variano in base alle dimensioni della pagina e se è stata attivata o meno la memorizzazione nella cache degli elementi di accessibilità.

Benchmark: tempo di esecuzione totale delle query su quattro elementi 1000 volte

È abbastanza chiaro che c'è un notevole divario di prestazioni tra il meccanismo di query supportato da CDP e gli altri due implementati esclusivamente in Puppeteer e la differenza relativa sembra aumentare drasticamente con le dimensioni della pagina. È un po' interessante vedere che il prototipo di attraversamento JS DOM risponde così bene all'abilitazione della memorizzazione nella cache dell'accessibilità. Con la memorizzazione nella cache disattivata, l'albero dell'accessibilità viene calcolato on demand e lo scarta dopo ogni interazione se il dominio è disabilitato. Se attivi il dominio, Chromium memorizza nella cache l'albero calcolato.

Per il trasferimento DOM JS chiediamo il nome e il ruolo accessibili per ogni elemento durante l'attraversamento, quindi se la memorizzazione nella cache è disattivata, Chromium calcola e scarta l'albero dell'accessibilità per ogni elemento visitato. Per gli approcci basati su CDP, d'altra parte, l'albero viene scartato solo tra ogni chiamata a CDP, ovvero per ogni query. Questi approcci traggono anche vantaggio dall'abilitazione della memorizzazione nella cache, poiché l'albero dell'accessibilità viene quindi mantenuto in tutte le chiamate CDP, ma l'incremento 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. Abbiamo quindi deciso di non abilitare la memorizzazione nella cache ad albero dell'accessibilità per impostazione predefinita. Gli utenti possono attivare autonomamente la memorizzazione nella cache abilitando il dominio Accessibilità di CDP.

Benchmark della suite di test DevTools

Il benchmark precedente ha mostrato che l'implementazione del nostro meccanismo di query a livello CDP aumenta le prestazioni in uno scenario di test delle unità clinici.

Per vedere se la differenza è pronunciata abbastanza da essere evidente in uno scenario più realistico dell'esecuzione di una suite di test completa, abbiamo applicato le patch alla suite di test end-to-end DevTools in modo da utilizzare i prototipi basati su JavaScript e CDP, nonché confrontato i runtime. In questo benchmark, abbiamo modificato un totale di 43 selettori da [aria-label=…] a un gestore di query personalizzato aria/…, che abbiamo poi implementato utilizzando ciascuno dei prototipi.

Alcuni selettori vengono utilizzati più volte negli script di test, quindi il numero effettivo di esecuzioni del gestore di query aria è stato di 113 per esecuzione della suite. Il numero totale di selezioni di query era 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 evidente nel tempo di esecuzione totale. I dati sono troppo rumorosi per concludere qualcosa di specifico, ma è chiaro che il divario di prestazioni tra i due prototipi si manifesta anche in questo scenario.

Un nuovo endpoint CDP

Alla luce dei benchmark di cui sopra e poiché l'approccio basato su flag di lancio era in generale indesiderato, abbiamo deciso di procedere con l'implementazione di un nuovo comando CDP per eseguire query sull'albero dell'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 illustrato nel grafico riportato di seguito, abbiamo provato diversi approcci per soddisfare questa interfaccia. Da questo, abbiamo scoperto che la dimensione degli oggetti restituiti, ad esempio se abbiamo restituito o meno nodi di accessibilità completa o solo backendNodeIds, non faceva alcuna differenza distinguibile. D'altra parte, abbiamo riscontrato che l'utilizzo della NextInPreOrderIncludingIgnored esistente era una scelta sbagliata per implementare la logica di attraversamento in questo caso, in quanto ciò produceva un notevole rallentamento.

Benchmark: confronto tra prototipi di attraversamento AXTree basati su CDP

Conclusione

Ora, con l'endpoint CDP, abbiamo implementato il gestore di query sul lato Puppeteer. Il lavoro suscettibile di lavoro è stato quello di ristrutturare il codice di gestione delle query per consentire la risoluzione diretta delle query tramite CDP invece di eseguire query tramite JavaScript valutato nel contesto della pagina.

Passaggi successivi

Il nuovo gestore aria viene fornito con Puppeteer v5.4.0 come gestore di query integrato. Non vediamo l'ora di scoprire in che modo gli utenti la adottano nei loro script per i test e non vediamo l'ora di conoscere le vostre idee su come possiamo rendere tutto questo ancora più utile.

Scaricare i canali in anteprima

Prendi in considerazione l'utilizzo di Chrome Canary, Dev o Beta come browser di sviluppo predefinito. Questi canali di anteprima ti consentono di accedere alle ultime funzionalità di DevTools, di testare le API delle piattaforme web all'avanguardia e di individuare i problemi sul tuo sito prima che lo facciano gli utenti.

Contattare il team di Chrome DevTools

Utilizza le seguenti opzioni per discutere delle nuove funzionalità e modifiche nel post o di qualsiasi altra informazione relativa a DevTools.

  • Inviaci un suggerimento o un feedback tramite crbug.com.
  • Segnala un problema di DevTools usando Altre opzioni   Altre   > Guida > Segnala un problema di DevTools in DevTools.
  • Invia un tweet all'indirizzo @ChromeDevTools.
  • Lascia commenti sulle novità nei video di YouTube di DevTools o nei video di YouTube dei suggerimenti di DevTools.