Cos'è Playwright e come si testano le app nel browser
Cos'è Playwright e come funzionano i test end-to-end: auto-waiting, locator per ruolo, codegen, trace viewer, integrazione in CI e quanti test e2e scrivere.
Hai i test sulle funzioni, passano tutti, e la registrazione è rotta da due giorni perché un pulsante finisce sotto il banner dei cookie. È il buco che nessun test unitario vede: nessuno ha mai aperto davvero l'applicazione. In questo articolo trovi cosa fa Playwright, perché l'auto-waiting rende i suoi test meno instabili, come si scrivono i locator giusti e quanti test end-to-end scrivere prima che diventino un problema.
Cos'è
Playwright è uno strumento che guida un browser vero al posto tuo: apre le pagine, clicca, compila i moduli e verifica che l'utente veda quello che deve vedere.
È la punta della piramide descritta in cosa sono i test software: il test end-to-end attraversa tutto — interfaccia, rete, backend, database — come farebbe una persona.
Nasce in Microsoft, dallo stesso gruppo di Puppeteer, e la differenza rispetto agli strumenti storici — Selenium in testa — è che è stato progettato quando i problemi dei test end-to-end erano già noti.
Le tre cose che lo distinguono
1. Un browser vero, anzi tre
Playwright pilota Chromium, Firefox e WebKit (il motore di Safari) con la stessa identica API. WebKit è il motivo più concreto: è il motore di ogni browser su iPhone, e i bug che compaiono solo lì sono un classico — poterli intercettare senza un Mac non è poco. C'è anche l'emulazione dei dispositivi mobili, che sposta i test responsive da "ricordarsi di controllare" ad "automatico".
2. L'auto-waiting: il motivo per cui i test smettono di essere instabili
È il punto centrale, e la ragione per cui oggi ha senso scrivere test end-to-end mentre dieci anni fa molti team ci avevano rinunciato.
Il problema. Il tuo test clicca un pulsante che compare dopo una chiamata di rete, a volte in 80 millisecondi e a volte in 900. Il test cerca il pulsante, non c'è ancora, fallisce. Lo rilanci, stavolta la rete è veloce, passa. Stesso codice, risultato diverso: un test flaky.
La soluzione sbagliata, quella che si usava, è mettere pause fisse:
await page.waitForTimeout(2000); // non fatelo
await page.click("#invia");
Due secondi non bastano mai e avanzano sempre: non bastano in CI, dove la macchina è più lenta, e avanzano ogni volta che la pagina è pronta subito. Poi qualcuno alza a cinque secondi, e la suite passa da lenta a inutilizzabile.
Cosa fa Playwright. Prima di ogni azione controlla in un ciclo che l'elemento sia utilizzabile: presente nel DOM, visibile, fermo (non in mezzo a un'animazione), abilitato, e non coperto da un altro elemento. Quando tutte le condizioni sono vere, agisce. Se entro il timeout non lo diventano, fallisce dicendoti quale condizione mancava.
await page.getByRole("button", { name: "Invia" }).click();
Quella riga contiene già l'attesa: il test aspetta il minimo necessario invece di una cifra inventata, e non scrivi mai attese a mano, quindi non le sbagli.
Lo stesso vale per le asserzioni, riprovate finché non sono vere o scade il tempo:
await expect(page.getByText("Ordine confermato")).toBeVisible();
Il controllo "coperto da un altro elemento" prende i casi reali: il banner dei cookie sopra il pulsante, il modale che si chiude con una dissolvenza, l'header appiccicoso che copre il link. Con Selenium quei clic finivano nel vuoto, con un messaggio incomprensibile.
3. I locator basati sui ruoli di accessibilità
Il secondo grande motivo di fragilità sono i selettori. Se scrivi:
await page.click(".btn.btn-primary.mt-4 > span:nth-child(2)");
hai legato il test a dettagli grafici. Qualcuno rinomina una classe o aggiunge un <div> di troppo, e il test si rompe anche se l'applicazione funziona benissimo.
Playwright spinge verso i locator semantici, quelli che descrivono cosa l'utente vede e fa:
page.getByRole("button", { name: "Accedi" });
page.getByLabel("Indirizzo email");
page.getByPlaceholder("cerca...");
page.getByText("Benvenuto");
page.getByTestId("riepilogo-ordine");
getByRole è quello da usare quasi sempre. Interroga l'albero di accessibilità: il ruolo dell'elemento (pulsante, link, casella di testo) e il nome con cui viene annunciato. Sono le due cose che restano stabili quando cambia il DOM attorno, perché rappresentano il significato e non la forma.
C'è un effetto collaterale utile: se non riesci a scrivere getByRole per un elemento, di solito è una <div> con un onClick sopra, senza ruolo né etichetta — cioè non è accessibile a un lettore di schermo, e il test te lo ha appena segnalato. Test robusti e interfaccia accessibile arrivano insieme.
getByTestId resta come ultima risorsa, quando non c'è niente di semantico a cui aggrapparsi.
Un test completo
npm init playwright@latest # configurazione, esempi e browser
npx playwright test
npx playwright test --ui # la modalità interattiva, la userai molto
// tests/checkout.spec.js
import { test, expect } from "@playwright/test";
test.describe("Checkout", () => {
test("un utente registrato completa un ordine", async ({ page }) => {
await page.goto("/login");
await page.getByLabel("Email").fill("mario@example.com");
await page.getByLabel("Password").fill("password-di-prova");
await page.getByRole("button", { name: "Accedi" }).click();
await expect(page.getByRole("heading", { name: "Il tuo account" })).toBeVisible();
await page.goto("/corsi/javascript");
await page.getByRole("button", { name: "Aggiungi al carrello" }).click();
await page.getByRole("link", { name: /carrello/i }).click();
await expect(page.getByText("1 articolo")).toBeVisible();
await page.getByRole("button", { name: "Vai al pagamento" }).click();
await expect(page).toHaveURL(/\/checkout/);
await expect(page.getByRole("heading", { name: "Riepilogo ordine" })).toBeVisible();
});
});
Si legge come la descrizione di quello che fa una persona, ed è il punto: nessuna attesa esplicita, nessun selettore CSS.
Una nota pratica: il login ripetuto in ogni test fa perdere minuti. Playwright permette di eseguirlo una volta, salvare lo stato della sessione e riusarlo, così ogni test parte già autenticato.
Il codegen: scrivere test registrando
npx playwright codegen https://miosito.it
Si apre un browser e una finestra accanto: tu navighi normalmente e Playwright scrive il codice del test mentre lo fai, scegliendo da solo i locator migliori.
Serve per capire come si scrivono i locator e per buttare giù lo scheletro di un percorso lungo. Poi va ripulito: registra anche clic inutili e non sa quali asserzioni ti interessano. Non è un modo per generare la suite senza pensarci — un test registrato e mai toccato è un test che nessuno capisce quando fallisce tra sei mesi.
Capire i fallimenti in CI: trace viewer e screenshot
Il momento peggiore dei test end-to-end: la pipeline è rossa, in locale funziona tutto, e l'unica informazione è "timeout dopo 30 secondi". Playwright ha risolto questo problema meglio di chiunque, ed è da solo un motivo di scelta.
// playwright.config.js
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
retries: process.env.CI ? 2 : 0,
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
video: "retain-on-failure",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
],
webServer: {
command: "npm run build && npm run start",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
});
Con trace: "on-first-retry" ogni test fallito produce una traccia:
npx playwright show-trace trace.zip
Dentro c'è una linea del tempo con uno screenshot del DOM per ogni singola azione: lo stato esatto della pagina al momento del clic mancato, le chiamate di rete, la console del browser. Quasi sempre il motivo è evidente in trenta secondi — "l'elemento c'era ma sotto un overlay" o "la chiamata ha risposto 500".
Nota anche webServer: Playwright avvia l'applicazione prima dei test e la spegne alla fine.
Il tema vero: quanti test end-to-end scrivere
Qui va detta la cosa che le guide tendono a nascondere, ed è la ragione per cui la maggior parte delle suite end-to-end viene abbandonata.
Sono lenti. Un test unitario dura millisecondi, un end-to-end secondi o decine di secondi. Cento test su tre browser sono mezz'ora di attesa a ogni push, e una suite da mezz'ora non viene lanciata prima di aprire una pull request: non protegge più niente.
Sono fragili anche con Playwright. L'auto-waiting elimina la categoria più grossa di instabilità, non tutte: restano i dati di test che si sporcano, i servizi esterni lenti, i fusi orari, le macchine di CI sovraccariche. Un test che fallisce a caso una volta su venti, per cento test, significa pipeline rossa quasi sempre — e una pipeline rossa per abitudine è una pipeline che nessuno guarda più.
Sono costosi da diagnosticare. Con un test unitario sai quale funzione guardare; con un end-to-end il colpevole può essere ovunque tra il pixel e il database.
La regola onesta: pochi test end-to-end, sui percorsi che se si rompono ti fanno perdere soldi o utenti. Per un prodotto tipico sono cinque-quindici:
- Registrazione e login
- Il checkout completo, fino al pagamento confermato
- La funzione che è la ragione per cui la gente usa il prodotto
- Il recupero password
- Una pagina pubblica chiave, per accorgerti se il sito è semplicemente giù
Tutto il resto — validazione del modulo, calcolo dell'IVA, casi limite — sta più in basso nella piramide, dove costa cento volte meno: è quello che fai con Vitest o Jest.
Il criterio per aggiungerne uno: "se si rompe di notte e nessuno se ne accorge, cosa succede?" Se la risposta è "niente di grave", non è un test end-to-end.
| Test unitario | Test end-to-end | |
|---|---|---|
| Durata | Millisecondi | Secondi o decine di secondi |
| Cosa verifica | Una funzione isolata | L'intero sistema, come lo vede l'utente |
| Quando fallisce | Sai subito dove | Devi indagare |
| Instabilità | Praticamente nulla | Presente, da gestire |
| Quanti | Centinaia | Cinque-quindici |
| Dove sta | Base della piramide | Punta |
In CI
Su GitHub Actions sono poche righe:
name: E2E
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 7
Tre cose che contano:
playwright install --with-depsscarica i browser e le librerie di sistema. Saltarlo è il primo errore di chiunque.- Carica il report come artifact, altrimenti quando la pipeline è rossa hai solo il log e nessuna traccia da aprire.
retries: 2solo in CI. Un ritentativo salva dai fallimenti casuali, ma è una medicina, non una cura: se un test lo richiede sempre, va sistemato.
Il quadro generale è in cos'è la CI/CD.
Quando non ti serve
Su un sito statico — un blog, una vetrina — non c'è quasi niente da testare: nessun flusso, nessuno stato, nessun login. Un controllo che le pagine rispondano 200 e un occhio ai link rotti fanno lo stesso lavoro con un centesimo dello sforzo.
Su un progetto piccolo o all'inizio, quando l'interfaccia cambia ogni due giorni, questi test si rompono più in fretta di quanto li riscrivi. Prima si stabilizzano i flussi, poi si bloccano con i test.
Se non hai nemmeno i test unitari, comincia da quelli: partire dalla punta della piramide ti dà la suite più lenta e fragile possibile, e la abbandoni entro un mese. E se hai un solo flusso critico, un test scritto bene può bastare — meglio uno che funziona sempre di quaranta che nessuno guarda.
In sintesi
Playwright guida un browser vero e verifica quello che l'utente vede, su Chromium, Firefox e WebKit con lo stesso codice.
L'auto-waiting è la differenza sostanziale rispetto agli strumenti storici: aspetta che l'elemento sia visibile, fermo, abilitato e non coperto, invece di farti indovinare i secondi di una pausa. È quello che toglie di mezzo la maggior parte dei test instabili.
I locator per ruolo rendono i test resistenti al refactoring grafico, e ti dicono quando un elemento non è accessibile.
Trace viewer e screenshot risolvono il problema peggiore: capire perché un test è fallito in CI e non in locale.
Pochi, sui percorsi che generano ricavi. Cinque-quindici test, non cento. Il resto sta più in basso nella piramide, dove è veloce e non si rompe da solo.
Su un sito statico o un progetto ancora instabile, non ti serve.
Per i test alla base della piramide vedi Vitest o Jest e cosa sono i test software. Per farli girare a ogni push, cosa sono le GitHub Actions.
Contenuto redatto con l'assistenza di strumenti di intelligenza artificiale e rivisto dalla redazione di Codegrind, che ne è responsabile. Hai trovato un errore? Scrivici.