page.waitForTimeout(2000) antes de uma asserção quase sempre está errado. As asserções do Playwright já fazem polling até passar, então o sleep é redundante em execuções rápidas e ainda insuficiente nas lentas. O padrão correto é deixar a asserção fazer a espera, ou usar waitForResponse dentro de Promise.all para capturar uma chamada de API específica antes de verificar o resultado.

Como o auto-waiting do Playwright funciona

Quando você escreve await page.getByRole('button', { name: 'Submit' }).click(), o Playwright não clica imediatamente. Ele aguarda o botão estar:

  • Anexado ao DOM
  • Visível (não oculto, não display: none)
  • Estável (sem animação em andamento)
  • Habilitado (não desabilitado)
  • Recebendo eventos de ponteiro (não coberto por outro elemento)

Isso acontece automaticamente, até o actionTimeout (padrão: 30 segundos). Você não escreve nenhum código de espera; o Playwright cuida disso.

Por isso o Playwright é mais rápido de escrever do que o Selenium. No Selenium, você escreveria WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ROLE, 'button'))). No Playwright: só clica.

Quando o auto-waiting não é suficiente

O auto-waiting funciona para interações com elementos. Mas não trata tudo:

Navegação de página: após submit de formulário ou clique em link, a URL muda. O auto-waiting não vai aguardar a nova página carregar completamente antes da próxima ação. Carregamento de dados: um elemento existe e está visível, mas exibe um spinner enquanto os dados carregam. O Playwright pode clicar nele antes dos dados chegarem. Múltiplas requisições de rede: um carregamento de página dispara três chamadas de API. O Playwright vê que o DOM está pronto, mas a terceira chamada ainda não terminou. Conclusão de animação: um elemento está tecnicamente visível mas no meio de uma animação. O auto-waiting trata transições CSS simples, mas não todos os estados de animação.

expect como ferramenta de espera

O padrão de espera mais subutilizado no Playwright: as asserções aguardam.

// Aguarda até o timeout para a URL casar
await expect(page).toHaveURL('/dashboard');

// Aguarda o texto aparecer
await expect(page.getByRole('heading')).toHaveText('Order confirmed');

// Aguarda a contagem de elementos chegar a 5
await expect(page.getByRole('listitem')).toHaveCount(5);

Toda asserção expect no Playwright faz polling até passar ou expirar. Isso torna as asserções a forma mais limpa de aguardar o estado da aplicação.

Anti-padrão:

await page.waitForTimeout(2000); // nunca faça isso
await expect(page.getByRole('heading')).toHaveText('Order confirmed');

Correto:

await expect(page.getByRole('heading')).toHaveText('Order confirmed');

O expect faz a espera. O waitForTimeout é um sinal de que você não sabe o que esperar.

Ferramentas de espera explícita

Quando você realmente precisa de esperas explícitas, o Playwright oferece opções específicas:

waitForURL

await page.getByRole('button', { name: 'Log in' }).click();
await page.waitForURL('/dashboard');
// Agora é seguro verificar o conteúdo do dashboard

waitForResponse: aguardar uma chamada de API específica

const [response] = await Promise.all([
  page.waitForResponse(resp => resp.url().includes('/api/orders') && resp.status() === 200),
  page.getByRole('button', { name: 'Place order' }).click(),
]);
const orderData = await response.json();
expect(orderData.status).toBe('created');

Inicie a espera antes da ação que dispara a requisição. O Promise.all garante que você não perca uma resposta rápida.

waitForRequest: verificar se uma requisição foi feita

const [request] = await Promise.all([
  page.waitForRequest(req => req.url().includes('/api/track') && req.method() === 'POST'),
  page.getByRole('button', { name: 'Purchase' }).click(),
]);
// Verifica que o evento de analytics foi disparado
expect(request.postDataJSON()).toMatchObject({ event: 'purchase' });

waitForSelector: aguardar um estado de elemento

// Aguarda o spinner de carregamento desaparecer
await page.waitForSelector('.spinner', { state: 'detached' });

// Aguarda o elemento ficar visível
await page.waitForSelector('[data-testid="results-table"]', { state: 'visible' });

Opções de state: 'attached', 'detached', 'visible', 'hidden'.

Prefira expect(locator).toBeVisible() em vez de waitForSelector; a abordagem com asserção é mais legível.

waitForLoadState

await page.goto('/heavy-page');
await page.waitForLoadState('networkidle'); // Aguarda até não haver atividade de rede por 500ms

Opções de loadState:

  • 'load': evento window.load disparado (padrão para goto)
  • 'domcontentloaded': DOM parseado, antes de imagens e scripts
  • 'networkidle': sem requisições de rede por 500ms
'networkidle' é lento e frágil. Evite, a menos que a página genuinamente não tenha outra forma de sinalizar "pronto". Prefira aguardar um elemento específico.

Configuração de timeouts

Os timeouts são configuráveis em três níveis:

// playwright.config.ts — aplica a todos os testes
export default defineConfig({
  timeout: 30000,          // Timeout do teste (teste inteiro)
  expect: {
    timeout: 5000,         // Timeout de asserção
  },
  use: {
    actionTimeout: 15000,  // Timeout de ação individual (click, fill, etc.)
    navigationTimeout: 30000,
  },
});

Sobrescreva por teste:

test('carregamento lento de dados', async ({ page }) => {
  test.setTimeout(60000); // Este teste tem 60 segundos
  // ...
});

Sobrescreva por asserção:

await expect(page.getByText('Report generated')).toBeVisible({ timeout: 30000 });

A regra do waitForTimeout

page.waitForTimeout(ms) é um sleep. Às vezes é necessário como último recurso, por exemplo, para aguardar um script de terceiros que você não consegue observar. Mas trate cada ocorrência como um TODO: o que você realmente deveria estar aguardando aqui?

Se você se pegar escrevendo await page.waitForTimeout(1000) em mais de um ou dois testes, sua suite tem um problema estrutural de espera que vale corrigir.

→ Veja também: Depurando Testes Instáveis: Um Guia Prático | Testes Instáveis: Por que Acontecem e Como Eliminá-los | Assertions no Playwright: O Guia Completo