Esquecer await em uma assertion do Playwright é um bug silencioso: expect(page.getByText('Error')).toBeVisible() sem await sempre passa porque avalia um objeto Promise, não o estado real da página. O JavaScript não espera operações do navegador terminarem por padrão. Todo método do Playwright que toca o navegador retorna uma Promise que precisa ser aguardada antes de o resultado ser usável.

O problema que async/await resolve

JavaScript é não-bloqueante por padrão. Quando você instrui o navegador a fazer algo, como navegar para uma URL ou clicar em um botão, o JavaScript não espera a operação terminar. Ele passa para a próxima linha imediatamente.

Para testes, isso é um problema. Você precisa navegar para a página antes de clicar no botão. Precisa clicar antes de verificar o resultado.

Antes do async/await, a solução eram callbacks e Promises. Funcionam, mas produzem código embaralhado e difícil de debugar. async/await é a solução moderna: faz operações assíncronas parecerem e se comportarem como sequenciais.

A explicação mais simples

await significa: "espere aqui até essa operação terminar, depois continue."

// Sem await: não espera, falha ou se comporta de forma inesperada
test('exemplo quebrado', async ({ page }) => {
  page.goto('https://lab.becomeqa.com'); // inicia a navegação e continua imediatamente
  page.getByRole('button', { name: 'Login' }).click(); // tenta clicar antes da página carregar
});

// Com await: espera cada passo terminar
test('exemplo correto', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com'); // aguarda a navegação completar
  await page.getByRole('button', { name: 'Login' }).click(); // então clica
});

O segundo teste funciona porque cada await pausa a execução até a operação completar.

O que async significa

async antes de uma função significa que ela pode usar await internamente e vai retornar uma Promise:

// Essa função é async — ela usa await
test('teste de login', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com');
  // ...
});

Em testes Playwright, todas as funções de teste são async automaticamente porque todas envolvem operações do navegador. O runner do Playwright cuida disso. Você só precisa lembrar de colocar await antes das operações.

A regra: quando usar await no Playwright

Use await antes de qualquer método Playwright que:

  • Interage com o navegador (navegação, cliques, preenchimentos, seleções)
  • Retorna informações da página (conteúdo de texto, valores de atributos, contagens)
  • Faz assertions (expect(...).toBeVisible())

test('demonstra o uso de await', async ({ page }) => {
  // Navegação — await
  await page.goto('https://lab.becomeqa.com');

  // Ações — await
  await page.getByRole('button', { name: 'Login' }).click();
  await page.getByLabel('Username').fill('admin@becomeqa.com');
  await page.getByLabel('Password').fill('testpass123');
  await page.getByRole('button', { name: 'Submit' }).click();

  // Assertions — await
  await expect(page.getByRole('heading', { name: 'My Travel Items' })).toBeVisible();

  // Obtendo valores da página — await
  const heading = await page.getByRole('heading').textContent();
  console.log(heading); // 'My Travel Items'

  // Verificando visibilidade — await
  const isVisible = await page.getByRole('button', { name: 'Add Item' }).isVisible();
  expect(isVisible).toBe(true);
});

O que acontece quando você esquece o await

As consequências dependem do que você esqueceu:

Esqueceu await em uma ação:

// Isso preenche o campo e continua imediatamente — às vezes funciona por acaso
// mas vai falhar sob qualquer carga ou rede lenta
page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByRole('button', { name: 'Submit' }).click(); // pode clicar antes do fill terminar

Esqueceu await em uma assertion:

// Erro muito comum — a assertion avalia um objeto Promise
// que é sempre truthy, então o teste sempre passa mesmo quando deveria falhar
expect(page.getByText('Error')).toBeVisible(); // ERRADO — falta await, teste sempre passa
await expect(page.getByText('Error')).toBeVisible(); // CORRETO

Esse último caso é particularmente perigoso: testes que sempre passam são piores do que testes que sempre falham. O TypeScript ajuda a pegar isso. Com strict mode ativado, o TypeScript avisa quando você tem uma Promise não aguardada em uma assertion.

Armazenando valores com await

Quando você precisa capturar o que o Playwright retorna:

test('verificando valores', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com');
  
  // Armazenar conteúdo de texto
  const title = await page.getByRole('heading').textContent();
  // title agora é uma string: 'My Travel Items'
  
  // Armazenar uma contagem
  const rowCount = await page.getByRole('row').count();
  // rowCount é um número: 5

  // Armazenar status de visibilidade
  const isButtonVisible = await page.getByRole('button', { name: 'Add Item' }).isVisible();
  // isButtonVisible é um boolean: true ou false
  
  // Agora use os valores
  expect(title).toBe('My Travel Items');
  expect(rowCount).toBeGreaterThan(0);
  expect(isButtonVisible).toBe(true);
});

