Un locator CSS como button.btn-primary-v2 se rompe en el momento en que un desarrollador renombra esa clase. getByRole('button', { name: 'Submit' }) sobrevive al cambio porque encuentra el botón de la misma manera que lo haría un usuario: por rol y etiqueta. Los seis tipos de locators de Playwright están ordenados de más a menos recomendados, y getByRole va primero porque su modo de fallo coincide con el impacto real en el usuario: si cambia el nombre accesible, el test debería romperse. Esta guía cubre cada tipo, cuándo usar getByLabel o getByTestId en su lugar, encadenamiento con filter(), y qué significa strict mode violation cuando un locator coincide con más de un elemento.

Por qué importan los locators

La causa más común de tests inestables no es el timing. Son los locators frágiles. Un test que encuentra un botón por su clase CSS (button.btn-primary-v2) se rompe en el momento en que un desarrollador renombra esa clase. Un test que encuentra un botón por su rol y etiqueta (getByRole('button', { name: 'Submit' })) sobrevive cualquier cambio de CSS porque busca el botón de la misma manera que lo hace un usuario: por lo que dice y lo que hace.

Playwright te da seis tipos de locators. Están listados aquí de más a menos recomendados.

getByRole: usalo primero

getByRole encuentra elementos por su rol ARIA y nombre accesible. Es el locator que Playwright recomienda por defecto, y por buena razón: es así como los usuarios y los lectores de pantalla identifican los elementos. Si el nombre accesible cambia, el test debería romperse, porque eso es un cambio real de UX.

// Botones
await page.getByRole('button', { name: 'Enviar' }).click();
await page.getByRole('button', { name: 'Login' }).click();
await page.getByRole('button', { name: 'Eliminar ítem' }).click();

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

// Encabezados
await expect(page.getByRole('heading', { name: 'Mis Ítems de Viaje' })).toBeVisible();
await expect(page.getByRole('heading', { level: 1 })).toContainText('Dashboard');

// Elementos de formulario
await page.getByRole('textbox', { name: 'Buscar' }).fill('Tokio');
await page.getByRole('checkbox', { name: 'Recordarme' }).check();
await page.getByRole('combobox', { name: 'Estado' }).selectOption('active');

// Tablas
const rows = page.getByRole('row');
await expect(rows).toHaveCount(6); // 1 encabezado + 5 filas de datos

Roles ARIA comunes que usarás: button, link, heading, textbox, checkbox, radio, combobox (dropdown), listitem, row, cell, dialog, table, navigation, main.

La opción name coincide con el nombre accesible del elemento. Para botones y links, es el texto visible. Para inputs, es el label asociado. No distingue mayúsculas y minúsculas por defecto.

// exact: false (por defecto): coincidencia parcial
page.getByRole('button', { name: 'env' }) // coincide con "Enviar", "Envoltura"

// exact: true: solo coincidencia completa
page.getByRole('button', { name: 'Enviar', exact: true }) // solo "Enviar"

getByLabel: para inputs de formulario

getByLabel encuentra un input, select o textarea por su elemento asociado. Es el locator correcto para formularios de login, barras de búsqueda y cualquier campo de formulario que tenga un label visible.

await page.getByLabel('Nombre de usuario').fill('admin@becomeqa.com');
await page.getByLabel('Contraseña').fill('testpass123');
await page.getByLabel('Correo electrónico').fill('usuario@ejemplo.com');
await page.getByLabel('Fecha de nacimiento').fill('1990-01-15');

Funciona tanto si el label usa for/id, aria-label, o envuelve el input. No necesitas saber cómo está implementado el label. Playwright lo resuelve solo.

<!-- Los tres son encontrados por getByLabel('Email') -->
<label for="email">Email</label><input id="email" />
<label><span>Email</span><input /></label>
<input aria-label="Email" />

getByPlaceholder: cuando no hay label

Algunos inputs tienen texto de placeholder en lugar de un label visible. getByPlaceholder maneja este caso.

await page.getByPlaceholder('Buscar destinos...').fill('Tokio');
await page.getByPlaceholder('Ingresá tu correo').fill('test@ejemplo.com');

Prefiere getByLabel cuando existe un label. getByPlaceholder es para inputs que solo tienen texto de placeholder.

getByText: para elementos no interactivos

getByText encuentra elementos por su contenido de texto visible. Úsalo para texto que quieres verificar que existe en la página, no para elementos que quieres clickear (para eso usa getByRole).

// Verificar que el texto está presente
await expect(page.getByText('Mis Ítems de Viaje')).toBeVisible();
await expect(page.getByText('Credenciales inválidas')).toBeVisible();

// Coincidencia exacta vs parcial
page.getByText('Viaje')          // coincide con "Mis Ítems de Viaje", "Guía de viaje"
page.getByText('Viaje', { exact: true })  // solo exactamente "Viaje"

// Acotado a un tipo de elemento específico
page.locator('p').getByText('Ocurrió un error')  // solo elementos <p>

