Écrire son premier scénario de test avec PrestaFlow
Préambule
Dans un précédent article, nous avons vu comment analyser statiquement le code d’un module avec PHPStan, sur plusieurs versions de PrestaShop, depuis un workflow GitHub.
Ce type de vérification attrape beaucoup de problèmes en amont. Mais il ne couvre qu’une seule dimension : est-ce que le code compile et respecte les contrats des classes de PrestaShop ?
Il reste une question à laquelle l’analyse statique ne répond pas : est-ce que le module fait bien ce qu’il est censé faire ?
Concrètement : le hook s’affiche-t-il vraiment sur la home ? La page de configuration BO enregistre-t-elle le champ ? La modification est-elle bien reflétée côté front ? Ces questions ne se posent qu’au moment de faire tourner le module dans un navigateur.
Réaliser ces vérifications à la main, sur chaque version de PrestaShop, à chaque changement de code, est fastidieux — c’est très exactement le même argumentaire que pour PHPStan, mais côté fonctionnel.
Voyons comment outiller cette vérification avec PrestaFlow.
PrestaFlow, c’est quoi ?
PrestaFlow est une librairie PHP open-source qui permet d’écrire des tests end-to-end (E2E) pour PrestaShop.
Un test E2E, ici, veut dire : PrestaFlow pilote un vrai navigateur (Chrome, sans interface), navigue dans votre boutique comme le ferait un client — ou un administrateur — et vérifie que ce qu’il voit correspond à ce que vous attendez.
Ce qui la distingue des outils généralistes comme Playwright ou Cypress :
- Elle est écrite en PHP. Pas besoin d’introduire une stack JavaScript ou Python dans un projet PS. Vos tests vivent à côté de votre module, dans le langage du projet.
- Elle connaît PrestaShop. Les pages front standard (produit, panier, checkout) et les principales pages BO (login, dashboard, modules) sont déjà modélisées. Vous n’écrivez pas les sélecteurs CSS du panier, ils sont fournis.
- Elle est livrée avec des scénarios prêts à l’emploi. Parcours d’achat, création de produit, gestion de commande… On peut en lancer un immédiatement, sans écrire une ligne.
Le tout est documenté sur prestaflow.io/docs.
Le module fil rouge
Pour illustrer l’article, nous allons tester un module minimaliste, psflowdemo, dont le seul rôle est :
- d’afficher un bloc sur la page d’accueil, via le hook
displayHome, - avec un titre configurable depuis une page BO.
C’est le plus petit module possible qui expose à la fois du front, du BO et de la persistance de configuration — les trois choses qu’on veut savoir tester.
Installation
Un détail compte avant de lancer la moindre commande : PrestaShop inclut le fichier modules/<module>/vendor/autoload.php de chaque module installé. Or la librairie PrestaFlow apporte ses propres composants Symfony 6, qui entrent en conflit avec ceux de la boutique et la font planter. Un module qui embarque ses tests range donc ses dépendances de développement ailleurs, dans vendor-dev/.
Ajoutez ces deux blocs au composer.json du module :
"config": {
"vendor-dir": "vendor-dev",
"process-timeout": 0
},
"scripts": {
"prestaflow": "./vendor-dev/prestaflow/php-library/bin/prestaflow"
}
Le script prestaflow donne un raccourci vers la CLI : tout ce qui suit -- lui est transmis, par exemple composer prestaflow -- run ./tests/prestaflow. process-timeout: 0 est nécessaire, car Composer coupe un script au bout de 300 secondes, et une suite E2E complète peut durer plus longtemps.
Puis, depuis la racine de votre module :
composer require --dev prestaflow/php-library
Ajoutez enfin vendor-dev/ au .gitignore du module, ainsi que prestaflow/, le dossier où la CLI écrit ses sorties.
PrestaFlow s’appuie sur chrome-php/chrome. Vous aurez besoin d’un binaire Chrome (ou Chromium) accessible sur la machine qui exécute les tests. En local, l’installation standard de Chrome suffit.
Créez ensuite un fichier .env à la racine du module, à côté du composer.json :
PRESTAFLOW_PS_VERSION=8.1.0
PRESTAFLOW_LOCALE=fr
PRESTAFLOW_FO_URL=https://localhost/
PRESTAFLOW_BO_URL=https://localhost/admin-dev/
PRESTAFLOW_BO_EMAIL="demo@prestashop.com"
PRESTAFLOW_BO_PASSWD="Correct Horse Battery Staple"
PRESTAFLOW_HEADLESS=true
PRESTAFLOW_DEBUG=false
Ces valeurs correspondent à une boutique de démonstration en français. Le dépôt psflowdemo fournit le même contenu dans un .env.example à copier : le .env, propre à chaque machine, reste hors de git.
Structure de dossiers cible pour la suite de l’article :
psflowdemo/
├─ psflowdemo.php
├─ composer.json
├─ .env
├─ vendor-dev/ (dépendances de dev, ignoré par git)
└─ tests/
└─ prestaflow/
├─ Pages/
│ └─ v8/
│ └─ Modules/
│ └─ Psflowdemo/
│ └─ Home/
│ └─ Page.php
└─ Suites/
Lancer un scénario fourni
Avant d’écrire quoi que ce soit, vérifions que l’installation fonctionne en lançant un scénario livré avec la librairie : GuestCheckout, un parcours d’achat en tant qu’invité.
Créez le fichier tests/prestaflow/Suites/Checkout.php :
<?php
namespace Tests\Suites;
use PrestaFlow\Library\Scenarios\GuestCheckout;
use PrestaFlow\Library\Tests\TestsSuite;
class Checkout extends TestsSuite
{
public function init()
{
$this
->describe('Parcours d\'achat invité')
->scenario(GuestCheckout::class, [
'locale' => $_ENV['PRESTAFLOW_LOCALE'] ?? 'fr',
]);
}
}
Puis, depuis la racine du module :
composer prestaflow -- run ./tests/prestaflow
PrestaFlow ouvre Chrome en arrière-plan, va sur la page produit, ajoute au panier, remplit l’adresse, choisit la livraison et le paiement, puis vérifie que la page de confirmation est bien atteinte. Le déroulé s’affiche dans le terminal, étape par étape, avec un bilan final.
Si tout est vert, l’installation est bonne. On peut passer à l’écriture de son propre scénario.
Premier scénario : vérifier le rendu sur la home
Objectif : vérifier que psflowdemo affiche bien son bloc, avec le titre par défaut, sur la page d’accueil.
Le pattern PrestaFlow, calqué sur ce qu’on retrouve dans les projets qui l’utilisent en production, tient en deux fichiers :
- une classe
Pagequi décrit où trouver les choses (sélecteurs) et expose des méthodes sémantiques (hasBlock,getBlockTitle), - une classe
TestsSuitequi décrit quoi vérifier, en n’appelant que ces méthodes sémantiques.
L’intérêt de séparer les deux : quand un sélecteur change (nouvelle version PS, refonte du thème), on ne touche qu’à la Page. Les scénarios restent lisibles et n’ont aucune connaissance du DOM.
La Page
Créez tests/prestaflow/Pages/v8/Modules/Psflowdemo/Home/Page.php :
<?php
namespace Tests\Pages\v8\Modules\Psflowdemo\Home;
use PrestaFlow\Library\Pages\v8\FrontOffice\Page as BasePage;
class Page extends BasePage
{
public function defineSelectors(): array
{
return [
'block' => '#psflowdemo-block',
'title' => '#psflowdemo-block h3',
];
}
public function hasBlock(): bool
{
return $this->isVisible($this->getSelector('block'), 5000);
}
public function getBlockTitle(): string
{
return html_entity_decode(
(string) $this->getTextContent($this->getSelector('title')),
ENT_QUOTES | ENT_HTML5,
'UTF-8'
);
}
}
Quatre points à retenir sur cette Page :
- Elle étend
FrontOffice\Pagede la version 8. Le namespacev8de la lib PrestaFlow apporte tout ce qui est spécifique à cette version majeure de PS. defineSelectors()centralise les sélecteurs CSS. On les récupère ensuite via$this->getSelector('block').- Les méthodes publiques exposent une API sémantique. Un scénario qui lit ce code comprend ce qui est testé sans avoir à connaître le DOM.
getTextContent()renvoie le texte avec ses entités HTML brutes (<plutôt que<), etfalsesi l’élément n’apparaît pas :getBlockTitle()convertit en chaîne et décode, pour que les scénarios comparent le titre tel qu’il s’affiche.
La suite
Créez tests/prestaflow/Suites/DisplayHome.php :
<?php
namespace Tests\Suites;
use PrestaFlow\Library\Expects\Expect;
use PrestaFlow\Library\Tests\TestsSuite;
class DisplayHome extends TestsSuite
{
public function init()
{
$this->importPage('Modules\Psflowdemo\Home', domain: 'Tests');
extract($this->pages);
$this
->describe('Bloc psflowdemo sur la home')
->it('affiche le bloc', function () use ($modulesPsflowdemoHomePage) {
$modulesPsflowdemoHomePage->goToPage('home');
Expect::that($modulesPsflowdemoHomePage->hasBlock())
->isTheSameAs(true);
})
->it('affiche le titre par défaut', function () use ($modulesPsflowdemoHomePage) {
Expect::that($modulesPsflowdemoHomePage->getBlockTitle())
->contains('Bienvenue');
});
}
}
À noter :
importPage('Modules\Psflowdemo\Home', domain: 'Tests')— le paramètredomainindique à PrestaFlow que la Page n’est pas dans la librairie mais dans votre namespaceTests(défini plus bas dans lecomposer.json).- La variable exposée suit le nommage camelCase du chemin :
Modules\Psflowdemo\Homedevient$modulesPsflowdemoHomePage. describe()->it()— la syntaxe est empruntée à Jest / Mocha. Chaqueitest une étape, qui échoue ou réussit indépendamment.Expect::that(...)->isTheSameAs(...) / ->contains(...)— les assertions. La liste complète est dans la classesrc/Expects/Expect.phpde la librairie.
Pour que le namespace Tests\ soit résolu, ajoutez dans le composer.json de votre module, à côté des blocs config et scripts :
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/prestaflow/"
}
}
Puis relancez composer dump-autoload.
Enfin, exécutez :
composer prestaflow -- run ./tests/prestaflow
Les deux étapes doivent passer si votre hook displayHome renvoie bien un bloc avec l’id psflowdemo-block.
Deuxième scénario : modifier la configuration depuis le BO
Objectif : se connecter au BO, ouvrir la page de configuration de psflowdemo, changer le titre, revenir sur la home, vérifier que le nouveau titre s’affiche.
C’est un scénario un peu plus intéressant : il enchaîne BO → front et vérifie la persistance.
Ce scénario met en jeu deux pages fournies par la librairie (BackOffice\Login, qui expose une méthode login() prête à l’emploi, et BackOffice\Dashboard, qui sert à vérifier que la connexion a réussi) et notre page custom (celle du bloc sur la home, que nous venons d’écrire).
Pour la page de configuration du module elle-même, on pourrait — et devrait, en régime de croisière — écrire une seconde Page custom. Pour rester dans le format de cette introduction, nous allons plutôt piloter cette page au niveau primitive (goToUrl, setValue, click) depuis la page BO déjà connectée. C’est un pattern utile à connaître : dès qu’on interagit avec une page one-shot, non factorisée, on descend aux primitives sans se sentir coupable.
Une particularité de PrestaShop à connaître : une URL de back office sans jeton de sécurité (token) affiche d’abord une page « Invalid security token », avec un lien pour continuer quand même. Le scénario clique sur ce lien (a.btn-continue) s’il apparaît.
Créez tests/prestaflow/Suites/UpdateTitle.php :
<?php
namespace Tests\Suites;
use PrestaFlow\Library\Expects\Expect;
use PrestaFlow\Library\Tests\TestsSuite;
class UpdateTitle extends TestsSuite
{
public function init()
{
$this->importPage('BackOffice\Login');
$this->importPage('BackOffice\Dashboard');
$this->importPage('Modules\Psflowdemo\Home', domain: 'Tests');
extract($this->pages);
$newTitle = 'Titre mis à jour par PrestaFlow';
$defaultTitle = 'Bienvenue sur notre boutique';
// PRESTAFLOW_BO_URL, lu depuis le .env, toujours terminé par un /
$configUrl = $this->getGlobals()['BO']['URL']
. 'index.php?controller=AdminModules&configure=psflowdemo';
// Adaptez l'URL et les sélecteurs à la page de config de votre module
$updateTitle = function (string $title) use ($backOfficeLoginPage, $configUrl) {
$backOfficeLoginPage->goToUrl($configUrl);
// Sans token dans l'URL, PrestaShop demande une confirmation
if ($backOfficeLoginPage->isVisible('a.btn-continue', 2000)) {
$backOfficeLoginPage->click('a.btn-continue');
$backOfficeLoginPage->waitForPageReload();
}
$backOfficeLoginPage->setValue('input[name="PSFLOWDEMO_TITLE"]', $title);
$backOfficeLoginPage->click('button[name="submitPsflowdemo"]');
$backOfficeLoginPage->waitForPageReload();
};
$this
->describe('Modification du titre depuis le BO')
->it('se connecte au BO', function () use ($backOfficeLoginPage, $backOfficeDashboardPage) {
$backOfficeLoginPage->goToPage('login');
$backOfficeLoginPage->login();
// Connexion réussie : on atterrit sur le tableau de bord
Expect::that($backOfficeDashboardPage->getPageTitle())
->contains($backOfficeDashboardPage->pageTitle());
})
->it('met à jour le titre du bloc', function () use ($updateTitle, $newTitle) {
$updateTitle($newTitle);
})
->it('affiche le nouveau titre sur la home', function () use ($modulesPsflowdemoHomePage, $newTitle) {
$modulesPsflowdemoHomePage->goToPage('home');
Expect::that($modulesPsflowdemoHomePage->getBlockTitle())
->contains($newTitle);
})
->it('remet le titre par défaut', function () use ($updateTitle, $defaultTitle) {
$updateTitle($defaultTitle);
});
}
}
Quelques remarques sur ce code :
- La preuve de connexion. Après
login(), le navigateur doit être sur le tableau de bord.getPageTitle()lit le titre du document, etpageTitle()renvoie le titre attendu de la page Dashboard (« Dashboard », traduit selon la locale : « Tableau de bord » en français). Si les identifiants sont faux, on reste sur la page de connexion et l’assertion échoue. - L’URL du BO vient de
$this->getGlobals()['BO']['URL']: la suite y trouve la valeur dePRESTAFLOW_BO_URL, normalisée avec un/final. - La dernière étape remet le titre par défaut. Sans elle, un second passage échouerait : les suites s’exécutent dans l’ordre alphabétique des fichiers, et
DisplayHometrouverait encore le titre modifié parUpdateTitlelors du passage précédent.
Notez que la troisième étape réutilise la Page custom créée pour le premier scénario : getBlockTitle(). C’est tout l’intérêt d’avoir factorisé — la vérification finale reste triviale à lire et on ne duplique aucun sélecteur.
Relancez la commande run. Passez PRESTAFLOW_HEADLESS=false le temps de la démo pour voir Chrome enchaîner : login BO, ouverture de la config, saisie, sauvegarde, retour front, vérification.
Lire le résultat
Il n’y a pas de rapport HTML à ouvrir : le résultat d’un run s’affiche dans le terminal, et les fichiers produits atterrissent dans ./prestaflow/, à la racine du module. Deux choses utiles à savoir y trouver :
- La liste des étapes de chaque suite avec leur statut, puis le bilan : suites et tests passés, en échec, ignorés, durée totale.
- Les captures d’écran d’échec, dans
prestaflow/screens/errors/. PrestaFlow n’en prend qu’au moment où une assertionExpectéchoue : pas de capture pour une étape qui passe.
Quand un it échoue sur une assertion, la capture pleine page montre l’état exact de la page à ce moment. Souvent, ça suffit pour identifier le problème sans avoir à rejouer localement.
Pour exploiter le résultat dans un autre outil, composer prestaflow -- run ./tests/prestaflow -o json --file écrit prestaflow/results.json. En v1.7.1, ce fichier ne contient que la dernière suite exécutée.
Notes
Nous avons vu le pattern minimal Page + Suite avec une seule Page custom. L’annexe Factoriser ses Pages entre plusieurs suites reprend le sujet en profondeur : couvrir plusieurs pages d’un module, gérer les sélecteurs instables (contenu injecté par JS, modales tierces qui polluent le DOM), factoriser entre suites via un use Trait, et adapter ses Pages entre PS 1.7, 8 et 9.
D’ici là, vous avez de quoi tester votre module en local, sur votre version de PrestaShop du moment. C’est déjà un filet de sécurité qui rattrape la classe de bugs “j’ai touché un template et j’ai cassé le rendu sans m’en rendre compte” — et cette classe est plus large qu’on ne le pense.
Dans la Série PrestaFlow — article 1 sur 23