L'assertion toHaveScreenshot() de Playwright capture un screenshot de référence et fait échouer les exécutions suivantes quand la différence de pixels dépasse un seuil configurable. Elle détecte les décalages de mise en page, les changements de couleur et les régressions de rendu que les assertions fonctionnelles ne vérifient jamais.
Ce qu'est réellement le test de régression visuelle
Un test de régression visuelle capture un screenshot d'une page ou d'un élément, le stocke comme référence, puis compare chaque exécution suivante contre cette référence pixel par pixel. Si la différence dépasse un seuil configurable, le test échoue et montre exactement quels pixels ont changé.
La distinction avec un simple screenshot est importante. page.screenshot() enregistre juste un fichier. Ça n'échoue jamais. Ça ne dit rien sur l'aspect correct de la page. Les tests de régression visuelle nécessitent une référence (l'image "voilà à quoi ça doit ressembler" validée) et une comparaison automatisée contre cette référence à chaque exécution.
L'intérêt est réel. On détecte des régressions visuelles qu'aucune assertion fonctionnelle ne remonterait. Exemples typiques : un changement CSS qui décale une modale, un bug de z-index qui cache un dropdown derrière une bannière, ou un mode sombre qui inverse accidentellement un logo. Ces bugs passent en revue de code parce que les relecteurs se concentrent sur la logique, pas les pixels.
Le défi est tout aussi réel. Les screenshots sont sensibles. Une différence d'un pixel d'anti-aliasing entre macOS et Linux, un horodatage dynamique sur la page, une publicité qui fait tourner du contenu : tout ça génère de faux échecs. Gérer ce bruit, c'est l'essentiel du travail pratique en tests de régression visuelle.
toHaveScreenshot() : l'assertion intégrée
L'assertion visuelle de Playwright est expect(locator).toHaveScreenshot() ou expect(page).toHaveScreenshot(). On peut capturer la page entière ou limiter à n'importe quel locator.
// tests/visual/homepage.spec.ts
import { test, expect } from '@playwright/test';
test('la page d\'accueil correspond à la référence', async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
// Screenshot de la page entière
await expect(page).toHaveScreenshot('homepage.png');
});
test('le bouton de connexion correspond à la référence', async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
// Screenshot limité à un élément spécifique
const loginButton = page.getByRole('button', { name: 'Login' });
await expect(loginButton).toHaveScreenshot('login-button.png');
});L'argument nom ('homepage.png') est optionnel. Sans lui, Playwright génère un nom automatiquement depuis le titre du test et un compteur. Donner un nom explicite facilite la recherche et la compréhension des fichiers de référence lors de leur révision.
À la première exécution, il n'y a pas de référence à comparer. Playwright en crée une.
Générer les screenshots de référence à la première exécution
Exécutez vos tests pour la première fois et vous verrez des erreurs comme celle-ci :
Error: A snapshot doesn't exist at tests/visual/homepage.spec.ts-snapshots/homepage-chromium-darwin.png, writing actual.C'est normal. Playwright indique qu'il a écrit le fichier de référence et vous demande de le vérifier puis de le committer. Le test échoue intentionnellement à la première exécution. Playwright ne crée pas silencieusement une référence sans vous en informer.
Après la première exécution, le projet contient un dossier de snapshots :
tests/
visual/
homepage.spec.ts
homepage.spec.ts-snapshots/
homepage-chromium-darwin.png
homepage-chromium-linux.png
login-button-chromium-darwin.pngVérifiez ces images. Si elles semblent correctes, committez-les dans votre dépôt. Elles constituent désormais la référence. Chaque exécution de test suivante compare contre ces fichiers committés.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// Emplacement des fichiers snapshot. Par défaut : à côté du fichier spec.
snapshotDir: './tests/__snapshots__',
use: {
baseURL: 'https://lab.becomeqa.com',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});snapshotDir centralise tous les snapshots dans un seul dossier, ce que certaines équipes préfèrent pour une organisation plus propre du dépôt.
Mettre à jour les références avec --update-snapshots
L'application change. Le design change. Quand un changement visuel est intentionnel, la référence doit être mise à jour :
npx playwright test --update-snapshotsCette commande écrase tous les snapshots existants par de nouveaux screenshots. Chaque test exécuté aura son état actuel comme nouvelle référence.
Pour mettre à jour uniquement les snapshots d'un fichier de test :
npx playwright test tests/visual/homepage.spec.ts --update-snapshotsOu pour un test spécifique par nom :
npx playwright test --update-snapshots -g "la page d'accueil correspond à la référence"--update-snapshots avec la même prudence que git push --force. L'exécuter sans précaution écrase des références légitimes avec des états cassés. Vérifiez toujours les images mises à jour avant de les committer. En CI, ce flag ne doit jamais être défini automatiquement. Il doit uniquement s'exécuter en réponse à une action délibérée du développeur.Après la mise à jour, committez les fichiers .png modifiés. Le diff en revue de code montrera les images avant/après, ce qui est exactement le bon endroit pour détecter des changements visuels non intentionnels.
Configurer les seuils de comparaison
La comparaison pixel par pixel fonctionne parfaitement dans un environnement contrôlé et génère du bruit constant partout ailleurs. Playwright propose trois options de seuil pour gérer la sensibilité.
test('la carte produit correspond à la référence', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/products');
const productCard = page.locator('.product-card').first();
await expect(productCard).toHaveScreenshot('product-card.png', {
// Nombre maximum de pixels autorisés à différer
maxDiffPixels: 100,
// Ratio maximum de pixels différents (0-1). 0.01 = 1% de tous les pixels
maxDiffPixelRatio: 0.01,
// Seuil de différence de couleur par pixel (0-1). Plus élevé = plus tolérant
threshold: 0.2,
});
});threshold contrôle à quel point un pixel doit différer pour être comptabilisé comme "différent". La valeur par défaut est 0.2, ce qui gère les légères différences d'anti-aliasing et de rendu sub-pixel. Montez à 0.3 ou 0.4 sur des composants avec beaucoup de courbes ou de dégradés où le rendu varie légèrement selon les plateformes.
maxDiffPixels est un comptage absolu. À utiliser pour les petits composants délimités où quelques pixels peuvent varier (rendu d'icône, border radius) mais où un décalage de 50 pixels doit toujours échouer.
maxDiffPixelRatio est un pourcentage du total de pixels. À utiliser pour les screenshots pleine page où le nombre total de pixels est élevé. maxDiffPixels: 100 sur une page 1920x1080 est extrêmement strict, mais maxDiffPixelRatio: 0.001 donne une tolérance raisonnable.
Définissez des valeurs par défaut dans playwright.config.ts pour éviter de répéter les mêmes seuils dans chaque test :
// playwright.config.ts
export default defineConfig({
expect: {
toHaveScreenshot: {
maxDiffPixels: 100,
threshold: 0.2,
},
},
});Les tests individuels peuvent toujours surcharger ces valeurs si nécessaire.
Masquer le contenu dynamique
Le contenu dynamique est la principale source de faux échecs dans les tests de régression visuelle. Un horodatage qui change chaque seconde, un avatar utilisateur chargé depuis un CDN, une bannière publicitaire qui fait tourner du contenu : chacun génère un diff à chaque exécution.
L'option mask de Playwright accepte un tableau de locators. Ces zones sont peintes avec une couleur unie avant la comparaison.
test('le tableau de bord correspond à la référence', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/dashboard');
await expect(page).toHaveScreenshot('dashboard.png', {
mask: [
// Masquer l'horodatage "Dernière mise à jour" dans l'en-tête
page.locator('[data-testid="last-updated-timestamp"]'),
// Masquer l'avatar utilisateur. Différent pour chaque utilisateur.
page.locator('[data-testid="user-avatar"]'),
// Masquer les conteneurs publicitaires tiers
page.locator('.ad-container'),
],
// Personnaliser la couleur du masque (par défaut : overlay magenta)
maskColor: '#FF00FF',
});
});Les zones masquées apparaissent dans la comparaison comme un bloc de couleur unie. La comparaison s'exécute quand même sur l'ensemble du screenshot. Les zones masquées correspondent toujours à elles-mêmes puisque le même masque est appliqué aux deux screenshots.
data-testid au contenu dynamique spécifiquement pour pouvoir le masquer de façon fiable dans les tests visuels. La sélection par nom de classe fonctionne, mais les noms de classe changent. Un data-testid="user-avatar" est stable et communique clairement son but à quiconque lit le test.Pour les animations, animations: 'disabled' arrête les animations CSS avant la capture :
test('la section hero animée correspond à la référence', async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
await expect(page).toHaveScreenshot('hero.png', {
animations: 'disabled',
});
});Cela fige les transitions et animations CSS à leur état initial, ce qui rend les composants animés déterministes. Pour les animations pilotées par JavaScript qui n'utilisent pas de transitions CSS, il peut être nécessaire d'attendre la fin de l'animation ou d'ajouter un waitForLoadState('networkidle') avant l'assertion.
Nommage des snapshots et organisation multi-plateforme
Regardez le nom de fichier généré par Playwright : homepage-chromium-darwin.png. Le navigateur et le système d'exploitation sont intégrés dans le nom. Ce n'est pas un hasard.
La même page rendue dans Chromium sur macOS versus Chromium sur Linux produit des pixels subtilement différents. Le hinting des polices, le rendu sub-pixel, et les petites différences dans la façon dont l'OS compose les graphiques créent des variations subtiles entre plateformes. On ne peut pas partager une seule image de référence entre elles. Playwright gère ça en créant des références séparées pour chaque combinaison navigateur/OS.
tests/__snapshots__/
homepage.spec.ts/
homepage-chromium-darwin.png (macOS Chrome)
homepage-chromium-linux.png (Linux Chrome)
homepage-firefox-linux.png (Linux Firefox)
homepage-webkit-darwin.png (macOS Safari)Le pattern de nommage se contrôle via snapshotPathTemplate dans playwright.config.ts :
// playwright.config.ts
export default defineConfig({
snapshotPathTemplate:
'{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}-{projectName}-{platform}{ext}',
});Les tokens disponibles :
{arg}: le nom passé àtoHaveScreenshot(){projectName}: le nom du projet dans la config (ex.chromium,firefox){platform}: l'OS (darwin,linux,win32){testFileName}: le nom du fichier spec sans extension{snapshotDir}: le dossier de base des snapshots
Gardez {platform} dans le template. Le supprimer pour partager une référence entre OS est l'erreur la plus courante des équipes qui configurent les tests visuels pour la première fois, et elle génère des faux échecs constants en CI.
Exécuter les tests visuels en CI
Exécuter les tests visuels en CI révèle immédiatement le problème cross-OS. Les références ont été générées sur un Mac de développeur. Le pipeline CI tourne sur Linux. Les snapshots ne correspondent pas.
La solution la plus propre : générer les références dans le même conteneur Docker que celui utilisé par la CI. Playwright fournit des images Docker officielles.
# .github/workflows/visual-tests.yml
name: Tests visuels
on: [push, pull_request]
jobs:
visual:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.44.0-jammy
steps:
- uses: actions/checkout@v4
- name: Installer les dépendances
run: npm ci
- name: Exécuter les tests visuels
run: npx playwright test tests/visual/
- name: Uploader le rapport diff en cas d'échec
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-visual-report
path: playwright-report/
retention-days: 7Quand les tests échouent en CI, le rapport uploadé contient le screenshot actuel, la référence attendue, et une image diff qui surligne exactement les pixels modifiés. C'est ainsi qu'on distingue une vraie régression visuelle d'une incompatibilité d'environnement.
Pour générer des références Linux depuis un Mac sans passer à Linux, exécutez le conteneur Docker Playwright en local :
# Générer des références compatibles Linux depuis votre Mac
docker run --rm \
-v "$(pwd):/work" \
-w /work \
mcr.microsoft.com/playwright:v1.44.0-jammy \
npx playwright test tests/visual/ --update-snapshotsCela écrit les nouveaux fichiers snapshot *-linux.png qui correspondront à ce que produit la CI. Committez ces fichiers et les échecs CI liés aux différences de plateforme disparaissent.
Un pattern courant en CI : exécuter les tests visuels dans une étape ou un projet séparé, conditionné au passage de la suite fonctionnelle. Les tests visuels sont plus lents que les tests fonctionnels et leurs échecs sont plus bruités. Les garder dans une étape dédiée évite de bloquer le feedback rapide sur les régressions fonctionnelles.
// playwright.config.ts
export default defineConfig({
projects: [
// Les tests fonctionnels s'exécutent en premier
{
name: 'functional',
testMatch: 'tests/functional/**/*.spec.ts',
},
// Les tests visuels s'exécutent après les tests fonctionnels
{
name: 'visual',
testMatch: 'tests/visual/**/*.spec.ts',
dependencies: ['functional'],
},
],
});Playwright intégré vs Applitools et Percy
Les tests visuels intégrés de Playwright couvrent beaucoup de terrain. Mais des outils commerciaux comme Applitools Eyes et Percy existent pour des raisons qui méritent d'être comprises.
La limite principale de l'approche intégrée : la gestion des snapshots. Chaque image de référence vit dans votre dépôt. Un projet avec 50 tests visuels, 3 navigateurs et 2 plateformes génère 300 fichiers PNG. Ajoutez des cas de test et le dépôt grossit. Réviser les changements visuels dans une pull request signifie regarder des diffs d'images dans l'interface GitHub. Ça fonctionne, mais ce n'est pas idéal pour les grandes images ou les changements subtils.
Applitools et Percy résolvent ça avec un stockage cloud des références et des interfaces dédiées à la révision des diffs visuels. Ils proposent aussi une comparaison par IA qui distingue les décalages de mise en page des changements de contenu, et des workflows d'équipe pour approuver ou rejeter les changements.
| | Playwright intégré | Applitools / Percy |
|---|---|---|
| Coût | Gratuit | Payant (offre gratuite disponible) |
| Configuration | Minutes | Minutes + clé API |
| Stockage des références | Dépôt Git | Cloud |
| Interface de révision des diffs | Rapport HTML Playwright | Interface cloud dédiée |
| Comparaison par IA | Non | Oui (Applitools) |
| Références multi-navigateurs | Fichiers séparés par navigateur/OS | Unifiées avec normalisation |
| Snapshots CI | Nécessite une image Docker correspondante | Géré par le service |
Pour un projet solo ou une petite équipe, l'approche intégrée est le bon point de départ. C'est gratuit, rapide à configurer, et ça gère correctement les cas courants. Le workflow Docker gère le problème cross-OS une fois mis en place.
Pour les équipes plus grandes avec plusieurs relecteurs, la friction de gérer des fichiers PNG dans git et de réviser des diffs dans GitHub devient réelle. C'est à ce moment qu'un service dédié commence à justifier son coût. On paye autant pour le workflow de révision que pour la technologie de comparaison.
FAQ
Comment exécuter uniquement les tests visuels sans toute la suite ?Utilisez un flag --grep ou organisez les tests visuels dans leur propre dossier : npx playwright test tests/visual/. Avec des projets dans la config, npx playwright test --project=visual n'exécute que le projet visuel.
Attendez que le spinner disparaisse avant l'assertion : await page.locator('[data-testid="loading-spinner"]').waitFor({ state: 'hidden' }). Ou masquez-le. Le masquage est plus résistant. Si le timing change, un masque gère quand même la situation, mais un waitFor avec un timeout serré peut ne plus suffire.
toHaveScreenshot() pour tester des viewports mobiles ?
Oui. Définissez le viewport dans la config du projet ou dans le test : await page.setViewportSize({ width: 375, height: 812 }). Playwright traitera les screenshots mobiles et desktop comme des références séparées s'ils sont capturés dans des tests ou projets distincts.
Moins qu'on ne le croit. Les tests visuels sont mieux réservés aux composants où le résultat visuel fait réellement partie des spécifications. Les états des boutons d'un design system, une visualisation de données, un aperçu d'export PDF en sont de bons exemples. Essayer de couvrir visuellement chaque page crée une charge de maintenance que les équipes abandonnent généralement en quelques mois.
Peut-on capturer un composant en isolation sans naviguer vers une page ?Pas directement avec Playwright. C'est un outil basé sur le navigateur qui opère sur des pages complètes. Pour les tests visuels de composants en isolation, Storybook avec Chromatic est l'outil plus approprié. Les tests visuels Playwright fonctionnent mieux au niveau de l'intégration : des pages réelles dans un vrai navigateur.
→ See also: Fixtures Personnalisés dans Playwright: Le Modèle qui Rend les Tests Lisibles | Fichier de Configuration Playwright Expliqué: Toutes les Options à Connaître | Déboguer les Tests Instables: Un Guide Pratique | Tests de Régression Visuelle par IA: Au-delà des Captures Pixel-Parfaites | Tests Multi-Navigateurs avec Playwright: Chrome, Firefox, Safari