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 { /* ... */ }
public function getBlockTitle(): string { /* ... */ }
}
Ç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.
Créez tests/prestaflow/Pages/v8/Modules/Psflowdemo/Configuration/Page.php :
<?php
namespace Tests\Pages\v8\Modules\Psflowdemo\Configuration;
use PrestaFlow\Library\Pages\v8\BackOffice\Page as BasePage;
class Page extends BasePage
{
public function defineSelectors(): array
{
return [
'titleInput' => 'input[name="PSFLOWDEMO_TITLE"]',
'submitButton' => 'button[name="submitPsflowdemo"]',
];
}
public function openConfiguration(string $boUrl): void
{
$this->goToUrl(rtrim($boUrl, '/') . '/index.php?controller=AdminModules&configure=psflowdemo');
}
public function fillTitle(string $title): void
{
$this->setValue($this->getSelector('titleInput'), $title);
}
public function save(): void
{
$this->click($this->getSelector('submitButton'));
$this->waitForPageReload();
}
}
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);
$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) {
$modulesPsflowdemoConfigurationPage->openConfiguration($_ENV['PRESTAFLOW_BO_URL']);
$modulesPsflowdemoConfigurationPage->fillTitle('Titre mis à jour par PrestaFlow');
$modulesPsflowdemoConfigurationPage->save();
})
->it('affiche le nouveau titre sur la home', function () use ($modulesPsflowdemoHomePage) {
$modulesPsflowdemoHomePage->goToPage('home');
Expect::that($modulesPsflowdemoHomePage->getBlockTitle())->contains('Titre mis à jour par PrestaFlow');
});
Plus une URL construite à la main, plus un sélecteur brut dans un it. 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 vu dans le monorepo lacitedesnuages/lacitedesnuages.be 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
{
$base = rtrim((string) ($_ENV['PRESTAFLOW_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 bourré 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 s’attend à trouver ses Pages dans un sous-dossier de version : Pages/v7/, Pages/v8/, Pages/v9/. Vos Pages custom suivent la même convention.
Quand le DOM du BO change entre deux versions majeures — le cas typique du passage à une nouvelle version — on duplique la Page dans le sous-dossier de la version cible :
tests/prestaflow/Pages/
├─ v7/Modules/Psflowdemo/Configuration/Page.php ← version legacy
└─ v8/Modules/Psflowdemo/Configuration/Page.php ← version reconstruite
Les deux classes ont la même API publique (openConfiguration, fillTitle, save) — ce qui change, ce sont les sélecteurs internes. Vos scénarios n’ont pas à savoir sur quelle version ils tournent : la lib route l’import selon la valeur de PRESTAFLOW_PS_VERSION.
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 14