Quando um clique abre uma nova aba, context.waitForEvent('page') precisa ser configurado como uma promise antes do clique, não depois. Se a nova aba abrir mais rápido do que o listener se registrar, o evento some e a chamada espera para sempre. Obter o novo objeto Page também não significa que ele carregou: ele é criado vazio e depois navega, então waitForLoadState() é obrigatório antes que qualquer locator encontre elementos.

Como o Playwright modela páginas, contextos e abas

Antes de escrever uma linha de código, você precisa entender a hierarquia de objetos que o Playwright usa.

Um Browser é o processo do navegador. Um BrowserContext é uma sessão isolada dentro desse navegador, com seus próprios cookies, storage e estado de rede. Uma Page representa uma única aba ou janela dentro de um contexto. Quando um usuário clica em um link que abre uma nova aba, o Playwright vê isso como uma nova Page sendo adicionada ao BrowserContext existente.

import { chromium } from '@playwright/test';

const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage(); // Aba 1

const page2 = await context.newPage(); // Aba 2 na mesma sessão

Esse modelo importa porque:

  • Páginas no mesmo contexto compartilham cookies e localStorage. Se a page 1 está logada, a page 2 também está.
  • Páginas em contextos diferentes são completamente isoladas. Por isso o Playwright usa contextos separados para simular múltiplos usuários.
  • Quando você precisa interceptar eventos em todas as abas (não só uma), use context.on() e context.route(), não page.on() e page.route().

Detectando uma nova aba com context.waitForEvent('page')

O cenário mais comum: um usuário clica em um link ou botão que abre algo em uma nova aba. Seu teste precisa obter uma referência a essa nova aba e interagir com ela.

O padrão correto é configurar o listener antes de disparar a ação que causa a nova aba abrir. Se você disparar a ação primeiro, o evento de nova página pode disparar antes de o listener estar no lugar.

import { test, expect } from '@playwright/test';

test('lida com nova aba aberta por clique em link', async ({ page, context }) => {
  await page.goto('https://lab.becomeqa.com');

  // Configura o listener ANTES de clicar
  const newPagePromise = context.waitForEvent('page');

  // Este clique abre uma nova aba
  await page.getByRole('link', { name: 'Abrir em nova aba' }).click();

  // Aguarda a nova página e deixa ela carregar
  const newPage = await newPagePromise;
  await newPage.waitForLoadState('domcontentloaded');

  // Agora interaja com a nova aba
  await expect(newPage).toHaveURL(/\/docs/);
  await expect(newPage.getByRole('heading', { level: 1 })).toBeVisible();

  // A página original ainda está acessível
  await expect(page).toHaveURL('https://lab.becomeqa.com');
});

context.waitForEvent('page') retorna uma promise que resolve com o novo objeto Page assim que ele é criado. Note que "criado" não significa "carregado". A página existe mas pode ainda estar navegando. Sempre siga com waitForLoadState() antes de tentar encontrar elementos.
Um erro comum é aguardar o clique antes de configurar o listener: await page.click(...) depois await context.waitForEvent('page'). Se a nova aba abrir rápido o suficiente, o evento dispara entre essas duas linhas e waitForEvent vai esperar para sempre. Configure sempre a promise primeiro, depois dispare a ação.

Abrindo uma nova aba programaticamente

Às vezes você quer controlar a nova aba inteiramente do seu teste. Pode ser para abrir uma URL específica junto com a página principal, ou para configurar uma segunda sessão e simular um segundo usuário. Use context.newPage() diretamente.

test('duas abas na mesma sessão', 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');

  // Ambas as páginas estão na mesma sessão logada
  await expect(page1.getByText('Bem-vindo de volta')).toBeVisible();
  await expect(page2.getByRole('heading', { name: 'Configurações da Conta' })).toBeVisible();

  // Traz page1 de volta ao foco (importa para alguns comportamentos específicos de navegador)
  await page1.bringToFront();
  await page1.getByRole('button', { name: 'Nova Viagem' }).click();
});

bringToFront() torna a página a aba ativa na UI do navegador. Raramente afeta a execução de testes headless, mas alguns comportamentos dependentes de foco (drag-and-drop, certos eventos de teclado) exigem isso.

Lidando com janelas popup

