page.getByText('Success').isVisible() retorna um booleano verificando o DOM exatamente uma vez: se o elemento ainda não renderizou, você recebe false mesmo que ele apareça 200ms depois. expect(page.getByText('Success')).toBeVisible() tenta novamente por até 5 segundos antes de falhar. Esse comportamento de retry é o núcleo do design de assertions do Playwright, e a maioria dos erros de iniciantes vem de não saber quais métodos o têm.

Como as assertions do Playwright funcionam

As assertions do Playwright usam a função expect() do @playwright/test. Não são as mesmas do expect do Jest. A versão do Playwright é assíncrona e tem lógica de retry nativa.

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

A diferença principal em relação à maioria dos frameworks de teste: as assertions do Playwright fazem retry automaticamente. Quando você escreve:

await expect(page.getByText('Welcome')).toBeVisible();

O Playwright não verifica só uma vez. Verifica repetidamente por até 5 segundos (o expect timeout padrão), aguardando a condição se tornar verdadeira. Isso elimina a necessidade de chamadas manuais de waitFor em 90% dos casos.

Se a condição nunca se tornar verdadeira dentro do timeout, o teste falha com uma mensagem clara mostrando o que era esperado e o que realmente existia.

Assertions de elemento (baseadas em locator)

Essas verificam propriedades de um elemento específico na página.

toBeVisible / toBeHidden

// Elemento está renderizado e visível para o usuário
await expect(page.getByText('Dashboard')).toBeVisible();

// Elemento não está presente, ou está presente mas oculto (display:none, visibility:hidden, opacity:0)
await expect(page.getByRole('dialog')).toBeHidden();

toBeHidden() é verdadeiro se o elemento não existe OU se existe mas está invisível. Use not.toBeAttached() se precisar confirmar especificamente que o elemento não está no DOM.

toHaveText / toContainText

// Correspondência exata de texto (remove espaços em branco automaticamente)
await expect(page.getByRole('heading', { level: 1 })).toHaveText('My Travel Items');

// Correspondência parcial de texto
await expect(page.getByRole('heading')).toContainText('Travel');

// Array: verificar texto de múltiplos elementos
await expect(page.getByRole('listitem')).toHaveText(['Tokyo', 'Paris', 'London']);

// Regex: correspondência por padrão
await expect(page.getByTestId('price')).toHaveText(/\$\d+\.\d{2}/);

toHaveText com array verifica o texto completo de cada elemento na ordem. Muito útil para confirmar linhas de tabela ou listas ordenadas.

toHaveValue

Para elementos <input>, <textarea> e <select>:

// Input de texto
await expect(page.getByLabel('Email')).toHaveValue('admin@becomeqa.com');

// Input vazio
await expect(page.getByLabel('Search')).toHaveValue('');

// Dropdown select
await expect(page.getByLabel('Status')).toHaveValue('completed');

toBeChecked / not.toBeChecked

Para checkboxes e radio buttons:

await expect(page.getByLabel('Lembrar de mim')).toBeChecked();
await expect(page.getByLabel('Assinar newsletter')).not.toBeChecked();

toBeEnabled / toBeDisabled

// Botão está clicável
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();

// Botão está desabilitado / tem atributo disabled
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled();

Use isso para verificar que um botão de submit só fica habilitado após os campos obrigatórios serem preenchidos.

toBeEditable / toBeReadOnly

await expect(page.getByLabel('Username')).toBeEditable();
await expect(page.getByLabel('Created At')).toBeReadOnly();

toHaveAttribute

Verifique qualquer atributo HTML:

// href em um link
await expect(page.getByRole('link', { name: 'Home' })).toHaveAttribute('href', '/');

// data-testid
await expect(page.getByTestId('status-badge')).toHaveAttribute('data-status', 'active');

// aria-label
await expect(page.getByRole('button', { name: 'Close' })).toHaveAttribute('aria-label', 'Close dialog');

// Correspondência de valor por regex
await expect(page.getByRole('img')).toHaveAttribute('src', /\/images\/.+\.svg/);

toHaveClass

// Elemento tem essa classe CSS
await expect(page.getByTestId('alert')).toHaveClass(/error/);

// Lista exata de classes
await expect(page.getByTestId('button')).toHaveClass('btn btn-primary active');

Atenção: toHaveClass verifica se a classe está presente, não se é a única. Use regex para correspondências parciais.

toHaveCount

Para coleções de elementos:

// Tabela tem cabeçalho + 5 linhas de dados
await expect(page.getByRole('row')).toHaveCount(6);

// Dropdown tem 4 opções
await expect(page.getByRole('option')).toHaveCount(4);

// Sem mensagens de erro
await expect(page.getByRole('alert')).toHaveCount(0);

toHaveCSS

