PrestaFlow : factoriser ses Pages entre plusieurs suites
Rappel
Dans l’article d’introduction, nous avons créé une seule Page pour psflowdemo, avec deux méthodes :
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'
);
}
}
Ça suffit pour deux scénarios simples. Dès que le module grandit — plusieurs écrans testés, sélecteurs qui bougent, plusieurs versions PrestaShop supportées — cette Page unique ne tient plus. Voyons quatre techniques concrètes, dans l’ordre où on les rencontre.
Technique 1 : plusieurs Pages par module
Dans l’article d’introduction, nous avions volontairement laissé la page de configuration BO en primitives (goToUrl, setValue, click sur $backOfficeLoginPage). Il est temps de la sortir de là et d’en faire une Page à part entière.
On suit l’organisation du dépôt psflowdemo : la logique vit dans une classe commune à toutes les versions, tests/prestaflow/Pages/Common/Modules/Psflowdemo/Configuration/Page.php, qui étend directement BackOfficePage de la lib :
<?php
namespace Tests\Pages\Common\Modules\Psflowdemo\Configuration;
use PrestaFlow\Library\Pages\BackOfficePage;
class Page extends BackOfficePage
{
public function defineSelectors(): array
{
return [
'titleInput' => 'input[name="PSFLOWDEMO_TITLE"]',
'submitButton' => 'button[name="submitPsflowdemo"]',
'successAlert' => '.alert.alert-success',
'invalidTokenContinue' => 'a.btn-continue',
];
}
public function openConfiguration(string $boUrl): void
{
$this->goToUrl(rtrim($boUrl, '/') . '/index.php?controller=AdminModules&configure=psflowdemo');
// Sans token dans l'URL, PrestaShop affiche « Invalid security token »
if ($this->isVisible($this->getSelector('invalidTokenContinue'), 2000)) {
$this->click($this->getSelector('invalidTokenContinue'));
$this->waitForPageReload();
}
}
public function fillTitle(string $title): void
{
$this->setValue($this->getSelector('titleInput'), $title);
}
public function save(): void
{
$this->click($this->getSelector('submitButton'));
$this->waitForPageReload();
}
public function hasSuccessMessage(): bool
{
return $this->isVisible($this->getSelector('successAlert'), 5000);
}
}
importPage() ne charge jamais Common directement : il cherche la classe de la version en cours. Créez donc tests/prestaflow/Pages/v8/Modules/Psflowdemo/Configuration/Page.php, qui se contente d’hériter (et ses équivalents dans v7/ et v9/ si vous testez ces versions, voir la technique 4) :
<?php
namespace Tests\Pages\v8\Modules\Psflowdemo\Configuration;
use Tests\Pages\Common\Modules\Psflowdemo\Configuration\Page as BasePage;
class Page extends BasePage
{
}
La suite UpdateTitle de l’article 1 devient alors :
$this->importPage('BackOffice\Login');
$this->importPage('Modules\Psflowdemo\Configuration', domain: 'Tests');
$this->importPage('Modules\Psflowdemo\Home', domain: 'Tests');
extract($this->pages);
$boUrl = $this->getGlobals()['BO']['URL'];
$this
->describe('Modification du titre depuis le BO')
->it('se connecte au BO', function () use ($backOfficeLoginPage) {
$backOfficeLoginPage->goToPage('login');
$backOfficeLoginPage->login();
})
->it('met à jour le titre du bloc', function () use ($modulesPsflowdemoConfigurationPage, $boUrl) {
$modulesPsflowdemoConfigurationPage->openConfiguration($boUrl);
$modulesPsflowdemoConfigurationPage->fillTitle('Titre mis à jour par PrestaFlow');
$modulesPsflowdemoConfigurationPage->save();
Expect::that($modulesPsflowdemoConfigurationPage->hasSuccessMessage())->isTheSameAs(true);
})
->it('affiche le nouveau titre sur la home', function () use ($modulesPsflowdemoHomePage) {
$modulesPsflowdemoHomePage->goToPage('home');
Expect::that($modulesPsflowdemoHomePage->getBlockTitle())->contains('Titre mis à jour par PrestaFlow');
})
->it('remet le titre par défaut', function () use ($modulesPsflowdemoConfigurationPage, $boUrl) {
$modulesPsflowdemoConfigurationPage->openConfiguration($boUrl);
$modulesPsflowdemoConfigurationPage->fillTitle('Bienvenue sur notre boutique');
$modulesPsflowdemoConfigurationPage->save();
});
Plus une URL construite à la main, plus un sélecteur brut dans un it. Au passage, la Page expose hasSuccessMessage() : l’étape de mise à jour vérifie désormais que PrestaShop a bien accepté l’enregistrement, ce que la version en primitives ne faisait pas. Le scénario redevient lisible en français : connexion → mise à jour → vérification.
Technique 2 : factoriser via un trait
Certaines actions ne dépendent pas d’un écran mais du contexte (BO ou front) : construire une URL absolue front, gérer un modal cookie systématique, encoder une convention interne (nommage des références, format de date maison).
Répéter ces actions dans chaque Page mène à de la duplication qui pourrit lentement. Le pattern PrestaFlow, visible dans les projets qui l’utilisent en production, est de les extraire dans un trait sous Tests\Support\.
Cas concret, tiré d’un projet qui utilise PrestaFlow en production : un trait qui préfixe une URL relative avec PRESTAFLOW_FO_URL :
<?php
namespace Tests\Support;
trait FrontOfficeUrl
{
protected function foUrl(string $path): string
{
// PRESTAFLOW_FO_URL, tel que la lib l'a chargé
$base = rtrim((string) $this->getGlobals()['FO']['URL'], '/');
$path = ltrim($path, '/');
return $base . '/' . $path;
}
}
Puis, dans toute Page front qui en a besoin :
class Page extends BasePage
{
use \Tests\Support\FrontOfficeUrl;
public function goToHome(): void
{
$this->goToUrl($this->foUrl('/'));
}
}
Le trait vit à un endroit, chaque Page l’importe via use, et le jour où la logique d’URL évolue (préfixe de langue, sous-répertoire multiboutique) on ne modifie qu’un fichier.
Technique 3 : sélecteurs stables
Les sélecteurs qui rendent un scénario fragile sont toujours les mêmes :
- sélecteur trop générique — un
h1qui capture accidentellement le titre d’un modal RGPD tiers, - contenu injecté par JavaScript avec un délai imprévisible,
- sélecteur qui dépend du thème — cassé dès qu’on teste sur un thème custom.
Trois parades, dans l’ordre à essayer :
1. Sélecteur plus spécifique. .page-title-h1 au lieu de h1, #psflowdemo-block h3 au lieu de h3. Ça règle l’essentiel des cas et ne coûte rien.
2. Fallback JavaScript pour la donnée serveur. Quand le contenu HTML est rempli par JS et arrive avec un délai, on peut lire à la place l’équivalent rendu côté serveur :
public function getHeading(): string
{
// Le H1 est rempli par JS et parfois vide selon le timing.
// document.title est rendu côté serveur, stable.
return trim((string) $this->getPage()
->evaluate('document.title')
->getReturnValue());
}
3. Timeout explicite. isVisible($selector, 8000) laisse 8 secondes au sélecteur pour apparaître au lieu du timeout par défaut plus court. À utiliser sur les éléments dont on sait qu’ils prennent du temps, pas sur tous — sinon un scénario cassé prend une éternité à échouer.
Technique 4 : une Page par version PrestaShop
La lib PrestaFlow range ses Pages dans un sous-dossier de version : Pages/v7/, Pages/v8/, Pages/v9/. Vos Pages custom suivent la même convention : importPage() construit le nom de la classe à partir de PRESTAFLOW_PS_VERSION (v7 pour 1.7, v8, v9).
Il n’y a pas de repli d’une version sur l’autre : si la classe v9 n’existe pas, un run en 9.0 s’arrête sur une erreur de classe introuvable au lieu de réutiliser celle de v8. Chaque version supportée doit donc avoir sa classe, même vide. C’est ce que fait psflowdemo, qui tourne sur 1.7.8, 8.1 et 9.0 :
tests/prestaflow/Pages/
├─ Common/Modules/Psflowdemo/Configuration/Page.php ← toute la logique
├─ v7/Modules/Psflowdemo/Configuration/Page.php ← hérite de Common
├─ v8/Modules/Psflowdemo/Configuration/Page.php ← hérite de Common
└─ v9/Modules/Psflowdemo/Configuration/Page.php ← hérite de Common
Les classes de version ont la même API publique (openConfiguration, fillTitle, save, hasSuccessMessage). Tant que le module rend le même formulaire partout, elles restent vides. Le jour où le DOM diverge sur une version, seule sa classe change. Vos scénarios n’ont pas à savoir sur quelle version ils tournent : c’est PRESTAFLOW_PS_VERSION qui décide quelle classe est chargée.
En résumé
Quatre leviers, appliqués dans l’ordre où le module les demande :
- Un dossier par écran, une Page par dossier dès qu’un deuxième écran devient testable.
- Un trait sous
Tests\Support\dès qu’une action est réutilisée entre deux Pages. - Sélecteur spécifique, fallback JS, timeout ciblé dès qu’une étape devient flaky.
- Sous-dossier par version PS dès qu’on cible plus d’une version majeure.
Vos scénarios restent alors des phrases en français qui décrivent le comportement métier, sans un sélecteur en vue.
Dans la Série PrestaFlow — article 7 sur 23