PrestaFlow : régressions visuelles
Ce que les assertions fonctionnelles ne voient pas
Reprenons le premier scénario de l’article d’introduction : on vérifie que le bloc #psflowdemo-block est visible sur la home et que son titre contient « Bienvenue ». Robuste, rapide à écrire.
Robuste, et aveugle à tout ce qui n’est pas testé par ces deux assertions :
- si quelqu’un colore le bloc en rose fluo, le test passe ;
- si le titre passe en
font-size: 6px, le test passe ; - si le bloc déborde de son conteneur sur mobile, le test passe ;
- si une mise à jour du thème casse la marge entre le bloc et le pied de page, le test passe.
Cette classe de bugs — visibles à l’œil, invisibles à l’assertion — est précisément ce que couvrent les régressions visuelles. Le principe : au premier run on prend une photo (le baseline), aux runs suivants on prend une nouvelle photo et on la compare au baseline.
PrestaFlow expose ça via une seule méthode : visualCheckpoint.
visualCheckpoint en une ligne
Reprenons la Page Modules\Psflowdemo\Home de l’article 1. On ne lui ajoute rien — la méthode visualCheckpoint est définie dans CommonPage, dont héritent toutes les Pages de la lib, donc aussi FrontOffice\Page dont elle hérite.
Dans la suite DisplayHome, ajoutez un troisième it :
->it('correspond visuellement au baseline', function () use ($modulesPsflowdemoHomePage) {
$modulesPsflowdemoHomePage->visualCheckpoint('home-with-block');
});
Signature complète :
visualCheckpoint(
string $name,
?string $selector = null,
float $threshold = 0.98,
bool $fullPage = true,
string $tag = 'auto'
): void
$name— identifiant logique du checkpoint (home-with-block,newsletter-submitted,product-page-mobile). Un nom par intention, comme pour unit.$selector— optionnel. Sans lui, capture toute la page ; avec lui, capture uniquement l’élément désigné.$threshold— score de similarité minimum (0.98 = 98 %).$fullPage— quand pas de sélecteur, choisir entre toute la page (défaut) ou seulement la fenêtre visible.$tag— variante du checkpoint ;'auto'par défaut résout à quelque chose commeauto-v8-1920x1080-en(voir plus bas).
Premier run : le baseline
Au tout premier lancement, aucun fichier de référence n’existe pour home-with-block. PrestaFlow prend la capture, la sauvegarde comme baseline, marque le checkpoint avec le statut baseline (visible dans le rapport visuel produit par --visual-report) — et n’échoue jamais.
Le fichier vit à la racine du projet, dans un dossier dédié :
./visual-baseline/home-with-block--auto-v8-1920x1080-en.png
1920x1080 est la taille de fenêtre par défaut du navigateur piloté par PrestaFlow (réglable avec PRESTAFLOW_WINDOW_SIZE_WIDTH et PRESTAFLOW_WINDOW_SIZE_HEIGHT), en la locale par défaut.
Trois choses à retenir :
- Commit ce dossier. Les baselines font partie de vos tests, exactement comme le code des Pages. Un baseline hors-repo, c’est un test qui ne survit pas au premier
git clone. - La nomenclature
{name}--{tag}.pngest stable. Ranger les fichiers vous-mêmes ne posera pas de problème tant que vous respectez cette convention. - Un baseline volontaire, pas accidentel. Regardez le PNG produit avant de committer. Sinon vous fixerez pour toujours un état non désiré (bandeau cookies mal fermé, spinner en cours, contenu partiellement chargé).
Runs suivants : la comparaison
Dès la seconde exécution, PrestaFlow prend une nouvelle capture (actual/), la compare au baseline via SapientPro\ImageComparator, calcule un score de similarité et écrit une image de diff (diff/), à chaque comparaison, que le checkpoint passe ou non.
Le score n’est pas une comparaison pixel à pixel. Chaque image est réduite à 8×8 pixels en niveaux de gris pour en tirer une empreinte de 64 bits (average hash) ; le score est la part de bits identiques, pondérée par la proximité des couleurs moyennes. Conséquence directe : un bit qui bascule coûte environ 1,6 point, et un changement très localisé sur une grande capture (un titre qui rétrécit sur une home entière) peut ne pas faire bouger le score du tout. L’image de diff, elle, est calculée pixel à pixel : elle montre en rouge tout ce qui a changé, même quand le score passe.
Si le score ≥ threshold → statut pass. Sinon → statut fail, avec les fichiers actual/ et diff/ attachés au résultat pour inspection.
L’arborescence finale d’un projet actif :
./
├─ visual-baseline/ ← les baselines (commit)
│ └─ home-with-block--auto-v8-1920x1080-en.png
└─ prestaflow/screens/
├─ actual/ ← capture du dernier run (temporaire)
│ └─ home-with-block--auto-v8-1920x1080-en.png
└─ diff/ ← diff du dernier run (temporaire)
└─ home-with-block--auto-v8-1920x1080-en.png
Choisir sa cible : full-page, viewport, élément
Le sélecteur définit le périmètre du checkpoint. Trois modes :
- Full-page (défaut) — utile pour surveiller la mise en page globale (structure, marges entre blocs, position footer).
- Viewport (
$fullPage = false, sans sélecteur) — la fenêtre visible seule. Plus stable qu’un full-page long qui bouge à chaque ajout de contenu. - Élément (
$selectorrenseigné) — le plus stable. Capture uniquement le bloc qui vous intéresse.
// Toute la page — surveille le layout global
$page->visualCheckpoint('home-full');
// Fenêtre visible — coupe à la hauteur du viewport
$page->visualCheckpoint('home-above-the-fold', fullPage: false);
// Élément — le plus robuste
$page->visualCheckpoint('home-newsletter-block', selector: '.block_newsletter');
Règle simple : le plus petit périmètre qui capture ce que vous voulez surveiller. Un checkpoint full-page peut échouer à cause d’un compteur de visiteurs dans le header — un checkpoint sur .block_newsletter seul, non. Et vu le mode de calcul du score, c’est aussi le seul moyen de voir un petit changement : sur un élément isolé, le bloc remplit toute l’empreinte.
Tags automatiques
Un même nom de checkpoint peut avoir plusieurs baselines si vous testez plusieurs contextes : PS 8 et PS 9, desktop et mobile, boutique française et anglaise. Sans mécanisme dédié, il faudrait nommer manuellement chaque variante.
Le tag 'auto' (défaut) résout à auto-v{major}-{width}x{height}-{locale}, donc un baseline distinct par combinaison :
visual-baseline/
├─ home-with-block--auto-v8-1920x1080-en.png
├─ home-with-block--auto-v8-1920x1080-fr.png
├─ home-with-block--auto-v9-1920x1080-en.png
└─ home-with-block--auto-v8-375x812-en.png ← mobile portrait
Le même appel visualCheckpoint('home-with-block') produit le bon fichier selon PRESTAFLOW_PS_VERSION, la taille de viewport (window.innerWidth × window.innerHeight, mesurée au moment de la capture), et PRESTAFLOW_LOCALE du run en cours. Aucune duplication côté scénario.
Vous pouvez passer un tag custom ('dark-mode', 'promotion-noel') quand vous voulez isoler manuellement une variante spécifique — le tag doit matcher [a-z0-9._-]+, sinon la lib lève une exception.
Threshold : le curseur qu’il faut caler
Le seuil par défaut est 0.98 — deux images sont considérées identiques si leur score atteint 98 % ou plus. Avec l’empreinte de 64 bits, cela tolère un bit différent, pas deux.
- Trop bas (0.90) : tolère jusqu’à six bits différents sur 64, et cache les vraies régressions (une couleur change de teinte, un padding gagne 20px et on ne voit rien).
- Trop haut (0.995) : le moindre bit qui bascule, ou une légère dérive de la couleur moyenne, fait échouer le checkpoint ; chaque run se retrouve à râler sur du bruit.
En pratique, 0.98 est un bon point de départ. On l’ajuste par exception, checkpoint par checkpoint :
// Bloc avec beaucoup de texte — on tolère deux bits d'écart
$page->visualCheckpoint('article-body', threshold: 0.96);
// Logo — aucune différence tolérée
$page->visualCheckpoint('brand-logo', selector: '.logo', threshold: 0.999);
Contenus dynamiques : les pièges à connaître
Trois classes de contenus font échouer un checkpoint sans qu’il y ait de vraie régression :
- Dates et heures — un
« Publié le 12 août 2026 »change chaque jour. - Compteurs et badges — panier, notifications, visiteurs en ligne.
- Contenus asynchrones — carrousel qui tourne au moment de la capture, spinner qui n’a pas fini, police web pas encore chargée.
Trois parades, dans l’ordre :
-
Restreindre le sélecteur au périmètre stable, à condition que la zone dynamique soit hors de ce périmètre —
.block_newsletterseul plutôt que la home entière. Sinon, la parade suivante. -
Masquer via CSS avant le checkpoint — un
evaluatequi appliquevisibility: hiddensur les zones dynamiques juste avant la capture (visibilitygarde la place de l’élément, donc la mise en page ne bouge pas) :$page->getPage()->evaluate("document.querySelectorAll('.visitor-counter, .blockcart .cart-products-count').forEach(function (e) { e.style.visibility = 'hidden'; })"); -
Baisser le threshold — solution paresseuse, à utiliser quand les deux précédentes ne suffisent pas.
Revue humaine côté plateforme
En local, valider un changement voulu comme nouveau baseline consiste à copier prestaflow/screens/actual/foo.png par-dessus visual-baseline/foo.png puis committer. Rapide, mais aveugle.
En CI (voir l’article 2), l’Action officielle (PrestaFlow/github-action@v2, input visual activé par défaut) synchronise avec prestaflow.io : elle télécharge les baselines de la plateforme dans visual-baseline/ avant le run, puis envoie les captures actual/ et diff/ après. La plateforme propose alors une revue humaine : côte-à-côte baseline vs actual, décision « changement voulu, promouvoir en baseline » ou « régression, à corriger ». Le nouveau baseline redescend automatiquement pour les runs suivants.
C’est là que la trilogie de la série montre son intérêt cumulé : la lib écrit le checkpoint, la CI l’exécute sur chaque commit, l’app / la plateforme arbitre les diffs.
Notes
Dans la Série PrestaFlow — article 25 sur 28