Verifique propriedades CSS computadas:

await expect(page.getByTestId('error-message')).toHaveCSS('color', 'rgb(220, 38, 38)');
await expect(page.getByRole('dialog')).toHaveCSS('display', 'flex');

Use valores computados (rgb(...)) e não variáveis CSS ou propriedades shorthand.

Assertions de página

Essas verificam propriedades da página inteira, não de um elemento específico.

toHaveURL

// URL exata
await expect(page).toHaveURL('https://lab.becomeqa.com/dashboard');

// Correspondência parcial com regex
await expect(page).toHaveURL(/\/dashboard/);

// Com baseURL configurada no playwright.config.ts
await expect(page).toHaveURL('/dashboard');

Use após uma navegação para confirmar que você chegou onde esperava.

toHaveTitle

await expect(page).toHaveTitle('My Travel Items | BecomeQA Lab');
await expect(page).toHaveTitle(/BecomeQA/);

toHaveScreenshot (regressão visual)

// Primeira execução cria a screenshot de referência
// Execuções seguintes comparam com ela
await expect(page).toHaveScreenshot('dashboard.png');

// Com opções
await expect(page).toHaveScreenshot('dashboard.png', {
    maxDiffPixels: 100,    // permite diferenças menores de pixels
    threshold: 0.2,        // escala 0-1 de tolerância de diferença de cor
});

Testes de regressão visual: a primeira execução salva uma imagem de referência. Todas as execuções seguintes comparam o estado atual com essa referência. Falha se diferem além do threshold.

Assertions de resposta de API

Ao usar a fixture request para testes de API:

test('GET /api/items retorna dados válidos', async ({ request }) => {
    const response = await request.get('/api/items');
    
    // Status code
    expect(response.status()).toBe(200);
    expect(response.ok()).toBeTruthy(); // true para 200-299
    
    // Body da resposta
    const items = await response.json();
    expect(items).toHaveLength(5);
    expect(items[0]).toHaveProperty('id');
    expect(items[0]).toHaveProperty('title');
    expect(items[0].status).toBe('planned');
    
    // Headers
    expect(response.headers()['content-type']).toContain('application/json');
});

Atenção: response.ok() é um método nativo (não uma assertion) que retorna true para status codes 2xx.

Assertions de valores genéricos

Para valores que não são elementos, como variáveis, respostas de API e valores computados:

// Igualdade
expect(items.length).toBe(5);
expect(user.role).toBe('admin');

// Diferente
expect(errorCode).not.toBe(0);

// Truthy/falsy
expect(isVisible).toBeTruthy();
expect(errorMessage).toBeFalsy();

// Null/undefined
expect(result).toBeNull();
expect(result).not.toBeNull();
expect(result).toBeDefined();
expect(result).toBeUndefined();

// Números
expect(count).toBeGreaterThan(0);
expect(price).toBeGreaterThanOrEqual(9.99);
expect(discount).toBeLessThan(100);

// Arrays
expect(statuses).toContain('completed');
expect(items).toHaveLength(3);
expect(tags).toEqual(['qa', 'automation', 'playwright']); // correspondência exata de array

// Objetos
expect(user).toMatchObject({ email: 'admin@becomeqa.com', role: 'admin' }); // correspondência parcial
expect(user).toEqual({ id: 1, email: 'admin@becomeqa.com', role: 'admin' }); // correspondência exata

// Strings
expect(message).toContain('success');
expect(slug).toMatch(/^[a-z0-9-]+$/);

Negação: not

Toda assertion pode ser negada com .not:

await expect(page.getByRole('dialog')).not.toBeVisible();
await expect(page.getByText('Error')).not.toBeAttached();
expect(response.status()).not.toBe(404);

Soft assertions: coletar todas as falhas

Por padrão, a primeira assertion com falha para o teste. Soft assertions continuam rodando mesmo após uma falha e reportam todas no final:

test('dados do dashboard estão corretos', async ({ page }) => {
    await page.goto('/dashboard');
    
    // Essas não param na primeira falha
    await expect.soft(page.getByRole('heading')).toHaveText('My Travel Items');
    await expect.soft(page.getByRole('row')).toHaveCount(6);
    await expect.soft(page).toHaveURL('/dashboard');
    
    // Isso lança erro se qualquer soft assertion acima falhou
    expect(test.info().errors).toHaveLength(0);
});

Use soft assertions quando quiser ver tudo que está quebrado em uma execução, não apenas a primeira falha.

Mensagens de assertion customizadas

Quando uma assertion falha, o Playwright mostra o que era esperado vs. o que foi encontrado. Você pode adicionar uma mensagem customizada para tornar as falhas mais legíveis:

await expect(page.getByRole('heading'), 'Página deve mostrar o dashboard após login')
    .toHaveText('My Travel Items');

