PrestaFlow : timeouts et attentes, le cookbook
Un sujet dispersé
Les annexes précédentes ont mentionné, chacune dans son coin, une manière d’attendre quelque chose :
- L’annexe debug range le wait manquant parmi les trois causes classiques de flake.
- L’annexe factoriser Pages donne un exemple de timeout explicite dans un sélecteur (
isVisible($sel, 8000)). - L’annexe tester un thème précise que les fonts custom peuvent être capturées avant d’être rendues.
Résultat : un lecteur qui traverse la série croise plusieurs mécaniques d’attente sans jamais voir la vue d’ensemble. Cette annexe range tout, avec une règle claire par cas. Toutes les méthodes citées existent dans la version publiée de la lib (1.7.1), sur toutes les Pages (elles sont définies dans CommonPage).
Le tableau de décision
Huit mécaniques, huit cas où chacune est le bon outil :
| Mécanique | Quand l’utiliser |
|---|---|
goToPage() / goToUrl() | Navigation que vous initiez. Elles attendent déjà l’événement load (30 s maximum) : rien à ajouter derrière. |
clickAndWaitReload($sel) / waitForPageReload() | Après un clic qui recharge la page (submit, redirection). Attend l’événement load du nouveau document, 10 s maximum. |
waitVisible($sel, $timeout) | Avant d’interagir avec un élément qui arrive plus tard (JS, AJAX). Lève une exception au timeout. |
waitHidden($sel, $timeout) | Attendre qu’un spinner ou un overlay soit retiré de la page. |
waitForText($text, $timeout, $sel) | Attendre un texte précis (message de succès, total recalculé). |
waitUntil($callable, $timeout) | Toute autre condition : état JS, fichier, API tierce. |
isVisible($sel, $timeoutMs) | Poser une question oui/non sans faire échouer le it. |
sleep($sec) PHP | En dernier recours, quand aucune condition observable n’existe. |
Chaque mécanique répond à une question différente. Les mélanger produit des scénarios lents ou instables. Les prendre au bon endroit produit des scénarios rapides et robustes.
La navigation attend déjà
goToPage() (FO et BO) et goToUrl() font navigate($url)->waitForNavigation() côté chrome-php : elles rendent la main quand l’événement load du document est tombé, ou lèvent une exception au bout de 30 secondes.
$page->goToPage('home');
// Le document est chargé : on peut lire le titre.
Expect::that($page->getPageTitle())->isNotEmpty();
Attention : load ne dit rien sur ce qui arrive après (appels AJAX, widgets injectés par JS, fonts chargées tardivement). Si votre assertion suivante dépend d’un de ces éléments, il faut une attente ciblée en plus.
clickAndWaitReload() — après un submit ou redirect
Sert quand ce n’est pas vous qui déclenchez la navigation, mais un click qui provoque un reload :
$page->clickAndWaitReload('#submit-form');
// Équivalent en deux temps :
$page->click('#submit-form');
$page->waitForPageReload();
Utile pour tous les formulaires PrestaShop qui soumettent et rechargent la page — login BO (c’est ce que fait login()), configuration de module, enregistrement d’une adresse. Sans ce wait, l’assertion suivante peut partir sur l’ancienne page qui n’a pas encore basculé.
Piège : waitForPageReload() n’échoue jamais. Au bout de 10 secondes sans rechargement, elle rend la main en silence. Faites-la suivre d’une attente qui, elle, échoue si l’état attendu n’arrive pas :
$page->clickAndWaitReload('button[name="submitPsflowdemo"]');
$page->waitVisible('.alert.alert-success', 5000);
Règle simple : si vous avez appelé goToPage() ou goToUrl(), il n’y a rien à ajouter. Si vous avez cliqué un bouton dont l’action est une navigation, c’est clickAndWaitReload().
waitVisible(), waitHidden(), waitForText() — attentes ciblées
Les plus fines des attentes. Elles interrogent la page toutes les 100 ms jusqu’au timeout (10 s par défaut), puis lèvent une TimeoutException : le it échoue avec un message explicite.
// Signatures (CommonPage, 1.7.1)
waitVisible($selector, int $timeout = 10000, ?string $message = null): void
waitHidden($selector, int $timeout = 10000, ?string $message = null): void
waitForText(string $text, int $timeout = 10000, string $selector = 'body', ?string $message = null): void
$page->waitVisible('#newsletter-success', 5000);
$page->waitHidden('.spinner', 5000);
$page->waitForText('Votre message a bien été envoyé', 8000, '#content');
C’est la parade au contenu injecté par JS avec un délai. Le timeout définit combien de temps on est prêt à attendre — 5 s pour un modal court, 8 s pour un chargement AJAX lourd.
Nuance importante : malgré son nom, waitVisible() attend que l’élément soit présent dans le DOM, pas qu’il soit affiché. Un élément déjà dans la page mais en display: none la satisfait immédiatement. Symétriquement, waitHidden() attend que l’élément soit retiré du DOM. Pour attendre un affichage réel, passez par waitUntil().
waitUntil() — n’importe quelle condition
La brique sur laquelle reposent les trois précédentes. Elle appelle votre closure toutes les 100 ms jusqu’à ce qu’elle renvoie true, sinon lève une TimeoutException avec votre message :
waitUntil(callable $condition, int $timeout = 10000, string $message = 'Condition was not met.'): void
Exemple : attendre que les web fonts soient chargées, ou qu’un élément soit réellement affiché.
$page->waitUntil(
fn () => $page->getPage()->evaluate('document.fonts.status')->getReturnValue() === 'loaded',
5000,
'Les web fonts ne sont pas chargées'
);
$page->waitUntil(
fn () => $page->getPage()->evaluate(
"(function () { var e = document.querySelector('#blockcart-modal'); return !!e && getComputedStyle(e).display !== 'none'; })()"
)->getReturnValue() === true,
5000,
'La modale panier ne s\'est pas affichée'
);
C’est l’équivalent, dans 1.7.1, d’une attente sur une condition JS.
isVisible($sel, $timeoutMs) — la question oui/non
isVisible() attend jusqu’à $timeoutMs (1 s par défaut) que le sélecteur soit présent dans le DOM, et retourne bool au lieu de lever une exception :
Expect::that($page->isVisible('#newsletter-success', 5000))->isTheSameAs(true);
À réserver aux cas où false est une réponse légitime (un bloc optionnel, un test qui vérifie une absence). Pour une simple attente, waitVisible() est plus lisible et son message d’échec plus parlant.
Piège récurrent : isVisible sans timeout (le défaut est 1 s) sur un contenu qui prend habituellement 3 s à apparaître. Vous obtenez un faux false, votre assertion tombe rouge, et vous cherchez pendant une heure un bug qui n’existe pas.
PRESTAFLOW_SCREENSHOT_DELAY — seulement pour les captures d’échec
Cette variable ne sert pas à attendre avant une étape. Elle fixe le nombre de secondes que la lib patiente avant de prendre la capture d’erreur, quand une assertion Expect échoue (la capture part dans prestaflow/screens/errors/). Défaut : 3 secondes.
PRESTAFLOW_SCREENSHOT_DELAY=5
Elle ne s’applique ni à visualCheckpoint(), ni aux étapes qui réussissent. Pour stabiliser une capture de régression visuelle, attendez une condition (waitVisible(), waitUntil() sur document.fonts.status) juste avant le checkpoint.
Coût : chaque assertion en échec coûte N secondes de plus. En debug local, où l’on relance souvent une suite rouge, PRESTAFLOW_SCREENSHOT_DELAY=0 raccourcit la boucle. La lib lit cette variable dans $_ENV : déclarez-la dans le .env du projet.
sleep($sec) — dernier recours assumé
Le vrai sleep() PHP est parfois inévitable. Cas typique : attendre qu’un système extérieur à la page ait fini son travail — une tâche planifiée, un webhook, un email à relever dans Mailpit — sans condition qu’on sache interroger.
$page->click('#submit-contact');
sleep(2); // la synchro vers l'ERP n'expose aucun état interrogeable
Règles à suivre :
- Justifiez-le en commentaire. Un
sleep(2)sans contexte donne envie à un mainteneur de le supprimer. Le commentaire évite la question. - Encapsulez-le si possible. Un
sleep(2)répété dans 15itdoit devenir un helper ($mailboxPage->waitForMessage($email, 10)) qui poll intelligemment plutôt que d’attendre bêtement. - N’en abusez pas. Un scénario truffé de
sleep(N)est un scénario qui va grossir en durée à chaque ajout et qui masque les problèmes de wait précis.
Poll manuel — pour les conditions custom hors Page
Dans une Page, waitUntil() fait le travail. Pour un helper qui n’est pas une Page (client d’API Mailpit, lecteur de fichiers), la boucle poll reste le pattern :
public function waitForMessage(string $recipient, int $timeoutSec = 10): ?array
{
$start = time();
while (time() - $start < $timeoutSec) {
$message = $this->findByRecipient($recipient);
if ($message !== null) {
return $message;
}
usleep(500_000); // 500ms
}
return null;
}
Trois choses à retenir : un timeout absolu (pour éviter l’infini), une granularité de poll raisonnable (500 ms est un bon défaut pour un appel réseau — 100 ms est trop agressif, 2 s est trop lent), un retour explicite du résultat (null = timeout).
C’est beaucoup plus fiable qu’un sleep(2) fixe : on rend la main dès que le message est là, et on attend plus longtemps quand le système est lent.
Cas concret : combiner plusieurs waits dans un it
Un it réel enchaîne souvent trois ou quatre types d’attente. Exemple avec le formulaire de contact de la boutique (URL contact-us d’une boutique en anglais) qui envoie un email :
->it('envoie le formulaire contact', function () use ($frontOfficeHomePage, $mailboxPage) {
// 1. Navigation initiale — goToPage attend déjà le chargement.
$frontOfficeHomePage->goToPage('contact-us');
// 2. Attente ciblée sur le formulaire.
$frontOfficeHomePage->waitVisible('textarea[name="message"]', 5000);
// 3. Interaction — pas de wait nécessaire, setValue est synchrone.
$frontOfficeHomePage->setValue('input[name="from"]', 'client@example.com');
$frontOfficeHomePage->setValue('textarea[name="message"]', 'test');
// 4. Le submit provoque un reload — clickAndWaitReload, puis preuve du succès.
$frontOfficeHomePage->clickAndWaitReload('input[name="submitMessage"]');
$frontOfficeHomePage->waitVisible('.alert.alert-success', 5000);
// 5. Attente de l'email — poll custom sur l'API Mailpit.
$message = $mailboxPage->waitForMessage('admin@prestashop.com', 10);
Expect::that($message)->isNotNull();
});
Chaque wait a son bon endroit. Rien de superflu, rien de manquant. Le scénario prend le temps qu’il doit prendre, ni plus, ni moins.
Interaction avec les autres annexes
- Debug — un scénario qui échoue avec une capture qui ressemble à “j’ai capturé au mauvais moment” est un problème de wait, pas un bug métier. Cette annexe donne le vocabulaire pour le diagnostiquer.
- Régressions visuelles (annexe à venir) — les baselines qui clignotent sont le plus souvent dues à un wait manquant avant
visualCheckpoint. UnwaitVisible()sur un élément stable, et au besoin unwaitUntil()surdocument.fonts.status, règlent l’essentiel des cas. - Tester un thème — les fonts custom, mentionnées comme piège, sont un cas typique où on attend sur “le titre final est rendu” plutôt que sur le seul chargement de la page.
Notes
Dans la Série PrestaFlow — article 24 sur 35