BrowserContext Puppeteer : propriété des pages et nettoyage
Identifiez qui possède navigateurs, contextes, pages, fenêtres contextuelles, délais et nettoyage Puppeteer afin d’éviter les fuites dans les tests autorisés.
Vous voulez la documentation structurée pour Démarrage ?
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.
Browser, BrowserContext et Page ont des propriétaires différents
En bref, un Browser Puppeteer possède la connexion à un processus de navigateur, un BrowserContext possède un groupe isolé de cibles et de données gérées par le navigateur, et une Page représente un onglet ou une cible de page dans un contexte. Le code de test est propriétaire des handles Puppeteer qu'il crée. Il doit les fermer dans le périmètre où il les a acquis, sauf si un fixture de niveau supérieur possède explicitement ce nettoyage. Ce modèle de propriété rend les échecs compréhensibles : l'échec d'une page appartient à un parcours, celui d'un contexte touche cette session isolée et celui d'un navigateur touche tous les contextes qui utilisent le même processus.
La méthode Browser.createBrowserContext() de Puppeteer crée un contexte qui ne partage ni cookies ni cache avec les autres contextes du navigateur. context.newPage() crée une page dans ce contexte. À l'inverse, browser.newPage() crée une page dans le contexte par défaut du navigateur. Cette distinction est facile à manquer, car les deux appels renvoient une Page, mais seul le premier rend le propriétaire du contexte visible dans le code. Un test qui nécessite une isolation par scénario doit créer délibérément un contexte autre que celui par défaut, puis créer ses pages depuis ce contexte.
Le contexte par défaut a une durée de vie particulière. browser.defaultBrowserContext() le renvoie, mais Puppeteer indique qu'il ne peut pas être fermé. Il prend fin avec le navigateur. Un contexte renvoyé par createBrowserContext() peut être fermé indépendamment, et sa fermeture ferme toutes les pages associées. Le contexte par défaut convient donc à un script court, mais constitue une mauvaise limite implicite de fixture dans un processus de test partagé. Un test ne peut pas le démonter de manière fiable tout en laissant se poursuivre des tests sans rapport.
Cette relation est un arbre de ressources, pas seulement un graphe d'objets. Un navigateur peut contenir plusieurs contextes. Un contexte peut contenir plusieurs pages et d'autres cibles, notamment des workers. browser.pages() couvre les pages de tous les contextes, tandis que context.pages() limite l'inventaire à un seul contexte. Une découverte large au niveau du navigateur peut donc récupérer une page créée par un autre test. Les helpers doivent recevoir explicitement une Page ou un BrowserContext au lieu de rechercher dans tout le navigateur la page qui correspond alors à une URL.
L'isolation a aussi une limite définie. Des contextes distincts évitent le partage accidentel des cookies et du cache gérés par le navigateur, mais ils ne constituent pas des sandboxes de système d'exploitation distinctes et ne créent pas de comptes de service séparés. Une application peut encore écrire dans une base de données externe, envoyer un e-mail, créer une entrée chez un prestataire de paiement ou maintenir une session serveur après la disparition de sa page. Le contexte du navigateur possède l'environnement de navigation côté client. L'application et le service restent responsables de leurs mécanismes pris en charge de déconnexion, de restauration et de conservation des données.
Il n'est pas nécessaire d'inventer un objet générique « état de stockage » pour expliquer ce cycle de vie. Puppeteer expose des opérations précises, notamment les cookies du contexte, les pages, les permissions et les cibles. Les autres stockages ont leur propre origine et leur propre comportement d'API. Une exportation de cookies n'est pas une capture complète du stockage local, d'IndexedDB, de Cache Storage, des service workers, du JavaScript en mémoire ou de l'état de session distant. Pour ces limites, consultez le modèle de stockage du navigateur et rendez explicite le propriétaire de chaque stockage.
Créez un contexte et conservez son nettoyage dans le même périmètre
La forme de fixture la plus fiable consiste à acquérir la ressource, à enchaîner immédiatement avec un bloc try et à prévoir un chemin de nettoyage toujours exécuté. Ne créez pas le contexte dans un module pour ne transmettre que sa page à travers plusieurs couches en espérant qu'un hook d'arrêt global retrouve plus tard le propriétaire manquant. Le code qui crée le contexte doit conserver son handle, même si le scénario ne travaille normalement qu'avec une page.
Cet exemple attribue un contexte et une page à un scénario autorisé. Il préserve aussi une erreur du parcours et une erreur de nettoyage. La première reste la cause de l'échec du test ; la seconde demeure visible au lieu de la remplacer ou de la masquer silencieusement.
import type { Browser } from 'puppeteer';
async function runCheckoutCheck(browser: Browser) {
const context = await browser.createBrowserContext();
let workflowFailed = false;
let workflowError: unknown;
try {
const page = await context.newPage();
page.setDefaultTimeout(10_000);
page.setDefaultNavigationTimeout(20_000);
await page.goto('https://example.test/checkout', {
waitUntil: 'domcontentloaded',
});
await page.locator('[data-test="cart-total"]').wait();
} catch (error) {
workflowFailed = true;
workflowError = error;
}
let cleanupFailed = false;
let cleanupError: unknown;
try {
await context.close();
} catch (error) {
cleanupFailed = true;
cleanupError = error;
}
if (workflowFailed && cleanupFailed) {
throw new AggregateError([workflowError, cleanupError], 'Workflow and BrowserContext cleanup both failed');
}
if (workflowFailed) throw workflowError;
if (cleanupFailed) throw cleanupError;
}
Créer le contexte avant d'entrer dans le parcours principal est acceptable ici, car le rejet de la promesse createBrowserContext() signifie qu'aucun handle de contexte n'a été renvoyé et ne doit être fermé. Si la configuration comporte plusieurs étapes d'acquisition, enregistrez chaque ressource dès que son acquisition réussit. Une page, un flux de téléchargement, un répertoire temporaire ou un enregistrement peut échouer après la création du contexte mais avant le début du scénario. Le nettoyage doit fonctionner avec une configuration partielle au lieu de supposer que toutes les variables ont été initialisées.
Il n'est généralement pas nécessaire de fermer chaque page avant de fermer son contexte. BrowserContext.close() ferme le contexte et toutes les pages associées. Les appels explicites à page.close() sont utiles lorsqu'une page a une durée de vie plus courte que le contexte, lorsqu'un test veut vérifier la fermeture d'une fenêtre contextuelle ou lorsqu'un long scénario doit libérer une page plus tôt. Ils ne doivent pas remplacer la fermeture du contexte propriétaire du groupe.
Le nettoyage de l'application précède celui du navigateur lorsque le contrat applicatif l'exige. Par exemple, un achat synthétique peut nécessiter un appel d'annulation pris en charge, ou un compte de test l'action normale de déconnexion de l'application. Effectuez cette opération tant que la page et le contexte fonctionnent encore, puis fermez les ressources du navigateur. Si le nettoyage applicatif échoue, tentez malgré tout celui du contexte et signalez les deux résultats. Ne supprimez jamais des fichiers partagés ni des entrées de service sans rapport simplement parce que la fermeture du navigateur ne s'est pas terminée correctement.
La sortie du fixture doit être plus petite que les ressources qu'il gère. Un résultat utile consigne le nom du scénario, la version du navigateur, le résultat de création du contexte, celui de la page et celui du nettoyage. Il n'a pas besoin des valeurs de cookies, des en-têtes d'autorisation, du HTML complet ni de données personnelles. Les captures d'écran et les traces peuvent aussi contenir des identifiants ou du contenu utilisateur ; ne les collectez que pour le test autorisé et conservez-les selon la politique habituelle de l'équipe en matière d'artefacts.
Traitez les pages et fenêtres contextuelles comme des ressources du contexte
Une Page appartient toujours à un contexte de navigateur. page.browserContext() permet au code de vérifier cette relation, mais il vaut mieux transmettre le bon propriétaire que le découvrir après un échec. Un helper qui ouvre un rapport doit recevoir la page ou le contexte du scénario. Il ne doit pas appeler browser.newPage(), sauf si le placement de la nouvelle page dans le contexte par défaut est intentionnel. Sinon, un helper peut franchir la limite d'isolation sans aucune erreur évidente.
Les fenêtres contextuelles introduisent un second problème de propriété : la nouvelle page est créée par le comportement du navigateur, et non par un appel direct à context.newPage(). Elle appartient toujours à un contexte, mais le test doit l'observer avant que l'action de l'utilisateur puisse la créer. Attendre après le clic crée une condition de concurrence. Une fenêtre contextuelle rapide peut apparaître avant que le listener ou le prédicat de cible n'existe, ce qui laisse le test attendre un événement déjà survenu.
Puppeteer propose un événement de page popup et la découverte de cibles au niveau du contexte. Une attente de cible limitée au contexte est utile lorsque le navigateur est partagé, car elle ne peut pas sélectionner accidentellement une cible d'un autre contexte. Enregistrez d'abord la promesse, déclenchez ensuite l'action, puis attendez la promesse déjà enregistrée.
const popupTargetPromise = context.waitForTarget(target => target.opener() === page.target(), { timeout: 10_000 });
const [popupTarget] = await Promise.all([popupTargetPromise, page.locator('[data-test="open-receipt"]').click()]);
const popup = await popupTarget.page();
if (!popup) {
throw new Error('The opened target was not a page');
}
await popup.locator('[data-test="receipt-number"]').wait();
await popup.close();
Le prédicat de l'élément déclencheur est important. Attendre la prochaine cible de type page ne suffit pas dans un contexte actif, car une action en arrière-plan sans rapport peut créer une autre page en premier. target.opener() === page.target() relie le résultat à la page qui a effectué l'action. Si l'application réutilise délibérément un onglet existant au lieu d'ouvrir une fenêtre contextuelle, utilisez la condition de navigation ou de contenu correspondante au lieu d'imposer au parcours l'hypothèse d'une fenêtre contextuelle.
La navigation suit la même règle d'ordre. Lorsqu'un clic doit déclencher une navigation, créez la promesse page.waitForNavigation() avant le clic et attendez les deux opérations ensemble. Lorsqu'un clic met la page à jour sans navigation, attendez plutôt une condition visible ou de réponse précise. Un délai générique ne prouve pas que la transition attendue s'est produite, et networkidle n'est pas un signal universel indiquant que l'application est prête pour les pages qui maintiennent ouvertes des requêtes d'analyse, de streaming ou d'arrière-plan.
Le nettoyage des fenêtres contextuelles suit l'arbre du contexte. Fermer la fenêtre contextuelle libère cette page tout en conservant la page parente et le contexte. Fermer le contexte libère les deux. Fermer uniquement la page qui a ouvert les autres ne garantit pas que toutes les pages ainsi créées ont aussi pris fin ; le démontage doit donc s'appuyer sur la limite du contexte une fois le scénario complet terminé. Avant une assertion en cours de scénario, context.pages() peut fournir un inventaire borné pour vérifier le nombre ou la propriété sans explorer les autres contextes.
Les workers et les téléchargements exigent une discipline similaire, même s'ils ne sont pas tous représentés par des objets Page. Un worker peut poursuivre l'activité de l'application après une transition de page, et un téléchargement peut survivre au clic qui l'a lancé. Attendez les tâches appartenant au test qui doivent se terminer, annulez-les au moyen d'une API prise en charge lorsque cette annulation fait partie du scénario, puis fermez le contexte propriétaire. La fermeture du contexte libère les ressources du navigateur, mais elle ne peut pas annuler une mutation distante déjà envoyée par un worker ni un fichier que le runner de test a déjà déplacé ailleurs.
Faites décrire aux délais une attente non satisfaite, pas le nettoyage
Un délai d'attente est une limite d'observation. Il indique combien de temps le test attendra une condition précise ; il ne prouve pas que le navigateur a arrêté le travail applicatif sous-jacent. Lorsque waitForSelector, l'attente d'un locator, une navigation ou waitForTarget dépasse son délai, le contexte peut encore être ouvert et la page continuer à s'exécuter. Le nettoyage doit donc suivre la gestion du délai comme il suit l'échec d'une assertion.
Puppeteer distingue les valeurs par défaut générales de celles de navigation. page.setDefaultTimeout(ms) fournit le maximum par défaut aux méthodes qui utilisent le réglage de délai de la page. page.setDefaultNavigationTimeout(ms) contrôle les méthodes de navigation et prend la priorité pour ces opérations. Une option explicite de la méthode est plus claire lorsqu'une action a légitimement besoin d'un délai différent. Choisissez les valeurs selon l'environnement de test et la transition visible attendue par l'utilisateur, et non d'après une promesse selon laquelle tous les sites finiraient dans un délai universel.
Les délais doivent identifier la condition qu'ils protègent. L'attente d'une cible de fenêtre contextuelle doit indiquer qu'aucune fenêtre provenant de l'élément déclencheur attendu n'est apparue. L'attente d'un sélecteur doit nommer l'état applicatif qui n'est pas devenu visible. Un délai de navigation doit distinguer l'absence de navigation d'une réponse HTTP arrivée avec un statut inattendu. Remplacer tous ces délais par un unique délai global du test produit un seul échec vague et met le nettoyage en concurrence avec ce même délai déjà épuisé.
Prévoyez un budget de démontage distinct au niveau du runner de test. Les méthodes Puppeteer context.close() et browser.close() n'acceptent pas les mêmes options de délai par action que les attentes de page. Un runner peut imposer un délai externe, mais Promise.race() cesse seulement d'attendre : il n'annule pas la promesse de fermeture perdante et ne prouve pas que les ressources du navigateur ont disparu. Si un harnais signale le dépassement du délai de fermeture, il doit marquer le nettoyage comme incomplet et confier l'arrêt du processus au composant réellement propriétaire du processus du navigateur.
N'ignorez pas une TimeoutError de Puppeteer pour continuer sur une page dont l'état est inconnu. Une action ayant dépassé son délai peut avoir partiellement réussi. Réessayer un achat, l'envoi d'un formulaire ou une opération destructive du service dans la même page peut dupliquer son effet. Déterminez d'abord si la condition en échec était en lecture seule et pouvait être réessayée. Pour un problème d'infrastructure réessayable, fermez l'ancien contexte, créez-en un nouveau avec les mêmes entrées synthétiques autorisées et lancez une nouvelle tentative. En cas d'incertitude côté application, utilisez son contrat d'idempotence ou de statut pris en charge avant de réessayer.
Les erreurs de nettoyage ont également besoin de leur propre catégorie. Une erreur de fermeture de page, une erreur de fermeture de contexte et une déconnexion du navigateur ne sont pas équivalentes à une assertion applicative. Signalez d'abord l'erreur initiale du parcours et joignez les erreurs de nettoyage, comme le fait l'exemple de fixture avec AggregateError. Cela conserve la preuve qu'un sélecteur a dépassé son délai tout en montrant que le démontage était incomplet. Un bloc finally qui lance une nouvelle erreur de fermeture sans conserver l'exception initiale complique le diagnostic du test.
Évitez de faire d'un hook de sortie de processus sans limite le principal mécanisme de nettoyage. Les hooks de sortie sont utiles comme dernière limite de diagnostic, mais le travail asynchrone peut ne pas se terminer dans tous les modes d'arrêt. Le démontage par test et par fixture assure une propriété déterministe tant que la boucle d'événements et la connexion fonctionnent. Un propriétaire au niveau de la suite doit ensuite fermer le navigateur partagé après la fin de tous les propriétaires de contexte, selon sa propre politique d'arrêt bornée.
Choisissez la fermeture ou la déconnexion selon le propriétaire du processus
page.close(), context.close(), browser.close() et browser.disconnect() ont volontairement des effets différents. Le choix entre ces opérations n'est pas une préférence stylistique. C'est une décision de propriété du processus.
| Opération | Ce qu'elle termine | Ce qui reste |
|---|---|---|
page.close() | Une page | Son contexte, les pages sœurs et le navigateur |
context.close() | Un contexte autre que celui par défaut et toutes les pages associées | Les autres contextes et le navigateur |
browser.close() | Le navigateur et toutes les pages associées | Le processus de test Node.js et ses ressources extérieures au navigateur |
browser.disconnect() | La connexion de Puppeteer au navigateur | Le processus du navigateur et ses pages continuent de s'exécuter |
Page.close() n'exécute pas les hooks beforeunload par défaut. Avec runBeforeUnload: true, elle les exécute, et Puppeteer n'attend pas que la page se ferme réellement. N'utilisez ce comportement que lorsque le handler fait partie du scénario testé. Le démontage ne doit pas compter sur le handler de déchargement d'une application pour effectuer une restauration distante, et la résolution de l'appel de fermeture dans ce mode ne prouve pas que le nettoyage local ou distant est terminé.
Browser.close() ferme le navigateur et toutes les pages associées. C'est normalement la bonne action finale lorsque le fixture possède un navigateur qu'il a lancé. Les propriétaires de contexte doivent d'abord fermer leurs contextes afin qu'un échec puisse être attribué au bon scénario ; le propriétaire du navigateur au niveau de la suite ferme ensuite la ressource de tout le processus. Appeler uniquement browser.close() à la fin peut libérer les ressources, mais masque le test qui a laissé fuir un contexte ou une page pendant l'exécution.
Browser.disconnect() déconnecte Puppeteer tout en laissant le processus du navigateur en cours d'exécution. Cette méthode convient lorsqu'un autre composant possède un navigateur à longue durée de vie et que le client actuel ne possède que sa connexion. Elle ne nettoie ni les pages, ni les contextes, ni les cookies, ni les téléchargements, ni les sessions serveur. Après la déconnexion, ce client ne peut plus gérer ces objets au moyen du handle Browser déconnecté. Le propriétaire du processus doit conserver un canal de contrôle distinct et une politique d'arrêt explicite.
Le choix de puppeteer.launch() ou de puppeteer.connect() dans le code constitue un indice utile de propriété, mais le contrat de déploiement est décisif. Un processus lancé par un worker appartient généralement à ce worker. Un processus atteint au moyen d'un endpoint WebSocket du navigateur appartient souvent à un service. Le code ne doit pas fermer un service partagé simplement parce que l'API le permet, ni se déconnecter d'un navigateur dont il est propriétaire pour affirmer ensuite que le processus a été libéré.
La fermeture n'efface pas les effets externes. context.close() supprime ce contexte actif et ses pages ; elle ne révoque pas un jeton déjà copié ailleurs, n'annule pas une commande, ne supprime pas un compte de test et n'efface pas un fichier téléchargé par le runner de test. Si un parcours doit effacer les données du site tout en maintenant le contexte actif, utilisez le mécanisme précis du navigateur ou de l'application et vérifiez sa portée documentée. Le guide de suppression des données de site explique pourquoi la suppression des données client et le nettoyage du compte sont deux affirmations distinctes.
Vérifiez le démontage avec des contrôles observables
Exécutez de petits contrôles avec la version du navigateur et la forme de fixture que vous utilisez réellement. Les observations suivantes proviennent d'une page locale activée dans Chrome 154 standard avec Puppeteer 24.40.0 ; elles fournissent des conditions d'acceptation pour un test, et non des garanties de délai universelles ou une validation de BotBrowser.
| Contrôle | Action minimale | Observez avant de poursuivre | N'en déduisez pas |
|---|---|---|---|
| Fermeture de page par défaut | Ajoutez un handler beforeunload, puis appelez page.close() sans runBeforeUnload | Aucun dialogue n'est observé et page.isClosed() est vrai | Que chaque chemin de fermeture affichera un dialogue de déchargement |
| Fermeture avec déchargement activé | Activez la page locale, appelez page.close({ runBeforeUnload: true }), puis acceptez le dialogue | La promesse de fermeture peut être résolue alors que page.isClosed() est faux ; attendez la condition fermée après avoir accepté le dialogue | Qu'un délai fixe garantit la fermeture ou le nettoyage distant |
| Isolation de contextes possédés | Définissez des cookies synthétiques et des valeurs de stockage local distincts dans deux contextes de la même origine | Chaque contexte ne lit que sa propre valeur | Que des contextes séparés isolent des comptes serveur ou des ressources du système d'exploitation |
| Fermeture de contexte et déconnexion | Fermez un contexte possédé, puis déconnectez un client qui utilise un endpoint de navigateur | Sa page est fermée alors que le navigateur reste connecté après la fermeture du contexte ; après la déconnexion, reconnectez-vous via l'endpoint avant d'utiliser des handles du navigateur | Que disconnect() termine le processus du navigateur ou nettoie l'état distant |
Répétez ces contrôles après une mise à niveau du navigateur ou de Puppeteer et attendez un état fermé observable plutôt qu'un minuteur. Ils décrivent un cas local borné, et non un résultat sur un site externe, un test de profil ou une garantie sur le nettoyage de l'application.
Appliquez le modèle à BotBrowser sans élargir l'affirmation
Lorsque Puppeteer pilote un processus Chromium BotBrowser, les API standard Browser, BrowserContext, Target et Page de Puppeteer continuent de définir l'arbre des ressources d'automatisation. L'adéquation spécifique vérifiée de BotBrowser est plus étroite : sa documentation sur l'isolation multi-compte décrit un stockage et des sessions distincts par contexte, ainsi que des contrôles de profil propres au contexte pour la licence documentée. Ces contrôles sont attribués avant la création des pages, car un renderer lit la configuration de son contexte au démarrage. BotBrowser ne remplace ni context.close(), ni le nettoyage des pages, ni le démontage applicatif, ni l'invalidation des sessions serveur, ni la gestion des secrets. Il ne peut pas faire en sorte que browser.disconnect() termine un processus, transformer le délai d'une page en annulation, annuler une requête distante ou garantir qu'un handler de déchargement a fini. Puppeteer et le fixture de test restent propriétaires du nettoyage de l'automatisation ; l'application et le service restent propriétaires de leur nettoyage distant pris en charge.
Cet ordre renforce la règle de propriété. Créez le contexte, appliquez toute configuration de contexte documentée et couverte par la licence au moyen de l'intégration prise en charge, puis seulement créez des pages dans ce contexte. Conservez une identité synthétique autorisée par contexte. Un profil de navigateur de base n'autorise pas la réutilisation silencieuse du compte d'un autre contexte, et un profil propre au contexte n'est pas un instantané Puppeteer sérialisé de tous les mécanismes de stockage web.
La disponibilité fait également partie du contrat. La documentation de BotBrowser énumère les prérequis de la prise en charge complète des fingerprints par contexte, notamment la licence d'entreprise applicable. Une équipe doit vérifier sa version installée, la compatibilité du profil et sa licence avant de dépendre de contrôles propres au contexte. La solution de repli sûre ne consiste pas à affirmer que des flags non pris en charge ont eu un effet. Faites échouer la configuration avant la création de la page, ou exécutez un scénario qui utilise uniquement les capacités réellement disponibles dans cet environnement.
Pour une validation bornée, consignez uniquement la version du navigateur, le nom du scénario synthétique, le résultat de création du contexte, le nombre de pages attendu et le résultat du nettoyage. Confirmez que deux contextes de test ne partagent pas l'entrée connue de cookie ou de cache utilisée par le test. Fermez ensuite chaque contexte au moyen du fixture qui le possède, puis fermez ou déconnectez le navigateur selon le contrat du processus. Cela vérifie la configuration choisie ; cela n'établit pas que des comptes distants sont impossibles à relier ni qu'un service a supprimé ses propres données.
Suivez cette séquence de revue avant de considérer un test de cycle de vie comme terminé :
- Nommez le propriétaire du processus du navigateur et décidez si l'action finale est
close()oudisconnect(). - Créez un contexte autre que celui par défaut pour chaque scénario isolé et créez les pages depuis ce contexte.
- Enregistrez les attentes de fenêtre contextuelle, de cible ou de navigation avant l'action qui peut les satisfaire.
- Donnez un sens explicite aux attentes d'action et préservez le premier échec pendant l'exécution du nettoyage.
- Vérifiez séparément le nettoyage de l'application, puis fermez le contexte et signalez tout démontage incomplet.
Cette séquence facilite également la revue des mises à niveau du navigateur. Les appels d'API sont visibles, la limite du contexte est explicite et une fenêtre contextuelle non libérée ne peut pas se cacher derrière l'arrêt de tout le processus. Pour l'état propre aux service workers, utilisez le guide du cycle de vie des service workers au lieu de considérer la fermeture d'une page comme une garantie concernant le cache ou les données distantes.
Sources
Articles Connexes
Faites passer BotBrowser de la recherche à la production
Utilisez ces guides pour comprendre le modèle, puis passez à la validation multi-plateforme, aux contextes isolés et au déploiement navigateur prêt pour l'échelle.