Um locator CSS como button.btn-primary-v2 quebra no momento em que um desenvolvedor renomeia essa classe. getByRole('button', { name: 'Submit' }) sobrevive ao rename porque encontra o botão da forma que um usuário faz: por role e label. Os seis tipos de locator do Playwright são ordenados do mais ao menos recomendado, e getByRole fica em primeiro porque seu modo de falha corresponde ao impacto real no usuário. Se o nome acessível muda, o teste deve quebrar.

Por que locators importam

A causa mais comum de testes flaky não é timing. São locators frágeis. Um teste que encontra um botão pela classe CSS (button.btn-primary-v2) quebra no momento em que um desenvolvedor renomeia essa classe. Um teste que encontra um botão por role e label (getByRole('button', { name: 'Submit' })) sobrevive a qualquer mudança CSS. Procura o botão da mesma forma que um usuário faz: pelo que ele diz e pelo que faz.

O Playwright oferece seis tipos de locator, listados aqui do mais ao menos recomendado.

getByRole: use este primeiro

getByRole encontra elementos pelo role ARIA e nome acessível. Este é o locator que o Playwright recomenda como padrão, e por boas razões: é como usuários e leitores de tela identificam elementos. Se o nome acessível muda, o teste deve quebrar, porque isso é uma mudança real de UX.

// Botões
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('button', { name: 'Login' }).click();
await page.getByRole('button', { name: 'Delete item' }).click();

// Links
await page.getByRole('link', { name: 'Home' }).click();
await page.getByRole('link', { name: 'Ver detalhes' }).click();

// Headings
await expect(page.getByRole('heading', { name: 'My Travel Items' })).toBeVisible();
await expect(page.getByRole('heading', { level: 1 })).toContainText('Dashboard');

// Elementos de formulário
await page.getByRole('textbox', { name: 'Search' }).fill('Tokyo');
await page.getByRole('checkbox', { name: 'Lembrar de mim' }).check();
await page.getByRole('combobox', { name: 'Status' }).selectOption('active');

// Tabelas
const rows = page.getByRole('row');
await expect(rows).toHaveCount(6); // 1 cabeçalho + 5 linhas de dados

Roles ARIA comuns que você vai usar: button, link, heading, textbox, checkbox, radio, combobox (dropdown), listitem, row, cell, dialog, table, navigation, main.

A opção name corresponde ao nome acessível do elemento. Para botões e links, é o texto visível. Para inputs, é o label associado. Por padrão é case-insensitive.

// exact: false (padrão) — correspondência parcial
page.getByRole('button', { name: 'sub' }) // corresponde a "Submit", "Subscribe"

// exact: true — correspondência completa apenas
page.getByRole('button', { name: 'Submit', exact: true }) // só "Submit"

getByLabel: para inputs de formulário

getByLabel encontra um input, select ou textarea pelo elemento associado. Este é o locator certo para formulários de login, barras de busca e qualquer campo de formulário com label visível.

await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByLabel('Endereço de email').fill('user@example.com');
await page.getByLabel('Data de nascimento').fill('1990-01-15');

Funciona independente de o label usar for/id, aria-label ou envolver o input. Você não precisa saber como o label está implementado. O Playwright descobre.

<!-- Os três abaixo são encontrados por getByLabel('Email') -->
<label for="email">Email</label><input id="email" />
<label><span>Email</span><input /></label>
<input aria-label="Email" />

getByPlaceholder: quando não há label

Alguns inputs têm texto de placeholder em vez de um label visível. getByPlaceholder lida com esse caso.

await page.getByPlaceholder('Buscar destinos...').fill('Tokyo');
await page.getByPlaceholder('Digite seu email').fill('test@example.com');

Prefira getByLabel quando um label existir. getByPlaceholder é para inputs que só têm texto de placeholder.

getByText: para elementos não interativos

getByText encontra elementos pelo conteúdo de texto visível. Use para texto que você quer verificar que existe na página, não para elementos que você quer clicar (use getByRole para esses).

// Verifica que o texto está presente
await expect(page.getByText('My Travel Items')).toBeVisible();
await expect(page.getByText('Credenciais inválidas')).toBeVisible();

// Correspondência exata vs parcial
page.getByText('Travel')          // corresponde a "My Travel Items", "Guia de viagens"
page.getByText('Travel', { exact: true })  // só o "Travel" exato

// Com escopo para um tipo específico de elemento
page.locator('p').getByText('Erro ocorreu')  // só elementos <p>

getByText encontra todos os elementos que contêm esse texto, incluindo containers pai. Se "Submit" aparece em um parágrafo e em um botão, getByText('Submit') retorna múltiplos elementos. Para elementos interativos, use getByRole.

getByTestId: o contrato explícito

getByTestId encontra elementos pelo atributo data-testid (configurável). Este é o locator para usar quando desenvolvedores adicionam explicitamente ganchos de teste ao DOM.

<button data-testid="submit-payment">Pagar agora</button>
<div data-testid="success-message">Pagamento concluído</div>