getByText encuentra todos los elementos que contienen ese texto, incluyendo los contenedores padre. Si "Enviar" aparece en un párrafo y en un botón, getByText('Enviar') devuelve múltiples elementos. Para elementos interactivos, usá getByRole.

getByTestId: el contrato explícito

getByTestId encuentra elementos por su atributo data-testid (configurable). Es el locator a usar cuando los desarrolladores explícitamente agregan anclajes de test al DOM.

<button data-testid="submit-payment">Pagar ahora</button>
<div data-testid="success-message">Pago completado</div>

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

La ventaja: los atributos data-testid son invisibles para los usuarios y no tienen significado funcional, así que los desarrolladores no los renombrarán por accidente. La desventaja: alguien tiene que agregarlos al código. Para apps que tú controlas, está bien. Para apps de terceros, dependes de la estructura que exista.

Si tu equipo no usa data-testid todavía, proponelo. Pedile a los desarrolladores que agreguen atributos data-testid a los elementos interactivos clave. Toma minutos por componente y hace que los locators sean trivialmente estables.

getByAltText y getByTitle

Dos locators menos comunes pero útiles ocasionalmente:

// Imágenes con texto alternativo
await page.getByAltText('Foto de perfil del usuario').click();
await expect(page.getByAltText('Logo de la empresa')).toBeVisible();

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

Los usarás raramente. La mayoría de los elementos interactivos deberían ser alcanzables vía getByRole.

Encadenar locators

Cuando necesitás reducir la búsqueda a un elemento específico dentro de un contenedor, encadenas locators:

// Encontrar una fila que contenga "Tokio", luego hacer clic en su botón Eliminar
const tokyoRow = page.getByRole('row').filter({ hasText: 'Tokio' });
await tokyoRow.getByRole('button', { name: 'Eliminar' }).click();

// Encontrar una sección de formulario, luego interactuar con su input
const addressSection = page.locator('.address-section');
await addressSection.getByLabel('Ciudad').fill('Buenos Aires');

filter({ hasText: '...' }) reduce un locator a elementos que contienen texto específico. Combinado con nth() para selección por índice:

// Primera fila de la tabla (índice 0)
const firstRow = page.getByRole('row').nth(1); // nth(0) es el encabezado, nth(1) es la primera fila de datos
await firstRow.getByRole('button', { name: 'Editar' }).click();

Qué evitar

Selectores CSS. Frágiles, específicos de la implementación, se rompen con refactors:

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

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

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

Selectores de texto sin contexto. Ambiguos cuando el texto aparece en varios lugares:

// Riesgoso: ¿qué pasa si "Editar" aparece en varios lugares?
page.getByText('Editar')
// Mejor: acotado a la fila que te importa
page.getByRole('row').filter({ hasText: 'Tokio' }).getByRole('button', { name: 'Editar' })

Ejercicio práctico en lab.becomeqa.com

Abre https://lab.becomeqa.com e intenta escribir locators para:

1. El botón Login en la navegación

2. Los inputs de Username y Password en el modal de login

3. El botón Submit del modal de login

4. La fila de la tabla que contiene un destino específico después del login

5. El botón Add Item en el dashboard

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

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

  // 1. Botón de login en la navegación
  await page.getByRole('button', { name: 'Login' }).click();

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

  // 4. Fila específica en la tabla
  const parisRow = page.getByRole('row').filter({ hasText: 'París' });
  await expect(parisRow).toBeVisible();

  // 5. Botón de agregar ítem
  await expect(page.getByRole('button', { name: 'Add Item' })).toBeVisible();
});

FAQ

¿Cuándo debería usar locator() directamente en lugar de los métodos getBy*?

Cuando necesitás selectores CSS o de atributos que los métodos getBy* no cubren. Por ejemplo, page.locator('[data-status="active"]') encuentra todos los elementos con un valor específico de atributo data. Úsalo como último recurso, no como primera opción.

¿Puedo combinar múltiples locators?

Sí. locator.and(otherLocator) encuentra elementos que coinciden con ambos:

// Un botón que es visible y tiene el texto "Enviar"
page.getByRole('button').and(page.getByText('Enviar'))

¿Qué pasa si dos elementos coinciden con mi locator?

Playwright lanza strict mode violation si un locator coincide con más de un elemento cuando intentás interactuar con él. Solucionalo haciendo el locator más específico: agregá un filter, usá nth(), o acotalo a un contenedor padre.

¿Cómo depuro un locator que no encuentra nada?

Usa el modo highlight en el Playwright Inspector: PWDEBUG=1 npx playwright test. O llama await locator.highlight() en tu test para marcar visualmente el elemento que coincide durante una ejecución con navegador visible.

→ See also: Assertions en Playwright: La Guía Completa | Empezando con Playwright: Tus Primeros Tests en 30 Minutos | Playwright Codegen: Graba Tests Sin Escribir Código | Cómo Leer los Mensajes de Error de Playwright