O padrão é sempre: const resultado = await page.algumMetodo().

Funções assíncronas fora dos testes

Quando você escreve funções auxiliares ou métodos de Page Object que usam Playwright, eles precisam ser async também:

// Método de Page Object — precisa ser async
class LoginPage {
  constructor(private page: Page) {}

  async login(email: string, password: string) {
    await this.page.getByRole('button', { name: 'Login' }).click();
    await this.page.getByLabel('Username').fill(email);
    await this.page.getByLabel('Password').fill(password);
    await this.page.getByRole('button', { name: 'Submit' }).click();
  }
}

// Quando você chama um método async, precisa de await
test('teste de login', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.login('admin@becomeqa.com', 'testpass123'); // await no método async
});

A regra: se uma função usa await internamente, declare-a como async. Se você chama uma função async, coloque await antes da chamada.

Operações paralelas com Promise.all

Às vezes você quer que duas coisas aconteçam simultaneamente. Promise.all executa múltiplas operações assíncronas em paralelo e espera todas terminarem:

// Aguarda navegação E um elemento específico aparecer simultaneamente
// Isso é mais rápido do que aguardar sequencialmente
await Promise.all([
  page.waitForURL('/dashboard'),
  expect(page.getByRole('heading', { name: 'My Travel Items' })).toBeVisible(),
]);

No Playwright, o uso mais comum é aguardar uma navegação e uma assertion juntas após clicar em algo. O Playwright lida com a maioria disso automaticamente via auto-waiting, mas você vai ver Promise.all em código mais antigo e em cenários de timing específicos.

O que Promises realmente são

Quando você vê algo assim:

const textContent = page.getByRole('heading').textContent();
// textContent agora é uma Promise<string>, não uma string

Uma Promise é um placeholder para um valor que ainda não existe, porque a operação ainda está rodando. await desempacota a Promise, pausando a execução até o valor estar pronto:

const textContent = await page.getByRole('heading').textContent();
// Agora textContent é uma string

Você não precisa entender Promises em profundidade para escrever testes Playwright. O modelo mental "await faz esperar" é suficiente para 95% do código de teste. Lembre só que todo método Playwright retorna uma Promise, então você precisa de await.

O TypeScript ajuda a não esquecer

O TypeScript sabe quais métodos retornam Promises. Se você esquecer o await, ele vai avisar:

Type 'Promise<string>' is not assignable to type 'string'

Esse é um dos motivos para usar TypeScript em testes Playwright: o compilador pega await faltando antes dos testes rodarem.

Referência rápida

| O que você está fazendo | Padrão |

|---|---|

| Navegar | await page.goto(url) |

| Clicar | await element.click() |

| Preencher input | await element.fill('texto') |

| Verificar visibilidade | await expect(element).toBeVisible() |

| Obter texto | const text = await element.textContent() |

| Obter contagem | const n = await elements.count() |

| Chamar função async | await minhaFuncaoAsync() |

| Executar em paralelo | await Promise.all([op1, op2]) |

FAQ

Preciso entender como funciona o event loop do JavaScript?

Não, não para escrever testes Playwright. "await faz esperar pelo resultado" é suficiente. O event loop é o mecanismo por baixo, mas você não precisa entender o mecanismo para usar a ferramenta.

Por que meu teste passa mesmo quando esqueci await no expect?

Porque expect(element).toBeVisible() sem await retorna um objeto Promise (que é truthy), e o test runner não avalia isso como uma assertion. O teste não vê falhas. É um bug silencioso. O strict mode do TypeScript sinaliza isso.

Posso usar .then() em vez de await?

Sim, .then() é a sintaxe antiga de Promise. Funciona, mas produz código mais difícil de ler:

// Sintaxe antiga com Promise
page.goto('https://lab.becomeqa.com').then(() => {
  return page.getByRole('button', { name: 'Login' }).click();
}).then(() => {
  // ...
});

// Equivalente com async/await — muito mais claro
await page.goto('https://lab.becomeqa.com');
await page.getByRole('button', { name: 'Login' }).click();

Use async/await. Evite cadeias de .then() em código de teste novo.

Vejo Promise no TypeScript. O que significa? void significa que a função não retorna um valor útil. Ela só faz algo (como navegar ou clicar). Você ainda precisa de await, mas não armazena o resultado em uma variável. → Veja também: TypeScript para QA: Por que os Tipos Estáticos Melhoram Seus Testes | Começando com Playwright: Seus Primeiros Testes em 30 Minutos