Retour au blog
Tests

PrestaFlow : tester un thème PrestaShop

PrestaEdit •
PrestaFlow : tester un thème PrestaShop

Décor

La série jusqu’ici s’est adressée à des développeurs de modules — quelqu’un qui touche à deux ou trois écrans précis et veut s’assurer qu’ils tiennent. L’intégrateur de thème a un jeu de contraintes complètement différent : il doit garantir que tous les gabarits front rendent correctement, sur plusieurs résolutions, dans plusieurs locales. Un thème qui plante sur la page catégorie en mobile portugais est cassé, même si les cinq autres pages passent.

Cette annexe traite ce cas — quels écrans systématiquement couvrir, comment structurer les suites, et comment tirer parti des tags automatiques de PrestaFlow pour multiplier les combinaisons sans multiplier le code.

Le carré des gabarits

Un thème PrestaShop doit rendre correctement au minimum les templates suivants — la check-list à laquelle tout thème publié doit se plier :

  • Home — bloc images, carrousel, mise en avant, footer
  • Catégorie — grille produits, filtres à facettes, pagination
  • Produit — galerie, ajout au panier, variantes, description, avis
  • Panier — vide, avec produit, avec code promo
  • Checkout — adresse, transporteur, paiement, confirmation
  • Compte client — login, register, mes commandes, mes adresses
  • CMS — page à propos, mentions légales
  • Contact — formulaire, avec/sans fichier joint
  • Recherche — résultats, vide
  • 404 — page introuvable
  • Maintenance — la page publique quand PS est en mode maintenance

C’est la base. Un thème complet ajoute ses templates spécifiques (page catalogue custom, page marque, wishlist si intégrée). Mais si un des templates ci-dessus casse, le thème n’est pas livrable.

Structure : une suite par gabarit

Contrairement aux modules où on organise les suites par cas métier (GuestCheckout, UpdateTitle), pour un thème on organise par écran testé. Ça donne une arborescence lisible qui reflète la check-list :

tests/prestaflow/
├─ Pages/v8/
│  └─ Theme/
│     ├─ Home/Page.php
│     ├─ Category/Page.php
│     ├─ Product/Page.php
│     ├─ CartEmpty/Page.php
│     ├─ Checkout/Page.php
│     └─ Cms/Page.php
└─ Suites/Theme/
   ├─ HomePage.php
   ├─ CategoryPage.php
   ├─ ProductPage.php
   ├─ CartEmpty.php
   ├─ CheckoutTunnel.php
   └─ CmsPage.php

Chaque suite fait typiquement 2 à 3 it :

namespace Tests\Suites\Theme;

use PrestaFlow\Library\Expects\Expect;
use PrestaFlow\Library\Tests\TestsSuite;

class ProductPage extends TestsSuite
{
    public function init()
    {
        $this->importPage('Theme\Product', domain: 'Tests');

        extract($this->pages);

        $this
        ->describe('Gabarit produit')
        ->it('la page charge sans erreur', function () use ($themeProductPage) {
            // 'product' → {index}-product.html ; PrestaShop redirige vers l'URL canonique.
            $themeProductPage->goToPage('product', ['index' => 1]);
            Expect::that($themeProductPage->isBroken())->isTheSameAs(false);
        })
        ->it('la structure DOM attendue est présente', function () use ($themeProductPage) {
            Expect::that($themeProductPage->hasCanonicalStructure())->isTheSameAs(true);
        })
        ->it('correspond visuellement au baseline', function () use ($themeProductPage) {
            $themeProductPage->visualCheckpoint('product-page');
        });
    }
}

Trois it, trois questions : ça charge ?, la structure est là ?, ça rend comme prévu ?. La première rattrape les erreurs serveur, la deuxième rattrape les régressions structurelles (un <main> disparu), la troisième rattrape les régressions purement visuelles (une couleur qui change).

Factoriser via un trait de sanity checks

Les deux premières assertions (isBroken, hasCanonicalStructure) reviennent identiquement sur tous les gabarits. C’est le cas d’école pour un trait sous Tests\Support\, comme vu dans l’annexe factoriser ses Pages.

<?php

namespace Tests\Support;

trait ThemeSanityChecks
{
    public function isBroken(): bool
    {
        // Une page cassée renvoie un 500, un titre "Fatal error", ou vide.
        $title = trim((string) $this->getPage()->evaluate('document.title')->getReturnValue());
        if ($title === '' || stripos($title, 'error') !== false) {
            return true;
        }

        return !$this->isVisible('body', 3000);
    }

    public function hasCanonicalStructure(): bool
    {
        // Header + main content + footer — le squelette qu'aucun thème PS ne peut zapper.
        return $this->isVisible('header, #header', 3000)
            && $this->isVisible('main, #main, [role="main"]', 3000)
            && $this->isVisible('footer, #footer', 3000);
    }
}

Chaque Page de gabarit le use, et les deux premiers it deviennent triviaux et identiques d’une suite à l’autre.

Responsive : trois breakpoints, un tag automatique