expect(items.length, `Esperava 5 itens mas encontrou ${items.length}`)
    .toBe(5);

Configurando timeouts

O timeout padrão de assertion é 5 segundos. Você pode sobrescrevê-lo por assertion ou globalmente:

// Por assertion (10 segundos para uma operação lenta)
await expect(page.getByText('Relatório pronto')).toBeVisible({ timeout: 10000 });

// Global no playwright.config.ts
export default defineConfig({
    expect: {
        timeout: 10000,  // todas as assertions aguardam até 10 segundos
    },
});

Não aumente timeouts para corrigir testes flaky. Um teste flaky com timeout maior continua flaky. Só falha mais devagar. Corrija a causa raiz.

Erros comuns

Usar page.locator().isVisible() em vez de expect().toBeVisible()

// Errado: verifica uma vez, sem retry, retorna booleano
const visible = await page.getByText('Success').isVisible();
expect(visible).toBe(true);

// Certo: faz retry até estar visível ou timeout
await expect(page.getByText('Success')).toBeVisible();

A primeira versão pode falhar de forma intermitente porque verifica exatamente uma vez. A segunda faz retry.

Assertar em locators desatualizados

// Não salve locators antes da navegação e assertar depois
const heading = page.getByRole('heading');
await page.goto('/nova-pagina');
await expect(heading).toHaveText('Nova Página'); // pode estar stale

// Melhor: crie o locator próximo à assertion
await page.goto('/nova-pagina');
await expect(page.getByRole('heading')).toHaveText('Nova Página');

Usar expect(await locator.textContent()).toBe(...) em vez de toHaveText

// Errado: avalia uma vez, sem retry
expect(await page.getByRole('heading').textContent()).toBe('Dashboard');

// Certo: faz retry com auto-waiting
await expect(page.getByRole('heading')).toHaveText('Dashboard');

Assertar count em listas dinâmicas

Se uma lista carrega de forma assíncrona, assertar o count após confirmar que a lista está visível:

await expect(page.getByRole('list')).toBeVisible(); // espera a lista aparecer
await expect(page.getByRole('listitem')).toHaveCount(5); // depois conta os itens

Um teste completo usando múltiplos tipos de assertion

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

test('usuário pode adicionar e visualizar um item de viagem', async ({ page }) => {
    await page.goto('/');
    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();

    // Assertion de página: verificar navegação
    await expect(page).toHaveURL('/dashboard');

    // Assertion de visibilidade
    await expect(page.getByRole('heading', { level: 1 })).toBeVisible();

    // Assertion de texto
    await expect(page.getByRole('heading', { level: 1 })).toHaveText('My Travel Items');

    // Adicionar um novo item
    await page.getByRole('button', { name: 'Add Item' }).click();
    await page.getByLabel('Destination').fill('Tokyo');
    await page.getByRole('button', { name: 'Save' }).click();

    // Assertion de count: tabela deve ter mais uma linha
    const rows = page.getByRole('row');
    await expect(rows).toHaveCount(7); // cabeçalho + 6 itens

    // Assertion de conteúdo de texto na nova linha
    await expect(page.getByRole('cell', { name: 'Tokyo' })).toBeVisible();

    // Assertion de status no novo item
    const tokyoRow = page.getByRole('row').filter({ hasText: 'Tokyo' });
    await expect(tokyoRow.getByRole('cell').last()).toHaveText('Planned');
});

FAQ

Por que minha assertion passa localmente mas falha no CI?

Timing. Máquinas de CI são mais lentas. O elemento existe mas demora mais para aparecer. Aumente o timeout de assertion no playwright.config.ts ou investigue por que o elemento carrega mais devagar em ambientes de CI.

Qual a diferença entre toEqual e toBe?

toBe verifica igualdade por referência (mesmo objeto na memória, ou primitivos idênticos). toEqual verifica igualdade profunda (mesma estrutura e valores, funciona para objetos e arrays). Para comparar objetos e arrays, use toEqual. Para strings, números e booleanos, use toBe.

Quando usar toMatchObject vs toEqual?

toMatchObject é uma correspondência parcial. O objeto real pode ter mais propriedades do que você especifica. toEqual exige uma correspondência exata. Para respostas de API onde você quer verificar campos principais sem listar todos, use toMatchObject.

Meu toHaveText falha porque o texto real tem espaço em branco extra. Como corrigir?

toHaveText remove espaços em branco no início e no final automaticamente. Para espaço em branco interno (múltiplos espaços, quebras de linha), use regex: toHaveText(/destination:\s+Tokyo/i).

→ Veja também: Fixtures Personalizados no Playwright: O Padrão que Torna os Testes Legíveis | Testes Instáveis: Por que Acontecem e Como Eliminá-los