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 session

Ce 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.
Une erreur courante est d'attendre le clic avant de configurer l'écouteur : 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.

Si vous n'êtes pas sûr du sélecteur à utiliser pour un iFrame, ouvrez les DevTools et inspectez l'élément