Sur un gros site public, le front bouge tout le temps : une migration Tailwind par-ci, un composant React par-là, un bloc CMS qui change de gabarit. Et comme toujours avec le CSS, la modification d’une classe qui semblait anodine peut très bien décaler un bloc trois pages plus loin, sans que personne ne s’en rende compte avant la mise en production.
Sur un de nos projets, nous avions déjà des tests Behat pour le fonctionnel, et des tests PHPUnit pour le métier. Mais aucun de ces tests ne disait « la page d’accueil ne ressemble plus à la page d’accueil ». C’est exactement ce que font les tests de non-régression visuelle, et Playwright le fait très bien nativement.
Dans cet article, nous allons voir la stack que nous avons mise en place sur ce projet : les tests eux-mêmes, les tasks Castor pour les piloter, le passage par Docker pour avoir un rendu stable entre les machines de l’équipe, et enfin comment nous postons les images de diff directement dans un commentaire de la pull request.
toHaveScreenshot()Playwright, la solution que nous utilisons déjà pour nos tests E2E, propose une assertion faite pour ça : toHaveScreenshot(). Elle prend une capture de la page, la compare avec l’image de référence commitée dans le dépôt, et échoue si les deux diffèrent trop. En l’occurrence, Playwright vérifie si le nombre de pixels différents entre les 2 images est inférieur à un seuil configuré.
Notre fichier application/e2e/screenshots.spec.ts couvre les pages structurantes du site : la home, la page de résultats de recherche, une page de détail, etc. Plusieurs pages, autant d’images de référence, et de quoi attraper l’immense majorité des régressions CSS.
Un test ressemble à ça :
test('homepage screenshot', async ({ page }) => { await page.goto(homeUrl); // The search form is a React component that mounts client-side; // wait for it so the layout below does not shift. await page.locator('#tab-search').waitFor({ state: 'visible' }); await stabilize(page); await expect(page).toHaveScreenshot('homepage.png', { fullPage: true, animations: 'disabled', maxDiffPixels: 100, // The header image is picked at random server-side, so mask it. mask: [page.getByTestId('homepage-header-image')], }); }); Écrire le test en lui-même est donc assez trivial. Toute la difficulté de l’exercice est ailleurs : il faut que la page soit déterministe. Un test visuel qui échoue une fois sur trois ne sert à rien, car au bout de deux semaines toute l’équipe relance le job sans même regarder. Nous avons donc passé pas mal de temps, non pas à écrire les tests, mais à supprimer une par une toutes les sources de variation.
Le piège classique : la capture est prise pendant que la page finit de se construire. Images en lazy loading, blocs asynchrones, polices web qui provoquent un reflow au moment où elles arrivent… La capture est techniquement valide, mais elle ne correspond à rien de reproductible.
D’où ce petit helper, appelé dans tous les tests :
// Wait for the page to be visually stable before taking a full-page screenshot: // network idle (lazy imag...
[Courte citation de 8% de l'article original]