Puppetaria: script di Puppeteer orientati all'accessibilità

Baia di Johan
Baia di Johan

Puppeteer e il suo approccio ai selettori

Puppeteer è una libreria di automazione del browser per Node che 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 si ottiene eseguendo query sugli elementi DOM utilizzando selettori basati su stringhe ed eseguendo azioni come fare clic o digitare il testo sugli elementi. Ad esempio, uno script che apre la pagina 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');
 })();

Il modo in cui gli elementi vengono identificati utilizzando i selettori di query è quindi una parte fondamentale dell'esperienza Puppeteer. Finora, i selettori in Puppeteer erano limitati a selettori CSS e XPath che, anche se molto efficaci in termini di espressione, possono presentare degli svantaggi legati alla persistenza delle interazioni del browser negli script.

Selettori sintattici e semantici

I selettori CSS sono di 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 agli sviluppatori web uno strumento fondamentale per modificare o aggiungere stili a un elemento di una pagina, ma in questo contesto lo sviluppatore ha il pieno controllo della pagina e del relativo albero 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 sull'implementazione della pagina su cui lo script Puppeteer non ha alcun controllo.

Di conseguenza, questi script possono essere fragili e soggetti a modifiche al codice sorgente. Supponiamo, ad esempio, che vengano utilizzati gli script Puppeteer per i test automatici di un'applicazione web che contiene il nodo <button>Submit</button> come terzo elemento figlio 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 Invia, ma è strettamente legato 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 testare gli autori: gli utenti di Puppeteer tentano già di scegliere selettori affidabili per questi cambiamenti. Con Puppetaria, offriamo agli utenti un nuovo strumento per questa missione.

Puppeteer ora viene fornito con un gestore di query alternativo basato sulle query sull'albero dell'accessibilità anziché affidarsi ai selettori CSS. La filosofia di base qui è che se l'elemento concreto che vogliamo selezionare non è cambiato, anche il nodo di accessibilità corrispondente non dovrebbe essere cambiato.

Denomina questi selettori "ARIA" e supportiamo le query del nome accessibile calcolato e del ruolo dell'albero dell'accessibilità. Rispetto ai selettori CSS, queste proprietà sono di natura semantica. Non sono legati alle proprietà sintattiche del DOM, ma ai 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 si riferisce al nome accessibile dell'elemento:

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

Se in un secondo momento decidiamo di modificare il contenuto testuale del pulsante da Submit a Done, il test avrà ancora esito negativo, ma in questo caso è preferibile: cambiando il nome del pulsante cambieremo i contenuti della pagina, anziché la sua presentazione visiva o la sua struttura nel DOM. I nostri test dovrebbero avvisarci in caso di modifiche di questo tipo per garantire che siano intenzionali.

Tornando all'esempio più ampio con la barra di ricerca, potremmo utilizzare 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 individuare 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.
  • Rendere più leggibili gli script di test (i nomi accessibili sono descrittori semantici).
  • Motivare le buone pratiche per l'assegnazione delle proprietà di accessibilità agli elementi.

Il resto di questo articolo fornisce maggiori dettagli su 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 al loro ruolo accessibili. Si tratta di proprietà dell'albero di accessibilità, un albero DOM che viene utilizzato da dispositivi come gli screen reader per mostrare le pagine web.

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

Come abbiamo affrontato l'implementazione

Persino limitandoci a utilizzare la struttura dell'accessibilità di Chromium, ci sono diversi modi in cui potremmo implementare le query ARIA in Puppeteer. Per capire il motivo, vediamo innanzitutto in che modo Puppeteer controlla il browser.

Il browser mostra un'interfaccia di debug tramite un protocollo chiamato Chrome DevTools Protocol (CDP). Ciò espone funzionalità come "ricarica la pagina" o "esegui questa parte di JavaScript nella pagina e restituisci il risultato" tramite un'interfaccia indipendente dalla lingua.

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

Le azioni di Puppeteer, come eseguire query, fare clic e valutare espressioni, vengono eseguite utilizzando i comandi CDP come Runtime.evaluate, che valutano JavaScript direttamente nel contesto della pagina e restituisce il risultato. Altre azioni di Puppeteer, come emulare il deficit della visione cromatica, acquisire screenshot o acquisire tracce, utilizzano la tecnologia CDP per comunicare direttamente con il processo di rendering di Blink.

CDP

Questo ci lascia già due percorsi per implementare la 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:

  • JS DOM trasversale: basato sull'inserimento di JavaScript nella pagina
  • attraversamento Puppeteer AXTree: basato sull'utilizzo dell'accesso CDP esistente all'albero dell'accessibilità
  • Trasversale DOM CDP: utilizzo di 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, con accesso riservato al flag di lancio ComputedAccessibilityInfo, per recuperare il nome e il ruolo di ogni elemento durante il trasferimento.