Le thème doit tenir sur mobile, tablette, desktop. La taille de viewport se règle sur l’onglet, avec setViewport() de chrome-php (la commande CDP Emulation.setDeviceMetricsOverride). Un détail impose l’ordre des appels : côté front, goToPage() ferme l’onglet et en ouvre un neuf à chaque appel. Un viewport réglé avant goToPage() est perdu avec l’ancien onglet. On navigue donc d’abord, on règle le viewport, puis on recharge la page dans le même onglet pour qu’elle se construise à la bonne largeur. Pattern d’itération dans une suite :

public function init()
{
    $this->importPage('Theme\Home', domain: 'Tests');
    extract($this->pages);

    $viewports = [
        ['w' => 375,  'h' => 812,  'label' => 'mobile'],
        ['w' => 768,  'h' => 1024, 'label' => 'tablet'],
        ['w' => 1280, 'h' => 800,  'label' => 'desktop'],
    ];

    $this->describe('Home responsive');

    foreach ($viewports as $vp) {
        $this->it("rend proprement en {$vp['label']}", function () use ($themeHomePage, $vp) {
            $themeHomePage->goToPage('home'); // recrée l'onglet

            $tab = $themeHomePage->getPage();
            $tab->setViewport($vp['w'], $vp['h'])->await();
            // Rechargement dans le même onglet : le réglage de viewport y survit.
            $tab->navigate($themeHomePage->getPageURL('home'))->waitForNavigation();

            $themeHomePage->visualCheckpoint('home');
        });
    }
}

Le tag 'auto' par défaut de visualCheckpoint intègre la taille de viewport dans le nom du fichier baseline : home--auto-v8-375x812-fr.png, home--auto-v8-1280x800-fr.png (la taille est mesurée dans la page, window.innerWidth × window.innerHeight, au moment de la capture). Aucune duplication du nom 'home' dans le code, PrestaFlow route sur le bon fichier selon le viewport courant.

Multi-locales en parallèle

Le thème doit aussi tenir dans les langues qu’il annonce supporter. Rien à changer côté suites — on boucle sur PRESTAFLOW_LOCALE avec le pattern shell de l’annexe scénarios multi-locales, sur une boutique où ces langues sont installées (l’image Flashlight n’installe que l’anglais) :

for locale in fr en de it es; do
    PRESTAFLOW_LOCALE=$locale composer prestaflow -- run ./tests/prestaflow --visual-report=reports/visual/$locale/index.html
done

Chaque run commence par vider prestaflow/ : les captures et résultats de la locale précédente disparaissent. Les baselines, dans visual-baseline/, ne sont pas touchées. Pour garder une trace de chaque locale, on écrit un rapport visuel par locale (--visual-report avec un chemin) : il embarque ses images, il reste lisible après les runs suivants.

Le tag automatique combine width × height × locale, donc un même visualCheckpoint('product-page') produit :

visual-baseline/
├─ product-page--auto-v8-1280x800-fr.png
├─ product-page--auto-v8-1280x800-en.png
├─ product-page--auto-v8-1280x800-de.png
├─ product-page--auto-v8-375x812-fr.png
├─ product-page--auto-v8-375x812-en.png
└─ ...

Pour les 6 gabarits de l’arborescence × 3 breakpoints × 5 locales = 90 baselines à approuver la première fois. Beaucoup, mais une fois figés, ces 90 fichiers gardent votre thème sous cloche.

Pièges spécifiques au thème

Contenus dynamiques qui cassent le rendu. Un thème doit gérer proprement une image produit manquante (fallback), une description vide (pas de bloc orphelin), un prix à 0 (affichage cohérent), un produit en rupture (bouton grisé au bon endroit). Chaque cas mérite au moins une suite dédiée avec fixture.

Hooks vides. Un thème rend souvent des zones prévues pour recevoir des modules (displayHome, displayReassurance, displayFooterBefore). Ces zones doivent rester propres même quand aucun module n’y branche rien — pas de div vide qui casse la mise en page, pas d’espace blanc bizarre. Test : boutique fraîche Flashlight sans modules ajoutés → tous les gabarits doivent rendre correctement.

Modules tiers déjà installés. À l’inverse, votre thème doit survivre à un blockreassurance, un ps_emailsubscription, un contactform — qui vont injecter leur markup dans vos hooks. Suite dédiée avec le module installé (voir annexe fixtures niveau 1 pour l’installation via init-script).

Itération dev : la boucle courte

Le pattern qui marche en pratique pour un intégrateur qui itère sur son thème :

  1. L’app PrestaFlow ouverte en second écran.
  2. PRESTAFLOW_HEADLESS=false — on voit Chrome.
  3. La suite de l’écran sur lequel on bosse (ex : Theme\HomePage) prête à cliquer.
  4. On retouche un .scss du thème, on rebuild, on relance la suite depuis l’app.
  5. On voit la nouvelle capture, on la compare au baseline, on approuve (on supprime la référence dans visual-baseline/, le run suivant la recrée à partir de la nouvelle capture) ou on corrige (retour au thème).

Cette boucle prend 10 à 30 secondes selon l’écran. C’est plus rapide que de recharger manuellement dans un navigateur et vérifier à l’œil, parce que le diff avec le baseline rattrape les régressions que l’œil manque.

Notes

Dans la Série PrestaFlow — article 13 sur 23