Playwright gère les flux multi-onglets, les fenêtres popup, les iFrames et le Shadow DOM via un modèle en couches composé d'objets Browser, BrowserContext et Page.
Comment Playwright modélise les pages, contextes et onglets
Avant d'écrire la moindre ligne de code, vous devez comprendre la hiérarchie d'objets de Playwright.
Un Browser est le processus du navigateur. Un BrowserContext est une session isolée dans ce navigateur, avec ses propres cookies, stockage et état réseau. Une Page représente un onglet ou une fenêtre dans un contexte. Quand un utilisateur clique sur un lien qui ouvre un nouvel onglet, Playwright le voit comme un nouvel objet Page ajouté au BrowserContext existant.
import { chromium } from '@playwright/test';
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage(); // Onglet 1
const page2 = await context.newPage(); // Onglet 2 dans la même sessionCe modèle a trois implications. Les pages dans le même contexte partagent les cookies et le stockage local : si la page 1 est connectée, la page 2 l'est aussi. Les pages dans des contextes différents sont complètement isolées, c'est pourquoi Playwright utilise des contextes séparés pour simuler plusieurs utilisateurs. Pour intercepter des événements sur tous les onglets (pas seulement un), utilisez context.on() et context.route(), pas page.on() et page.route().
Détecter un nouvel onglet avec context.waitForEvent('page')
Le scénario le plus courant : un utilisateur clique sur un lien ou un bouton qui ouvre quelque chose dans un nouvel onglet. Votre test doit obtenir une référence à ce nouvel onglet et interagir avec lui.
Le bon schéma est de configurer l'écouteur avant de déclencher l'action qui ouvre le nouvel onglet. Si vous déclenchez l'action en premier, l'événement de nouvelle page peut se déclencher avant que votre écouteur soit en place.
import { test, expect } from '@playwright/test';
test('gère un nouvel onglet ouvert par un clic de lien', async ({ page, context }) => {
await page.goto('https://lab.becomeqa.com');
// Configurer l'écouteur AVANT de cliquer
const newPagePromise = context.waitForEvent('page');
// Ce clic ouvre un nouvel onglet
await page.getByRole('link', { name: 'Ouvrir dans un nouvel onglet' }).click();
// Attendre la nouvelle page et la laisser se charger
const newPage = await newPagePromise;
await newPage.waitForLoadState('domcontentloaded');
// Maintenant interagir avec le nouvel onglet
await expect(newPage).toHaveURL(/\/docs/);
await expect(newPage.getByRole('heading', { level: 1 })).toBeVisible();
// La page originale est toujours accessible
await expect(page).toHaveURL('https://lab.becomeqa.com');
});context.waitForEvent('page') retourne une promise qui se résout avec le nouvel objet Page dès sa création. "Créé" ne signifie pas "chargé". La page existe mais peut encore être en cours de navigation. Suivez toujours avec waitForLoadState() avant de chercher des éléments.
await page.click(...) puis await context.waitForEvent('page'). Si le nouvel onglet s'ouvre assez vite, l'événement se déclenche entre ces deux lignes et waitForEvent attendra indéfiniment. Configurez toujours la promise en premier, puis déclenchez l'action.Ouvrir un nouvel onglet par programmation
Parfois vous voulez ouvrir un nouvel onglet vers une URL spécifique, ou configurer une deuxième session pour simuler un deuxième utilisateur. Utilisez context.newPage() directement.
test('deux onglets dans la même session', async ({ context }) => {
const page1 = await context.newPage();
const page2 = await context.newPage();
await page1.goto('https://lab.becomeqa.com/dashboard');
await page2.goto('https://lab.becomeqa.com/settings');
// Les deux pages sont dans la même session connectée
await expect(page1.getByText('Bon retour')).toBeVisible();
await expect(page2.getByRole('heading', { name: 'Paramètres du compte' })).toBeVisible();
// Remettre page1 au premier plan
await page1.bringToFront();
await page1.getByRole('button', { name: 'Nouveau voyage' }).click();
});bringToFront() met la page en onglet actif dans l'interface du navigateur. Cela affecte rarement l'exécution headless, mais certains comportements dépendant du focus (glisser-déposer, certains événements clavier) le nécessitent.
Gérer les fenêtres popup
Les popups (fenêtres ouvertes avec window.open()) suivent exactement le même schéma que les onglets. Dans le modèle de Playwright, ce sont simplement de nouveaux objets Page. L'approche waitForEvent('page') fonctionne de façon identique.
test('gère une fenêtre popup OAuth', async ({ page, context }) => {
await page.goto('https://lab.becomeqa.com/login');
// Écouter le popup avant de cliquer
const popupPromise = context.waitForEvent('page');
await page.getByRole('button', { name: 'Se connecter avec Google' }).click();
const popup = await popupPromise;
await popup.waitForLoadState('networkidle');
// Interagir avec le popup OAuth
await popup.getByLabel('Email').fill('test@example.com');
await popup.getByRole('button', { name: 'Suivant' }).click();
await popup.getByLabel('Mot de passe').fill('testpassword');
await popup.getByRole('button', { name: 'Se connecter' }).click();
// Après completion OAuth, le popup se ferme et la page principale se met à jour
await popup.waitForEvent('close');
await expect(page).toHaveURL(/\/dashboard/);
});Attendre la navigation après qu'un lien ouvre un nouvel onglet
Une variation subtile : les liens avec target="_blank" ouvrent un nouvel onglet et naviguent immédiatement vers une URL. Le nouveau Page est créé vide, puis navigue. Cela peut provoquer une condition de course si vous assertez avant la fin de la navigation.
test('attend la navigation dans un nouvel onglet', async ({ page, context }) => {
await page.goto('https://lab.becomeqa.com');
const newPagePromise = context.waitForEvent('page');
await page.getByRole('link', { name: 'Documentation' }).click();
const newPage = await newPagePromise;
// Attendre la fin de la navigation spécifique, pas seulement DOMContentLoaded
await newPage.waitForLoadState('load');
// Maintenant il est sûr d'asserter sur l'URL et le contenu
await expect(newPage).toHaveURL(/\/docs\//);
await expect(newPage.getByRole('navigation')).toBeVisible();
});Utilisez waitForLoadState('load') quand vous avez besoin que toutes les ressources (images, scripts) se chargent. Utilisez waitForLoadState('domcontentloaded') pour des vérifications plus rapides. Utilisez waitForLoadState('networkidle') quand la page déclenche des requêtes XHR supplémentaires après le chargement.
Quand vous connaissez l'URL exacte vers laquelle le nouvel onglet naviguera, waitForURL() est plus précis :
const newPage = await newPagePromise;
await newPage.waitForURL('**/docs/getting-started');
await expect(newPage.getByRole('heading', { name: 'Démarrer' })).toBeVisible();iFrames : pourquoi c'est pénible et comment frameLocator résout le problème
Un iFrame est un document séparé intégré dans la page principale. Du point de vue du navigateur, il a son propre DOM, son propre contexte JavaScript et sa propre origine de sécurité. Les locators standard (page.getByRole(), page.getByText()) ne cherchent que dans le DOM de la page principale. Ils ne voient pas à l'intérieur des iFrames.
frameLocator() retourne un locator limité au contenu de l'iFrame, vous permettant de chaîner les locators exactement comme vous le feriez sur la page principale.
test('remplit un formulaire de paiement dans un iFrame', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/checkout');
// Localiser l'iFrame par son sélecteur, puis chaîner les locators dedans
const paymentFrame = page.frameLocator('iframe[name="payment-widget"]');
await paymentFrame.getByLabel('Numéro de carte').fill('4111111111111111');
await paymentFrame.getByLabel('Date d\'expiration').fill('12/28');
await paymentFrame.getByLabel('CVV').fill('123');
// Retour sur la page principale pour le bouton de soumission
await page.getByRole('button', { name: 'Payer maintenant' }).click();
await expect(page.getByText('Paiement réussi')).toBeVisible();
});Vous pouvez utiliser n'importe quel sélecteur CSS valide dans frameLocator() : iframe#checkout-frame, iframe[src*="stripe.com"], iframe.payment-container.
. Les attributs name, id, src et class sont tous des cibles valides pour frameLocator().iFrames imbriqués
Les widgets de paiement et les embeddings tiers imbriquent parfois des iFrames. frameLocator() supporte le chaînage directement.
test('interagit avec un iFrame imbriqué', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/checkout');
// iFrame externe
const outerFrame = page.frameLocator('iframe#payment-container');
// iFrame interne imbriqué dans l'externe
const innerFrame = outerFrame.frameLocator('iframe#card-number-frame');
await innerFrame.getByPlaceholder('Numéro de carte').fill('4111111111111111');
// Retour à l'iFrame externe pour expiration et CVV (iFrames internes différents)
const expiryFrame = outerFrame.frameLocator('iframe#expiry-frame');
await expiryFrame.getByPlaceholder('MM / AA').fill('12/28');
const cvvFrame = outerFrame.frameLocator('iframe#cvv-frame');
await cvvFrame.getByPlaceholder('CVV').fill('123');
});Stripe Elements est l'exemple classique de ce schéma en conditions réelles. Chaque champ (numéro de carte, expiration, CVV) vit dans son propre iFrame imbriqué séparé pour des raisons de conformité PCI.
Shadow DOM
Le Shadow DOM n'est pas des iFrames, mais cause le même symptôme : les locators standard ne trouvent pas les éléments. C'est une fonctionnalité du navigateur qui encapsule le DOM interne d'un composant. Les web components, éléments personnalisés et certaines bibliothèques UI l'utilisent.
La bonne nouvelle : les locators de Playwright traversent le Shadow DOM par défaut. page.getByRole(), page.getByText() et page.locator() cherchent tous à travers les shadow roots sans configuration supplémentaire.
test('localise des éléments dans le Shadow DOM', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/components');
// Fonctionne même si le bouton est dans un shadow root
await page.getByRole('button', { name: 'Valider' }).click();
// Les sélecteurs CSS ont besoin du combinateur >>> pour traverser le Shadow DOM
await page.locator('custom-login-form >>> input[type="email"]').fill('user@example.com');
});Utilisez >>> dans les sélecteurs CSS quand vous devez traverser le Shadow DOM explicitement. Pour la plupart des cas, préférez les locators sémantiques (getByRole, getByLabel). Ils traversent le Shadow DOM automatiquement.
Erreurs courantes
Changer d'onglet avant qu'il se charge. Le bug le plus fréquent : vous obtenez la référence de la nouvelle page depuiswaitForEvent('page') et essayez immédiatement de cliquer sur quelque chose. La page est encore vide. Appelez toujours waitForLoadState() avant d'interagir.
// Faux : en compétition avec la navigation
const newPage = await newPagePromise;
await newPage.getByRole('button', { name: 'Accepter' }).click(); // Peut échouer
// Correct
const newPage = await newPagePromise;
await newPage.waitForLoadState('domcontentloaded');
await newPage.getByRole('button', { name: 'Accepter' }).click();test('garde la référence à la page originale', async ({ page, context }) => {
const originalPage = page; // Renommer pour plus de clarté avec plusieurs onglets
const newPagePromise = context.waitForEvent('page');
await originalPage.getByRole('link', { name: 'CGU' }).click();
const termsPage = await newPagePromise;
await termsPage.waitForLoadState('load');
await expect(termsPage.getByRole('heading', { name: 'Conditions d\'utilisation' })).toBeVisible();
await termsPage.close();
// Retour à l'original. Utilisez explicitement originalPage.
await expect(originalPage.getByRole('heading', { name: 'Inscription' })).toBeVisible();
});page.frames() quand frameLocator() est disponible. L'ancienne API page.frames() retourne un tableau d'objets Frame. Elle fonctionne mais vous oblige à gérer manuellement les index ou noms de frames. frameLocator() est chaînable, typé, et s'intègre bien à l'auto-waiting de Playwright.
FAQ
Puis-je fermer un onglet spécifique sans terminer le test ?Oui. Appelez await newPage.close() pour fermer n'importe quel objet Page. La page originale et le contexte restent ouverts et utilisables.
Utilisez context.pages(), qui retourne un tableau de tous les objets Page ouverts.
Oui. Chaînez les appels frameLocator() : page.frameLocator('iframe#outer').frameLocator('iframe#inner').
waitForEvent('page') fonctionne-t-il pour les popups ouverts par JavaScript (pas par des clics de liens) ?
Oui. Tout appel window.open() dans le navigateur crée un événement Page sur le contexte, quel que soit son déclencheur.
Oui, c'est l'un des points forts de Playwright par rapport aux outils basés sur WebDriver plus anciens. Les iFrames cross-origin fonctionnent avec frameLocator() sans configuration spéciale.