Ammetto di trovarmi sempre più a mio agio con lo sviluppo guidato dai test (TDD - Test-driven development). Anche perché man mano che sorgono i problemi so dove andare a cercarli. Ovviamente sconto ancora una certa inesperienza. E penso di pagare anche una certa ingenuità. Un problema che mi ha fatto sbattere la testa è legato alle modifiche non volute dell’aspetto grafico di una pagina web. Per risolvere questo problema ho dovuto imparare come eseguire dei Visual Regression Test.
Scelgo che strumenti usare
Per una panoramica generale consiglio di leggere questo articolo di Leonardo Giroto, è di qualche tempo fa ma è scritto bene. Per quello che riguarda il mio problema, invece, ho valutato 3 opzioni. In sintesi, ho preso in considerazione cypress, Puppeteer e Playwright. Sono tutti strumenti validi ma solo Playwright permette di interfacciarsi facilmente con Electron. Lo so, lo so, questa è un’esigenza mia: il progetto che ho in testa prevede una parte costruita con Electron quindi questa caratteristica è fondamentale per me.
Capito questo è il momento di cominciare con il codice. Riprendo quindi in mano il mio template Svelte Component Package Starter e comincio con aggiungere Playwright:
npm i -D playwright @playwright/test
E poi installo i browser da usare come base per i test
npx playwright install
Per un uso base non mi serve altro ma preferisco continuare a usare Jest anche per questo genere di test. Mi serve però un pacchetto aggiuntivo: Jest Image Snapshot:
npm i --save-dev jest-image-snapshot @types/jest-image-snapshot
Metto in ordine i test vecchi
È buona pratica tenere separati i test e2e (End to End). Quindi modifico leggermente la struttura del mio template e creo le due cartelle src/__tests__/unit
e src/__tests__/e2e
:
src
├── __tests__
│ ├── unit
│ │ ├── ChromaColors.test.ts
│ │ ├── GridColors.test.ts
│ │ └── Slider.test.ts
│ └── e2e
├── lib
├── routes
├── app.css
├── app.html
└── global.d.ts
Copio dentro unit
i test precedenti e lascio vuota, per il momento, e2e
.
Il primo problema che mi si pone è che eseguendo npm run test
eseguo sia i test di unità che quelli e2e. Modifico quindi package.json
in modo da tenere separati i due binari:
{
// ...
"scripts": {
// ...
"test": "cross-env TAILWIND_MODE=build jest --runInBand ./src/__tests__/unit",
"test:e2e": "jest --runInBand ./src/__tests__/e2e",
}
//...
}
Configuro Jest per fare screenshot
Per utilizzare Jest-Image-Snapshot
devo prima estendere expect
per supportare toMatchImageSnapshot
. Modifico quindi jest-setup.ts
:
import '@testing-library/jest-dom';
import { toMatchImageSnapshot } from 'jest-image-snapshot';
expect.extend({ toMatchImageSnapshot });
Creo un test di esempio
Come primo test mi serve qualcosa di semplice e banale, giusto per verificare che tutto funzioni a dovere. Creo il file e2e.test.ts
:
import { Browser, chromium } from 'playwright';
describe('jest-image-snapshot: test is working', () => {
let browser: Browser;
beforeAll(async () => {
browser = await chromium.launch();
});
afterAll(async () => {
await browser.close();
});
test("should work", async () => {
const page = await browser.newPage();
await page.goto('https://www.example.com/');
const image = await page.screenshot();
expect(image).toMatchImageSnapshot();
})
})
Cosa fa questo test? Usa Playwright per lanciare un browser nascosto, va alla pagina https://www.example.com
, cattura un’immagine della pagina e la confronta con quella salvata in memoria. Se non esiste un’immagine di riferimento ne crea una nuova nella cartella __image_snapshots__
. Eseguo
npm run test:e2e
e ottengo l’immagine:

Ovviamente questo test è puramente didattico: serve a me per capire come utilizzare questo strumento. Cambio l’indirizzo della pagina da aprire (uso www.google.com
). Rieseguo il test e ottengo

È inoltre apparsa una nuova cartella __diff_output__
al cui interno c’è un’immagine:

