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 dadosRoles 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.
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)')// Ruim
page.locator('//button[@class="btn btn-primary" and text()="Submit"]')// 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 usarlocator() 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.
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'))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.
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.