PrestaFlow : neutraliser les popups intempestifs (chat, promo, push)
Le cas résiduel
L’annexe cookies pré-injectés a montré comment neutraliser un bandeau RGPD quand il expose un cookie de consentement. On pré-règle le cookie, le bandeau ne s’affiche plus, tous les scénarios sont peinards.
Ça règle une catégorie de popups. Il en reste trois, chacune sans mécanisme cookie propre, qui empoisonnent les scénarios sur les boutiques un peu vécues :
- Le chat widget (Zopim, Crisp, Tawk, Intercom) — s’ouvre en bas à droite, sur toutes les pages, et masque parfois un bouton ou un sélecteur du parcours checkout.
- Le modal promo saisonnier — Halloween, Black Friday, Soldes d’été. S’ouvre au premier chargement de la home, réapparaît après suppression du localStorage.
- La notification push navigateur — la modale native Chrome « Autoriser les notifications ? », déclenchée par le site au premier scroll.
Ces trois cas ne se règlent pas avec un cookie. Cette annexe donne les patterns qui marchent.
Stratégie 1 : masquer via CSS pré-run
La plus rapide et la plus robuste. Vous connaissez le sélecteur du widget, vous cachez son container en injectant un <style> dans la page. Chrome le rend invisible, aucun clic accidentel, aucun scroll sur le sélecteur bloqué.
Un détail décide de l’endroit où placer l’injection : côté front, goToPage() et goToUrl() ferment l’onglet et en ouvrent un neuf à chaque appel (TestsSuite::recreatePage()). Tout ce qui a été posé sur l’onglet précédent disparaît avec lui, y compris un <style> injecté dans before(). L’injection doit donc suivre chaque goToPage(), et se faire en deux temps :
evaluate()pour le document déjà chargé ;addPreScript()(chrome-php,Page.addScriptToEvaluateOnNewDocument) pour les navigations suivantes dans le même onglet : clic sur un lien, submit,clickAndWaitReload().
Le pattern se range dans la Page utilisée par vos scénarios (ou dans un trait Tests\Support\HidesThirdPartyWidgets) :
public function goToPage($page = null, $params = null)
{
parent::goToPage($page, $params);
$this->hideThirdPartyWidgets();
}
public function hideThirdPartyWidgets(): void
{
$script = "
(function () {
var css = '#crisp-chatbox, .zopim, .tawk-min-container, #hs-messages-iframe-container,'
+ ' .promo-halloween-overlay, .newsletter-popup { display: none !important; }';
var inject = function () {
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
};
if (document.head) { inject(); } else { document.addEventListener('DOMContentLoaded', inject); }
})();
";
$tab = $this->getPage();
$tab->evaluate($script); // le document courant
$tab->addPreScript($script); // les prochains documents de cet onglet
}
Le addPreScript() vit aussi longtemps que l’onglet : le prochain goToPage() côté front repart d’un onglet vierge, et l’override le repose. Côté BO, goToPage() reste dans le même onglet : un second appel empile un script identique, sans autre effet qu’un <style> en double.
Avantages : simple, aucune interaction requise, ne consomme aucun temps par étape. Limites : suppose que vous connaissez tous les sélecteurs à masquer — un nouveau widget qui arrive n’est pas neutralisé automatiquement.
Stratégie 2 : fermer programmatiquement
Utile quand vous voulez tester l’interaction avec le widget (le chat doit être ouvrable, la newsletter doit se fermer proprement) sans qu’il pollue les autres scénarios.
Deux temps :
Une suite qui exerce le widget — un it unique dans une suite dédiée qui l’ouvre, vérifie qu’il fonctionne, le ferme, vérifie qu’il est fermé. Le widget est ici un sujet de test à part entière.
Toutes les autres Pages ferment le widget dans leur goToPage() overridé, sans le tester. Exemple pour un chat Crisp :
public function goToPage($page = null, $params = null)
{
parent::goToPage($page, $params);
// Ferme la bulle du chat si elle s'est réouverte au chargement.
$this->getPage()->evaluate("
if (window.\$crisp) { window.\$crisp.push(['do', 'chat:hide']); }
");
}
Avantages : préserve la testabilité du widget, honore son rôle métier réel. Limites : nécessite de connaître l’API programmatique du widget (chaque outil a la sienne).
Stratégie 3 : notification push navigateur
Cas particulier — c’est Chrome lui-même qui gère la modale « Autoriser les notifications ? », pas la page. Le CSS n’y peut rien.
La bonne approche est de refuser la permission au niveau du navigateur avant de démarrer, par un message CDP envoyé sur la connexion du navigateur (et non sur l’onglet, qui sera recréé). En fin de before() overridé dans la suite (cf. annexe fixtures, niveau 3), après parent::before() :
\PrestaFlow\Library\Tests\TestsSuite::getBrowser()->getConnection()->sendMessageSync(
new \HeadlessChromium\Communication\Message('Browser.setPermission', [
'permission' => ['name' => 'notifications'],
'setting' => 'denied',
])
);
La permission vaut pour tout le navigateur : elle survit aux onglets recréés par goToPage(). Le site lit Notification.permission === 'denied' et ne peut plus déclencher la demande. Effet analogue pour geolocation, camera, microphone — les autres permissions qui peuvent surgir.
Combiner les trois stratégies
Les popups d’un vrai site en production se cumulent. Une même Page a intérêt à appliquer les trois en cascade :
<?php
namespace Tests\Support;
use HeadlessChromium\Communication\Message;
use PrestaFlow\Library\Tests\TestsSuite;
trait NeutralizesUiNoise
{
public function goToPage($page = null, $params = null)
{
parent::goToPage($page, $params);
$this->neutralizeUiNoise();
}
public function neutralizeUiNoise(): void
{
// 1. Notifications : permission refusée au niveau du navigateur
// (elle survit aux onglets recréés par goToPage).
try {
TestsSuite::getBrowser()->getConnection()->sendMessageSync(
new Message('Browser.setPermission', [
'permission' => ['name' => 'notifications'],
'setting' => 'denied',
])
);
} catch (\Throwable $e) { /* best-effort */ }
// 2. CSS masquant les widgets : document courant + navigations suivantes dans cet onglet.
$hideCss = "
(function () {
var inject = function () {
var style = document.createElement('style');
style.textContent = '#crisp-chatbox, .zopim, .newsletter-popup { display: none !important; }';
document.head.appendChild(style);
};
if (document.head) { inject(); } else { document.addEventListener('DOMContentLoaded', inject); }
})();
";
$tab = $this->getPage();
$tab->evaluate($hideCss);
$tab->addPreScript($hideCss);
// 3. Fermeture programmatique des widgets à API
$tab->evaluate("
if (window.\$crisp) { window.\$crisp.push(['do', 'chat:hide']); }
");
}
}
Utilisé (use NeutralizesUiNoise;) dans une Page qui étend FrontOfficePage, le trait s’exécute après chaque goToPage() et rend la boutique testable sans avoir à gérer ces popups au cas par cas dans chaque scénario.
Interaction avec les autres annexes
- Cookies pré-injectés — pour tout popup qui expose un cookie de consentement, la solution cookie reste la plus propre (elle simule un utilisateur qui a déjà interagi, pas un utilisateur qui verrait un site cassé). Cette annexe est le complément pour tous les autres cas.
- Régressions visuelles — un widget masqué en CSS n’apparaît pas dans les baselines. Bien. Mais un widget qu’on n’a pas pensé à masquer produit un baseline qui contient le widget — la première fois qu’il change (nouvelle version du chat, promo Halloween → Noël), tous les checkpoints qui le capturent risquent de tomber d’un coup. Toujours masquer avant de figer les baselines.
- TDD — un bug qui apparaît uniquement quand un popup se superpose à un sélecteur est un cas parfait de scénario TDD : reproduisez le popup + le clic bloqué, fixez le CSS de votre site pour que le popup respecte le z-index, le scénario passe et reste comme garde-fou.
Notes
Dans la Série PrestaFlow — article 19 sur 23