Attraversamento Puppeteer AXTree

Qui, invece, recuperiamo l'albero dell'accessibilità completo 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, l'esecuzione di query può avvenire nel back-end mediante un'implementazione di C++ anziché nel contesto della pagina tramite JavaScript.

Benchmark test delle unità

La figura seguente confronta il tempo di esecuzione totale dell'esecuzione di query su quattro elementi 1000 volte per i tre prototipi. Il benchmark è stato eseguito in 3 diverse configurazioni, diverse per le dimensioni della pagina e in base all'attivazione o meno della 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 relativa differenza sembra aumentare drasticamente con le dimensioni della pagina. È piuttosto interessante vedere che il prototipo di attraversamento JS DOM risponde così bene consentendo la memorizzazione nella cache di accessibilità. Se la memorizzazione nella cache è disabilitata, la struttura ad albero dell'accessibilità viene calcolata on demand ed elimina la struttura dopo ogni interazione se il dominio è disabilitato. L'attivazione del dominio consente a Chromium di memorizzare nella cache l'albero calcolato.

Per l'attraversamento DOM JS chiediamo il nome e il ruolo accessibili per ogni elemento durante l'attraversamento, quindi se la memorizzazione nella cache è disabilitata, Chromium calcola ed elimina l'albero dell'accessibilità per ogni elemento che visiti. Per gli approcci basati su CDP, invece, la struttura ad albero viene eliminata solo tra ogni chiamata a CDP, ovvero per ogni query. Questi approcci traggono vantaggio anche dall'abilitazione della memorizzazione nella cache, poiché la struttura dell'accessibilità viene quindi persistente in tutte le chiamate CDP, ma l'incremento delle prestazioni è quindi relativamente minore.

Anche se in questo caso l'attivazione della memorizzazione nella cache sembra desiderabile, comporta un costo aggiuntivo di memoria. Per gli script Puppeteer che, ad esempio, registrano i file di traccia, ciò potrebbe essere problematico. Di conseguenza, abbiamo deciso di non abilitare la memorizzazione nella cache dell'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 di CDP migliora le prestazioni in uno scenario di test delle unità cliniche.

Per verificare se la differenza è pronunciata abbastanza da renderla evidente in uno scenario più realistico di esecuzione di una suite di test completa, abbiamo implementato la suite di test end-to-end DevTools per utilizzare i prototipi basati su JavaScript e CDP e 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, pertanto il numero effettivo di esecuzioni del gestore di query aria era 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 si vede nella figura sopra, esiste una differenza evidente nel tempo di esecuzione totale. I dati sono troppo caotici 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 precedenti e poiché l'approccio basato su flag di lancio era sconsigliato in generale, 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, è necessario 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 sottostante, abbiamo provato alcuni approcci che soddisfacessero questa interfaccia. Da qui, abbiamo scoperto che le dimensioni degli oggetti restituiti, ad esempio se abbiamo restituito o meno nodi di accessibilità completi o solo backendNodeIds, non faceva alcuna differenza distinguibile. D'altra parte, abbiamo riscontrato che utilizzare l'attuale NextInPreOrderIncludingIgnored non era una scelta corretta per implementare la logica di attraversamento in questo caso, poiché ciò produceva un notevole rallentamento.

Benchmark: confronto dei prototipi di attraversamento AXTree basati su CDP

Riepilogo

Ora, con l'endpoint CDP attivo, abbiamo implementato il gestore di query sul lato Puppeteer. Il problema è stato la ristrutturazione del codice di gestione delle query per consentire la risoluzione delle query direttamente tramite CDP, invece di eseguire query tramite JavaScript valutato nel contesto della pagina.

Passaggi successivi

Il nuovo gestore aria 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 di test e non vediamo l'ora di conoscere le vostre idee su come rendere questo strumento 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 in anteprima ti consentono di accedere alle ultime funzionalità DevTools, testare le API delle piattaforme web all'avanguardia e individuare eventuali 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 qualsiasi altra informazione relativa a DevTools.

  • Inviaci un suggerimento o un feedback tramite crbug.com.
  • Segnala un problema di DevTools utilizzando Altre opzioni   Altre   > Guida > Segnala i problemi di DevTools in DevTools.
  • Tweet all'indirizzo @ChromeDevTools.
  • Lascia commenti sui video di YouTube o sui suggerimenti di DevTools sui video di YouTube di DevTools.