Sono evidenziate in rosso le differenze tra un’immagine e l’altra. Essendo due pagine completamente diverse è quasi tutto rosso. Ma spiega bene il senso di questo tipo di test.
Faccio finta per un momento che la nuova pagina sia corretta e che le differenze siano volute. Per superare il test devo aggiornare lo screenshot. Creo uno script che mi semplifichi il lavoro:
"test:e2e-update": "jest --runInBand --updateSnapshot ./src/__tests__/e2e",
e lo eseguo:
npm run test:e2e-update
Adesso la mia pagina di riferimento è diventata:

Questa volta ripetendo il test npm run test:e2e
non ottengo errori.
Creo un test personalizzato
Ovviamente questo test è inutile: serve solo a me per capire come creare qualcosa di utile. Creo una pagina src/routes/test.svelte
dedicata ai test:
<script lang="ts">
import GridColors from '$lib/components/GridColors.svelte';
import { stringToColorStyle } from '../lib/functions/ChromaColors';
const settings = {
firstColor: 'khaki',
secondColor: 'teal',
steps: 9
};
settings.firstColor = stringToColorStyle(settings.firstColor).hex;
settings.secondColor = stringToColorStyle(settings.secondColor).hex;
let borderColor = 'orange';
$: settings.firstColor = stringToColorStyle(settings.firstColor).hex;
$: settings.secondColor = stringToColorStyle(settings.secondColor).hex;
const changeBorderColor = () => (borderColor = borderColor === 'orange' ? 'green' : 'orange');
const changeFirstColor = () =>
(settings.firstColor =
settings.firstColor === stringToColorStyle('khaki').hex ? 'tomato' : 'khaki');
const changeSecondColor = () =>
(settings.secondColor =
settings.secondColor === stringToColorStyle('teal').hex ? 'dimgray' : 'teal');
const reset = () => {
settings.firstColor = 'khaki';
settings.secondColor = 'teal';
settings.steps = 9;
};
</script>
<main>
<h1>Visual Regression Test</h1>
<p>Use this page to test component graphics changes</p>
<div id="grid-colors">
<GridColors {...settings} --border-color={borderColor} />
</div>
<section>
<button id="change-border-color" on:click={changeBorderColor}>Change border color</button>
<button id="change-first-color" on:click={changeFirstColor}>Change first color</button>
<button id="change-second-color" on:click={changeSecondColor}>Change second color</button>
<div>
<span>Steps:</span>
{#each Array(23) as array, i}
<label>
<input type="radio" bind:group={settings.steps} value={i + 2} />
{i + 2}
</label>
{/each}
</div>
<button id="reset" on:click={reset}>Reset</button>
</section>
</main>
<style lang="postcss">
#grid-colors { @apply mb-2 mt-2; }
main { @apply overflow-y-auto; }
section { @apply flex flex-col space-y-1; }
</style>
Ho inserivo vari pulsanti e controlli per testare il mio componente in varie situazioni. Modifico il file e2e.test.ts
in modo da fare riferimento alla pagina con i test:
import { Browser, chromium } from 'playwright';
describe('visual regression test', () => {
let browser: Browser;
beforeAll(async () => {
browser = await chromium.launch();
});
afterAll(async () => {
await browser.close();
});
test("test page", async () => {
const page = await browser.newPage();
await page.goto('http://localhost:3000/test');
const image = await page.screenshot();
expect(image).toMatchImageSnapshot();
})
})
Ed eseguo il test con il comando npm run test:e2e
.
Non mi interessa uno screenshot statico, mi interessa vedere cosa succede quando modifico i vari parametri. Aggiungo quindi un’azione per cliccare automaticamente su vari pulsanti, registrare lo schermo e poi confrontare il risultato:
test("test page", async () => {
const page = await browser.newPage();
await page.goto('http://localhost:3000/test');
const image = await page.screenshot();
expect(image).toMatchImageSnapshot();
await page.click('text=Change border color');
let changeBorder = await page.screenshot();
expect(changeBorder).toMatchImageSnapshot();
await page.click('text=Change border color');
changeBorder = await page.screenshot();
expect(changeBorder).toMatchImageSnapshot();
})
Creo in maniera simile i test per tutti i pulsanti e i controlli.
Dopo aver sistemato i test posso tornare a modificare il codice. La cosa bella è che sbaglio qualcosa, o se succede qualcosa di non previsto, posso avere un warning e accorgermi rapidamente che qualcosa non funziona:

Un’altra cosa interessante è che gli screenshot danno una buona idea delle caratteristiche del componente e valgono quasi come documentazione:

Questo è tutto, per il momento. Come al solito è possibile vedere il codice del repository all’indirizzo el3um4s/svelte-component-package-starter.