await page.getByTestId('submit-payment').click();
await expect(page.getByTestId('success-message')).toBeVisible();

A vantagem: atributos data-testid são invisíveis para usuários e não têm significado funcional, então desenvolvedores não vão renomeá-los acidentalmente. A desvantagem: alguém precisa adicioná-los ao código. Para apps que você controla, isso é tranquilo. Para apps de terceiros, você fica preso com a estrutura existente.

Se seu time ainda não usa data-testid, proponha. Peça aos desenvolvedores para adicionar atributos data-testid nos elementos interativos principais. Leva minutos por componente e torna os locators trivialmente estáveis.

getByAltText e getByTitle

Dois locators menos comuns, mas ocasionalmente úteis:

// Imagens com texto alt
await page.getByAltText('Foto de perfil do usuário').click();
await expect(page.getByAltText('Logo da empresa')).toBeVisible();

// Elementos com atributos title
await page.getByTitle('Fechar diálogo').click();

Você vai usar esses raramente. A maioria dos elementos interativos deve ser alcançável via getByRole.

Encadeando locators

Quando você precisa chegar a um elemento específico dentro de um container, encadeie locators:

// Encontra a linha contendo "Tokyo", depois clica no botão Deletar
const tokyoRow = page.getByRole('row').filter({ hasText: 'Tokyo' });
await tokyoRow.getByRole('button', { name: 'Deletar' }).click();

// Encontra uma seção de formulário, depois interage com seu input
const addressSection = page.locator('.address-section');
await addressSection.getByLabel('Cidade').fill('São Paulo');

filter({ hasText: '...' }) restringe um locator a elementos contendo texto específico. Combinado com nth() para seleção por índice:

// Primeira linha na tabela (índice 0)
const firstRow = page.getByRole('row').nth(1); // nth(0) é cabeçalho, nth(1) é primeira linha de dados
await firstRow.getByRole('button', { name: 'Editar' }).click();

O que evitar

Seletores CSS. Frágeis, específicos à implementação, quebram em refatorações:

// Ruim
page.locator('.btn.btn-primary')
page.locator('#submit-button')
page.locator('div > ul > li:nth-child(3)')

XPath. Verboso, frágil, difícil de ler:

// Ruim
page.locator('//button[@class="btn btn-primary" and text()="Submit"]')

Seletores de texto sem contexto. Ambíguos quando o texto aparece em múltiplos lugares:

// Arriscado — e se "Editar" aparecer em múltiplos lugares?
page.getByText('Editar')
// Melhor — com escopo para a linha que interessa
page.getByRole('row').filter({ hasText: 'Tokyo' }).getByRole('button', { name: 'Editar' })

Exercício prático em lab.becomeqa.com

Abra https://lab.becomeqa.com e tente escrever locators para:

1. O botão de Login na navegação

2. Os inputs de Username e Password no modal de login

3. O botão Submit no modal de login

4. A linha da tabela contendo um destino específico após o login

5. O botão Add Item no dashboard

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

test('prática de locators', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com');

  // 1. Botão de login na navegação
  await page.getByRole('button', { name: 'Login' }).click();

  // 2 e 3. Formulário de login
  await page.getByLabel('Username').fill('admin@becomeqa.com');
  await page.getByLabel('Password').fill('testpass123');
  await page.getByRole('button', { name: 'Submit' }).click();

  // 4. Linha específica na tabela
  const parisRow = page.getByRole('row').filter({ hasText: 'Paris' });
  await expect(parisRow).toBeVisible();

  // 5. Botão de adicionar item
  await expect(page.getByRole('button', { name: 'Add Item' })).toBeVisible();
});

FAQ

Quando devo usar locator() diretamente em vez dos métodos getBy*?

Quando você precisa de seletores CSS ou de atributo que os métodos getBy* não cobrem. Por exemplo, page.locator('[data-status="active"]') encontra todos os elementos com um valor específico de data attribute. Use como último recurso, não como primeira escolha.

Posso combinar múltiplos locators?

Sim. locator.and(otherLocator) encontra elementos que correspondem aos dois:

// Um botão que é tanto visível quanto tem o texto "Submit"
page.getByRole('button').and(page.getByText('Submit'))

E se dois elementos corresponderem ao meu locator?

O Playwright lança strict mode violation se um locator corresponder a mais de um elemento quando você tenta interagir com ele. Corrija tornando o locator mais específico: adicione um filter, use nth(), ou limite-o a um container pai.

Como debugar um locator que não está encontrando nada?

Use o modo highlight no Playwright Inspector: PWDEBUG=1 npx playwright test. Ou chame await locator.highlight() no seu teste para marcar visualmente o elemento correspondido durante uma execução com interface gráfica.

→ Veja também: Assertions no Playwright: O Guia Completo | Começando com Playwright: Seus Primeiros Testes em 30 Minutos | Playwright Codegen: Grave Testes Sem Escrever Código | Como Ler Mensagens de Erro do Playwright