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ãoEsse 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()econtext.route(), nãopage.on()epage.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.
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.
Aguardando navegação após um link abrir nova aba
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.
. Os atributos name, id, src e class são todos alvos válidos para frameLocator().iFrames aninhados
Widgets de pagamento e embeds de terceiros às vezes aninham iFrames: um iFrame externo contendo um interno. frameLocator() suporta encadeamento direto.
test('interage com iFrame aninhado', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/checkout');
// iFrame externo
const outerFrame = page.frameLocator('iframe#payment-container');
// iFrame interno aninhado dentro do externo
const innerFrame = outerFrame.frameLocator('iframe#card-number-frame');
await innerFrame.getByPlaceholder('Número do cartão').fill('4111111111111111');
// De volta ao iFrame externo para validade e CVV (diferentes iFrames internos)
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 é o exemplo clássico desse padrão no mundo real. Cada campo de input (número do cartão, validade, CVV) vive em seu próprio iFrame aninhado separado por razões de conformidade PCI. Cada um precisa de sua própria cadeia de frameLocator.
Shadow DOM
Shadow DOM não é iFrame, mas causa o mesmo sintoma: locators padrão não encontram elementos. Shadow DOM é um recurso do navegador que encapsula o DOM interno de um componente. Web components, elementos customizados e algumas bibliotecas de UI o usam.
A boa notícia: os locators do Playwright atravessam o Shadow DOM por padrão. page.getByRole(), page.getByText() e page.locator() buscam através de shadow roots sem configuração extra.
test('localiza elementos dentro do Shadow DOM', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/components');
// Funciona mesmo que o botão esteja dentro de um shadow root
await page.getByRole('button', { name: 'Submit' }).click();
// Seletores CSS precisam do combinador >>> para atravessar Shadow DOM
await page.locator('custom-login-form >>> input[type="email"]').fill('user@example.com');
});Use >>> em seletores CSS quando precisar atravessar Shadow DOM explicitamente. Para a maioria dos casos, prefira locators semânticos (getByRole, getByLabel). Eles atravessam Shadow DOM automaticamente e não exigem conhecer a estrutura interna.
Erros comuns
Trocar de aba antes de carregar. O bug mais frequente: você obtém a referência da nova página comwaitForEvent('page') e imediatamente tenta clicar em algo. A página ainda está em branco. Sempre chame waitForLoadState() antes de interagir.
// Errado: race com a navegação
const newPage = await newPagePromise;
await newPage.getByRole('button', { name: 'Aceitar' }).click(); // Pode falhar
// Correto
const newPage = await newPagePromise;
await newPage.waitForLoadState('domcontentloaded');
await newPage.getByRole('button', { name: 'Aceitar' }).click();test('gerencia referências de múltiplas abas', async ({ page, context }) => {
const originalPage = page; // Renomeia para clareza ao trabalhar com múltiplas abas
const newPagePromise = context.waitForEvent('page');
await originalPage.getByRole('link', { name: 'Termos' }).click();
const termsPage = await newPagePromise;
await termsPage.waitForLoadState('load');
// Assertions na página de termos
await expect(termsPage.getByRole('heading', { name: 'Termos de Serviço' })).toBeVisible();
await termsPage.close();
// De volta à original. Use explicitamente originalPage, não page.
await expect(originalPage.getByRole('heading', { name: 'Cadastro' })).toBeVisible();
});page.frames() quando frameLocator() está disponível. A API mais antiga page.frames() retorna um array de objetos Frame. Funciona mas obriga a gerenciar índices ou nomes de frame manualmente. frameLocator() é encadeável, type-safe e funciona bem com o auto-waiting do Playwright. Use frameLocator() como padrão a menos que tenha um motivo específico para usar a API de frame diretamente.
FAQ
Posso fechar uma aba específica sem encerrar o teste?Sim. Chame await newPage.close() para fechar qualquer objeto Page. A página original e o contexto permanecem abertos e usáveis.
Use context.pages(), que retorna um array de todos os objetos Page abertos. O primeiro elemento é tipicamente a primeira aba aberta naquele contexto.
Sim. Encadeie chamadas frameLocator(): page.frameLocator('iframe#outer').frameLocator('iframe#inner'). Cada nível restringe o escopo ao documento aninhado.
waitForEvent('page') funciona para popups abertos por JavaScript (não por cliques em links)?
Sim. Qualquer chamada window.open() no navegador cria um evento Page no contexto, independente de ter sido disparado por um clique em link ou por JavaScript.
Certifique-se de que o iFrame existe no DOM no momento em que seu locator roda. Se for injetado dinamicamente, adicione um page.waitForSelector('iframe#my-frame') antes de usar frameLocator(). Verifique também que está selecionando o próprio elemento iFrame, não algo dentro dele. frameLocator() recebe o seletor da tag .
Sim, essa é uma das forças do Playwright em relação a ferramentas antigas baseadas em WebDriver. iFrames cross-origin funcionam com frameLocator() sem configuração especial.