Popups (janelas abertas com window.open()) seguem exatamente o mesmo padrão das abas. No modelo do Playwright, eles são apenas novos objetos Page. A abordagem waitForEvent('page') funciona de forma idêntica.

test('lida com janela popup de OAuth', async ({ page, context }) => {
  await page.goto('https://lab.becomeqa.com/login');

  // Escuta o popup antes de clicar
  const popupPromise = context.waitForEvent('page');

  await page.getByRole('button', { name: 'Login with Google' }).click();

  const popup = await popupPromise;
  await popup.waitForLoadState('networkidle');

  // Interage com o popup de OAuth
  await popup.getByLabel('Email').fill('test@example.com');
  await popup.getByRole('button', { name: 'Next' }).click();
  await popup.getByLabel('Password').fill('testpassword');
  await popup.getByRole('button', { name: 'Sign in' }).click();

  // Após o OAuth completar, o popup fecha e a página principal atualiza
  await popup.waitForEvent('close');
  await expect(page).toHaveURL(/\/dashboard/);
});

Se o popup fecha automaticamente após completar seu fluxo (como janelas OAuth tipicamente fazem), aguarde o evento close na página popup. Isso confirma que terminou antes de fazer assertions na página original.

Uma variação sutil do padrão de nova aba: links com target="_blank" abrem uma nova aba e imediatamente navegam para uma URL. A nova Page é criada vazia, depois navega. Isso pode causar uma race condition se você tentar fazer assertions antes da navegação completar.

test('aguarda navegação em nova aba', async ({ page, context }) => {
  await page.goto('https://lab.becomeqa.com');

  const newPagePromise = context.waitForEvent('page');
  await page.getByRole('link', { name: 'Documentação' }).click();

  const newPage = await newPagePromise;

  // Aguarda a navegação específica completar, não só DOMContentLoaded
  await newPage.waitForLoadState('load');

  // Agora é seguro fazer assertions na URL e conteúdo
  await expect(newPage).toHaveURL(/\/docs\//);
  await expect(newPage.getByRole('navigation')).toBeVisible();
});

Use waitForLoadState('load') quando precisar que todos os recursos (imagens, scripts) terminem. Use waitForLoadState('domcontentloaded') para verificações mais rápidas quando só importa o HTML. Use waitForLoadState('networkidle') quando a página dispara requisições XHR adicionais após o carregamento, embora este seja mais lento e ocasionalmente flaky em páginas movimentadas.

Para casos onde você sabe a URL exata para qual a nova aba vai navegar, waitForURL() é mais preciso:

const newPage = await newPagePromise;
await newPage.waitForURL('**/docs/getting-started');
await expect(newPage.getByRole('heading', { name: 'Getting Started' })).toBeVisible();

iFrames: por que são chatos e como frameLocator resolve

Um iFrame é um documento separado embutido dentro da página principal. Da perspectiva do navegador, ele tem seu próprio DOM, seu próprio contexto JavaScript e sua própria origem de segurança. Locators padrão (page.getByRole(), page.getByText()) só buscam no DOM da página principal. Eles não conseguem ver dentro de iFrames.

frameLocator() é a solução limpa. Retorna um locator com escopo para o conteúdo do iFrame, então você pode encadear locators exatamente da mesma forma que faria na página principal.

test('preenche formulário de pagamento dentro de um iFrame', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com/checkout');

  // Localiza o iFrame pelo seletor, depois encadeia locators dentro dele
  const paymentFrame = page.frameLocator('iframe[name="payment-widget"]');

  await paymentFrame.getByLabel('Número do Cartão').fill('4111111111111111');
  await paymentFrame.getByLabel('Data de Validade').fill('12/28');
  await paymentFrame.getByLabel('CVV').fill('123');

  // De volta à página principal para o botão de submit
  await page.getByRole('button', { name: 'Pagar Agora' }).click();

  await expect(page.getByText('Pagamento realizado com sucesso')).toBeVisible();
});

Você pode usar qualquer seletor CSS válido em frameLocator(): iframe#checkout-frame, iframe[src*="stripe.com"], iframe.payment-container. Se a página tiver múltiplos iFrames, seja específico o suficiente para corresponder exatamente a um.

Se não tiver certeza de qual seletor usar para um iFrame, abra o DevTools do navegador e inspecione o elemento