Charger une extension dans Puppeteer et Playwright
Une extension non empaquetée exige un contexte persistant et deux flags appariés, et elle peut toujours ne pas charger. Une exécution mesurée montre pourquoi il faut vérifier.
BotBrowser Team
Vous voulez la documentation structurée pour Plateforme ?
Cet article fait partie de la bibliothèque éditoriale. Pour les étapes de configuration, la référence et les mises à jour continues, passez directement à la section docs.
Charger une extension non empaquetée depuis un lancement automatisé ressemble à un problème de deux flags et se comporte comme un problème de conditions de lancement. BotBrowser fournit un profil déclaré qui maintient cohérentes entre elles les surfaces qu’une page peut lire d’un lancement à l’autre, y compris les réglages au niveau du lancement. Il ne rend pas un build capable d’accepter une extension qu’il refuse de charger, et il ne garantit pas qu’un service worker reste actif. Les conditions dont une extension chargée a besoin s’énumèrent, et une exécution mesurée les a toutes montrées apparemment réunies sans qu’aucune extension n’apparaisse.
Résumé
- Une extension non empaquetée a besoin d’un contexte persistant, parce qu’un contexte non persistant ne peut pas conserver l’état de l’extension entre deux pages.
- Les deux flags sont faits pour être passés ensemble : l’un nomme ce qui est autorisé, l’autre exclut tout le reste.
- Passer seulement le flag de chargement laisse en place l’ensemble d’extensions par défaut, ce qui change ce que voit la page.
- Un service worker MV3 n’est pas un objet permanent, donc un worker absent ne prouve pas que l’extension n’a pas chargé.
- Dans une exécution mesurée, les deux flags étaient passés sous
headless=new, le navigateur est sorti proprement et le profil listait zéro extension. - La même chose s’est produite en ancien mode headless et en mode avec interface sur le même hôte, donc l’échec venait de l’environnement plutôt que du mode.
- La réponse utile consiste à affirmer que l’extension est présente au démarrage, et à faire échouer l’exécution quand elle ne l’est pas.
- Un test qui tourne en silence sans son extension réussit pour la mauvaise raison, ce qui est pire qu’un test qui échoue.
Sommaire
- Les conditions de lancement, et à quoi ressemble un échec mesuré
- Le service worker n’est pas permanent, et la liste de contrôle
- La liste de contrôle comme fixture
- Quand headless n’est pas la variable
- Conclusion pratique
Les conditions de lancement, et à quoi ressemble un échec mesuré
Trois conditions décident si une extension non empaquetée a une chance, et elles sont indépendantes. La première est le type de contexte. Une extension vit dans le profil du navigateur, donc un contexte qui jette son profil à la fin n’a nulle part où la garder. Un contexte persistant avec un --user-data-dir explicite est la seule forme qui tienne, et chaque outil l’exprime à sa façon : Playwright prend le répertoire comme premier argument de chromium.launchPersistentContext, tandis que Puppeteer atteint le même profil par les options de lancement et les arguments de ligne de commande. Les arguments de lancement sont traités dans pièges de lancement et de profil avec Puppeteer, y compris la surcharge de viewport qui l’emporte sur l’affichage d’un profil.
persistent context -> profile on disk; extension state can persist
non-persistent -> fresh profile per launch; nothing to hold the extension
La deuxième est la paire de flags. Un flag nomme le dossier de l’extension à charger, et l’autre nomme le seul ensemble d’extensions autorisé à tourner. Passer le flag de chargement seul ajoute l’extension à tout ce que le profil contient déjà, ce qui correspond rarement à ce que veut un test ; passer les deux rend l’ensemble d’extensions du navigateur lancé égal à celui qui était prévu.
--disable-extensions-except=/path/to/ext
--load-extension=/path/to/ext
La troisième condition est que le build du navigateur accepte le dossier. Ce n’est pas une formalité : un build où le chargement d’extensions est restreint, un manifest que le build juge invalide et un chemin que le navigateur ne peut pas lire produisent tous le même résultat observable, à savoir aucune extension.
Les trois conditions ci-dessus sont faciles à énoncer et faciles à croire réunies. Une exécution a été enregistrée pour vérifier cette croyance, avec une extension MV3 minimale dotée d’un service worker d’arrière-plan et les deux flags passés ensemble.
persistent_headless_new: headless=true persistent=true extLoaded=false sw_count=0 id=(none)
persistent_headed: headless=false persistent=true extLoaded=false sw_count=0 id=(none)
Le profil a ensuite été lu après la sortie du navigateur, ce qui est une vérification indépendante du minutage à l’intérieur d’un navigateur en cours d’exécution :
--headless=new: exit=0 prefs_extension_ids=0
--headless: exit=0 prefs_extension_ids=0
(headed): exit=0 prefs_extension_ids=0
Trois modes, trois sorties propres, zéro extension dans le profil. Ce résultat mérite une lecture attentive, parce qu’il dit quelque chose de plus étroit que « les extensions ne fonctionnent pas ». Il dit que sur cet hôte, avec ce build et ce dossier, le passage des deux flags documentés n’a pas installé l’extension, et que le navigateur n’a signalé aucune erreur à ce sujet.
La conséquence pratique est qu’un test qui suppose les flags efficaces tournera sans son extension et réussira quand même. extLoaded=false est le champ qui enregistre le chargement, et la lecture du profil après la sortie indique prefs_extension_ids=0 ; le compteur de workers vaut sw_count=0, mais cette valeur seule ne sépare pas un chargement raté d’un worker jamais démarré. Chaque assertion à l’intérieur de l’extension est sautée, la page testée voit l’environnement par défaut et l’exécution annonce un succès. C’est une issue pire qu’un échec visible, parce que rien dans la sortie n’invite à y regarder deux fois.
Le correctif est petit et mécanique, mais les deux outils exposent des preuves différentes. Puppeteer énumère ce qui est installé, donc l’assertion porte sur cette liste. Playwright n’a pas de méthode pour l’ensemble installé : le même fait se lit dans le profil après la sortie du navigateur, où extensions.settings est indexé par identifiant d’extension.
puppeteer: installed = await browser.extensions() // Map: id -> {name, version}
playwright: JSON.parse(readFileSync(join(userDataDir, 'Default', 'Secure Preferences')))
.extensions.settings // keys are extension ids
assert installed.size > 0, "no extension installed; check flags, manifest and build"
Une vérification de worker répond à une autre question et ne peut pas remplacer cette assertion. Attendre context.serviceWorkers() avec Playwright ou browser.waitForTarget avec un prédicat service_worker avec Puppeteer confirme que le contexte d’arrière-plan a démarré ; une attente bornée avec un budget comme 30_000 ms convient tant que son message d’échec dit que le worker n’a pas démarré, et non que l’extension n’a pas été chargée.
Le service worker n’est pas permanent, et la liste de contrôle
Une deuxième raison fait qu’une vérification fondée sur la liste des workers peut induire en erreur, et c’est une propriété de la plateforme d’extensions plutôt que de l’outil d’automatisation. Un service worker d’arrière-plan MV3 démarre quand quelque chose en a besoin et s’arrête quand il est inactif. Un worker absent peut être un worker qui n’a jamais démarré ou un worker déjà suspendu.
absent worker + extension installed -> not started, or suspended
absent worker + extension not present -> load failed
Les deux se ressemblent à l’identique sur une seule interrogation et veulent dire des choses différentes, et c’est pourquoi la vérification du chargement porte sur l’ensemble installé tandis que celle du worker reste une assertion de vivacité distincte avec son propre message. Appeler browser.extensions() avec Puppeteer distingue les deux cas Lire le profil après la sortie, ou demander au navigateur ses extensions installées, les distingue ; interroger la liste des workers pendant un nombre fixe de secondes n’y parvient pas.
Le cycle de vie change aussi la manière dont un test doit attendre un worker. Une pause fixe assez longue pour une machine rapide sera trop courte sur une machine lente, et un test qui attend l’apparition du worker puis abandonne après un intervalle fixe mesure la machine plutôt que l’extension. Attendre la condition avec un budget borné comme 30_000 ms, et signaler le budget quand il expire, produit un échec qui nomme sa cause.
Une quatrième condition mérite son propre paragraphe parce que c’est celle qui change avec l’outil plutôt qu’avec le navigateur. Puppeteer et Playwright exposent tous deux une liste d’arguments au moment du lancement et acceptent les mêmes deux flags, mais le contexte qu’ils construisent par défaut diffère. Puppeteer lance un navigateur et crée une page dans son contexte par défaut ; Playwright lance un navigateur et laisse l’appelant créer les contextes. L’extension vit au niveau du navigateur dans les deux cas, ce qui la rend visible depuis un contexte persistant et invisible depuis un contexte créé par la voie non persistante.
puppeteer.launch({ args }) -> browser-level flags; page may still be transient
chromium.launchPersistentContext(dir) -> profile on disk; extension state survives
browser.newContext() -> fresh profile; extension state absent
Cette différence explique que deux équipes puissent rapporter des résultats opposés avec les mêmes flags documentés. L’une les passe à un lancement dont le profil persiste, l’autre les passe à un lancement puis crée un contexte qui jette le profil. Aucune ne se trompe sur ce qu’elle a observé, et le flag seul n’explique pas la différence.
Une seconde source de confusion est la version du manifest. Une extension MV2 et une extension MV3 échouent différemment : MV2 a une page d’arrière-plan persistante qui rend l’extension évidente dès qu’elle charge, tandis que MV3 ne démarre un service worker que lorsque quelque chose le demande. Quelqu’un passé d’une ancienne extension à une nouvelle peut donc voir un changement de comportement qui est une différence de manifest et non un échec de chargement, et la vérification qui les distingue doit regarder l’ensemble installé plutôt que le worker en cours.
MV2 -> persistent background page; visible immediately
MV3 -> service worker; starts on demand, suspends when idle
Ces deux différences pointent vers la même discipline, qui consiste à vérifier la chose qui ne peut pas être ambiguë. L’ensemble des extensions installées est cette chose, parce qu’il contient l’extension ou non, et qu’il ne dépend pas de la présence d’un worker au moment de l’interrogation.
La liste de contrôle comme fixture
Une liste de contrôle travaillée ferme le sujet, parce que les conditions s’appliquent plus facilement comme liste ordonnée que comme prose. Chaque étape évite un échec précis, et l’ordre compte : le type de contexte décide si le reste peut fonctionner, donc il vient en premier.
1. launch a persistent context with an explicit user-data directory
2. pass --disable-extensions-except and --load-extension with the same path
3. validate the manifest before launch
4. read the installed extension set after launch
5. assert the set contains the extension, and fail with the four recorded fields
La première étape est la plus souvent sautée, parce qu’un appel chromium.launch() semble devoir fonctionner et que l’échec est silencieux. Ajouter les flags à un lancement qui crée un contexte neuf, c’est faire la bonne chose au mauvais niveau, et le résultat observable est le même que passer aucun flag.
La troisième étape est sautée parce qu’un manifest.json mal formé produit souvent un message qui nomme le navigateur plutôt que le fichier. Valider le JSON avant le lancement transforme cela en erreur d’analyse avec un numéro de ligne, ce qui est un correctif de cinq secondes au lieu d’une recherche de quinze minutes dans les options de lancement.
Les étapes quatre et cinq sont celles qui transforment une réussite silencieuse en échec bruyant. Sans elles, un test qui dépend de l’extension exécutera ses assertions contre une page que l’extension n’a jamais touchée, et le rapport dira que les assertions sont passées. C’est l’issue la plus coûteuse sur ce sujet, parce que le travail d’écriture du test est déjà fait et que le résultat ne veut rien dire.
La liste est assez courte pour vivre dans une fixture plutôt que dans un document, ce qui est sa bonne place. Une fixture qui exécute les cinq étapes une fois et expose le contexte obtenu fait que chaque test d’une suite hérite de l’assertion au lieu de la répéter, et un environnement cassé échoue dans la fixture plutôt qu’à l’intérieur d’un test sans rapport.
Quand headless n’est pas la variable
Une mesure de plus mérite d’être enregistrée parce qu’elle retire une hypothèse que le lecteur formerait sinon. L’échec précédent pouvait raisonnablement être imputé au mode headless, puisque les navigateurs headless sont ceux que l’on décrit le plus souvent comme dépourvus de support des extensions. Le même dossier et les mêmes flags ont donc été essayés en mode avec interface sur le même hôte.
persistent_headless_new: extLoaded=false
persistent_headed: extLoaded=false
Le mode avec interface a échoué de la même façon, ce qui écarte --headless comme cause sur cet hôte et désigne le build ou le dossier. C’est un résultat négatif utile : il évite au lecteur de passer un après-midi à basculer entre modes headless alors qu’aucun des deux n’est la variable. Un résultat négatif qui élimine l’une des trois conditions vaut autant qu’un résultat positif qui en confirme une autre.
La lecture rappelle aussi comment ces résultats doivent être notés. « Les extensions ne fonctionnent pas en headless » est une affirmation que la mesure ne soutient pas, et c’est une affirmation sur laquelle un lecteur agirait. « Sur cet hôte, avec ce build et ce dossier, les deux modes headless et le mode avec interface ont rapporté aucune extension » est l’affirmation que la mesure soutient, et elle indique une étape suivante différente.
La même discipline vaut pour le cas positif. Une exécution où l’extension charge prouve que les trois conditions étaient réunies pour ce build sur cet hôte, et elle ne prouve pas que les flags soient corrects en général. Enregistrer les quatre champs est ce qui permet à un lecteur ultérieur de savoir laquelle de ces deux affirmations une exécution a réellement établie.
loaded + recorded build -> this build accepted this directory
loaded + no build -> something worked, cause unknown
Une dernière remarque sur le rapport entre une extension et les valeurs qu’annonce une page, parce que c’est la raison pour laquelle ce sujet voisine la cohérence de profil plutôt qu’une aide d’outil séparée. Une extension peut lire et modifier le document dans lequel elle tourne, et elle le fait à l’intérieur du même navigateur que le profil décrit. Cela veut dire qu’une extension qui altère une surface de la page devient une partie de ce que la page annonce, et qu’un profil qui n’en tient pas compte décrit un navigateur qui n’est pas celui testé. Le même raisonnement vaut pour toute surcharge qu’une page peut observer, que les signaux d’automatisation et leur cohérence traite comme une question de concordance des surfaces plutôt que comme une liste d’ajouts.
profile without extension -> describes the browser alone
profile with extension -> describes the browser plus what the extension changed
La conséquence pratique est que l’extension appartient au même enregistrement que le build et le profil, pour la même raison que les deux autres. Une valeur qui change quand on ajoute une extension n’est un défaut ni de l’un ni de l’autre ; c’est le résultat attendu de l’entrée d’un acteur supplémentaire dans le document, et l’enregistrement doit le dire, sinon un lecteur ultérieur attribuera le changement à la mauvaise cause.
C’est aussi pourquoi un environnement de test qui perd son extension en silence produit un résultat qui paraît propre et ne l’est pas. La page annonce les valeurs d’un navigateur sans l’extension, les assertions qui dépendent de l’extension ne tournent jamais, et le rapport est cohérent avec lui-même au sujet d’une configuration qui n’a jamais servi. L’assertion de la liste ci-dessus existe pour empêcher exactement cette issue, et c’est la ligne de code qui rend le reste du sujet applicable.
Conclusion pratique
Construis le lancement dans l’ordre que la plateforme attend : contexte persistant avec un dossier de profil explicite, les deux flags ensemble et une assertion juste après le lancement que l’extension est réellement présente. Ne traite la liste des workers comme une preuve que lorsqu’elle n’est pas vide, parce qu’un worker absent est ambigu et une installation absente ne l’est pas. Valide le manifest avant le lancement plutôt qu’après, pour qu’un fichier invalide échoue avec un message sur le fichier au lieu d’un délai dépassé sur un worker.
Quand l’assertion échoue, enregistre ce qui a été passé et ce que le navigateur a rapporté, parce que les trois conditions ci-dessus forment tout l’espace de recherche. Une exécution qui note seulement « l’extension n’a pas chargé » laisse le lecteur suivant deviner laquelle des trois a échoué, et la supposition porte souvent sur les flags quand la cause est le build.
Pour ce qu’une extension peut voir une fois qu’elle tourne, lis isolation des scripts de contenu d’une extension de navigateur ; pour savoir quel build est installé et comment l’épingler, lis épinglage de version Chrome for Testing.
Une remarque sur l’emplacement de l’extension appartient aux conditions, parce qu’un lecteur qui déplace un test entre machines déplace souvent l’extension sans le remarquer. Une extension non empaquetée se charge depuis un chemin du système de fichiers, et ce chemin fait partie du lancement plutôt que du test. Un conteneur qui monte l’extension ailleurs exige donc une autre valeur de flag, et une configuration de lancement en cache qui fige un chemin échoue en silence sur l’autre machine.
host A: --load-extension=/work/ext
host B: --load-extension=/opt/ci/ext (same extension, different path)
L’identifiant de l’extension découle du chemin pour une extension non empaquetée, donc un dossier déplacé produit un identifiant différent en même temps qu’une autre valeur de flag. Tout ce qui dépend de l’identifiant, comme une clé de stockage ou une entrée de permissions, cesse alors de correspondre au même instant, et l’échec ressemble à un problème de permissions plutôt qu’à un problème de chemin.
C’est pourquoi les quatre champs enregistrés contiennent la chaîne des flags plutôt qu’un booléen comme loadExtension=true. Un booléen survit intact à un changement de machine et masque la différence, tandis que la chaîne montre que le chemin a changé et explique d’un coup d’œil l’extension manquante et l’identifiant modifié.
Le même raisonnement vaut pour une extension empaquetée installée depuis une boutique. L’identifiant est stable et le chemin n’est pas visible, donc une extension empaquetée peut passer d’une machine à l’autre sans changer quoi que ce soit de ce qu’un lecteur enregistre ; le mode d’échec est alors une divergence de version plutôt qu’une divergence de chemin, et le champ à enregistrer est la version de l’extension au lieu du dossier.
Sources
Cet article fait partie du guide : Surfaces de la plateforme web : carte de revue
Articles Connexes
Chrome headless nouveau et ancien : shell, mode et écarts
PlateformeIsolation des content scripts d’extensions de navigateur
PlateformeWidevine dans les navigateurs automatisés : pourquoi EME échoue
PlateformeExécuter des profils de navigateur Windows sur macOS et Linux
ComparaisonCœur du navigateur et scripts JavaScript pour les empreintes
DémarrageScraping avec Puppeteer : options et observations
Exécutez la même vérification dans BotBrowser
Chargez un profil, gardez dans le navigateur les signaux que vous venez de lire, puis confirmez le résultat sur votre propre machine avant le déploiement.