Locators de Playwright: codegen, herramientas y cómo se compara con Selenium
Codegen genera locators automáticamente. UI Mode muestra cada paso. Un error de idioma que en Selenium no pasa. Todo contra demo.serenity.is.
locator('div').filter({ hasText }).Nota para el lector
Este post documenta cómo exploré los locators de Playwright usando las herramientas que trae el framework.
Si querés ir al grano:
- Codegen y cómo genera locators automáticamente → "Codegen: grabar en vez de buscar"
- Las herramientas de Playwright (UI Mode, headed, reporte) → "Las herramientas"
- El error de idioma que me encontré → "El error que en Selenium no pasa"
- Tests finales funcionando → "Test final ejecutado"
Contexto: locators en Selenium vs Playwright
En la serie de Selenium, le dediqué un post entero a locators: "Localizadores en Selenium: de id a XPath, jerarquía práctica". Ahí explicaba DOM, selector, locator, By: qué es cada cosa y cómo elegir el mejor. Buscaba elementos inspeccionando el HTML manualmente, armando XPaths, probando CSS selectors.
En Playwright, el approach es otro. El framework trae herramientas para que no tengas que buscar locators a mano.
Codegen: grabar en vez de buscar
Playwright tiene un generador de código integrado. Se llama codegen y se ejecuta así:
npx playwright codegen demo.serenity.is
Se abren dos ventanas:
- El browser — donde interactuás con la app normalmente
- Playwright Inspector — donde aparece el código generado en tiempo real

locator('div').filter({ hasText }).Cada click, cada campo que llenás, cada navegación: Playwright genera el código con el locator que considera más robusto. Es parecido al Record Script de TestComplete, donde grabás un circuito de acciones y se genera un script automático. Pero acá ves el código generándose en tiempo real mientras interactuás con la app.
Lo que hice
Abrí codegen contra demo.serenity.is y grabé un flujo completo:
- Login con admin/serenity
- Verificar que llegué al dashboard
- Navegar a Northwind → Clientes
- Verificar que la grilla cargó con datos
Lo que codegen generó
await page.goto('https://demo.serenity.is/Account/Login/?ReturnUrl=%2F');
await page.getByRole('button', { name: 'Iniciar sesión', exact: true }).click();
await page.getByRole('link', { name: '📁 Northwind 📁' }).click();
await page.getByRole('link', { name: '📁 Clientes' }).click();
Todo semántico. getByRole('button'), getByRole('link'). Playwright eligió locators legibles sin que yo tocara un solo selector CSS ni XPath.
En Selenium, para el mismo botón de login hacía:
driver.findElement(By.cssSelector("button[type='submit']"));
O peor:
driver.findElement(By.xpath("//button[contains(text(),'Sign In')]"));
En Playwright: getByRole('button', { name: 'Iniciar sesión' }). El locator describe lo que el usuario ve, no cómo está armado el HTML.
Dónde codegen no alcanza
Para la grilla de Clientes, codegen generó locators como este:
locator('div').filter({ hasText: /^Around the Horn$/ }).nth(2)
locator + filter + nth. Ya no es semántico. Para elementos complejos como grillas (demo.serenity.is usa SlickGrid), codegen cae en locators estructurales. Es lógico: una fila de grilla no tiene un rol accesible limpio.
En Selenium pasaba lo mismo: para grillas terminaba con XPaths largos como //div[contains(@class,'slick-row')][3]//div[contains(@class,'slick-cell')][2].
Assertions desde codegen
Codegen no solo graba clicks. También puede generar assertions. En la toolbar del browser hay botones para esto:

Son 4 tipos de assertions:
Assert visibility (toBeVisible) — ¿El elemento se ve en la página?
await expect(page.getByRole('heading', { name: 'Tablero' })).toBeVisible();
Assert text (toContainText) — ¿El contenedor tiene este texto?
await expect(page.locator('#GridDiv')).toContainText('ANTON');
Assert value (toHaveValue) — ¿El input tiene este valor?
await expect(page.getByRole('textbox', { name: '* Nombre de usuario' })).toHaveValue('admin');
Assert ARIA snapshot (toMatchAriaSnapshot) — Valida la estructura de accesibilidad del elemento. Es el más avanzado y menos común. Para tests funcionales no lo necesitás.

Los que más uso: toBeVisible para verificar que algo cargó, toContainText para verificar datos en grillas.
Pick Locator
Hay otro botón en la toolbar: Pick Locator. No graba acciones. Pasás el mouse sobre cualquier elemento y te muestra el locator abajo en el tab "Locator" del Inspector.

Útil para explorar locators sin grabar un flujo completo. Apuntás a un elemento, ves qué locator usaría Playwright, y decidís si te sirve o necesitás otro.
Target: lenguaje del código
En el Inspector hay un dropdown "Target" que por defecto dice "Test Runner".

Eso controla el lenguaje del código generado. "Test Runner" genera Playwright + TypeScript listo para copiar a tus tests. Las otras opciones (Python, Java, C#, Library) son para gente que usa Playwright en esos lenguajes. Para esta serie: dejalo en Test Runner.
Las herramientas
Playwright trae varias formas de correr y analizar tests. Estas son las que usé:
--headed: ver el browser en vivo
npx playwright test serenity-locators --headed
Abre el browser y ejecuta los tests visualmente. Es como cuando corría tests en Selenium con la interfaz visible. Va rápido y se cierra solo al terminar.
Sirve para: ver rápidamente que el flujo funciona.
UI Mode: explorar paso a paso
npx playwright test --ui
Se abre una ventana llamada Playwright Test (UI Mode).

Es la herramienta más completa. Tiene:
- Timeline arriba — screenshots de cada momento de la ejecución
- Panel de acciones — cada paso del test con su duración (Navigate, Click, Fill, Expect)
- Browser — la página en el estado exacto del paso seleccionado
- Código fuente abajo — resalta la línea que se ejecutó
- Tabs — Locator, Source, Call, Log, Errors, Network, Attachments
Hacés click en cualquier acción del panel izquierdo y el browser te muestra cómo se veía la página en ese momento. Es como un debugger visual.
En Selenium no tenía nada parecido. Lo más cercano eran los screenshots en el reporte de Allure, pero solo capturaban el momento del fallo, no cada paso.
Sirve para: explorar, debuggear, entender qué pasa en cada paso.
Reporte HTML
npx playwright test serenity-locators
npx playwright show-report
El primer comando corre los tests (en headless, sin browser visible). El segundo abre el reporte en localhost:9323.

Muestra cada test con su browser (Chromium, Firefox, WebKit), tiempo de ejecución, estado. Filtros por passed, failed, flaky, skipped.
Headless es el default
Algo que me sorprendió viniendo de Selenium: Playwright corre en headless por defecto. No abre browser. Los tests corren en background y solo ves los resultados en la terminal.
¿Cuándo usar cada uno?
- Headless (default) → para CI/CD y correr suites completas
--headed→ para ver rápidamente que un test funciona- UI Mode → para explorar y debuggear
El error que en Selenium no pasa
Mi primer test contra demo.serenity.is falló.
Codegen generó los locators en español: 'Iniciar sesión', '* Nombre de usuario', '* Contraseña'. Porque cuando usé codegen, mi browser tenía el idioma en español.
Pero cuando corrí los tests, Playwright abre un browser limpio con idioma por defecto (inglés). La app de Serenity detecta el idioma del browser y cambia su interfaz.
El test buscaba '* Nombre de usuario' pero la página decía 'Username'.
Error: locator.click: Error: strict mode violation:
getByRole('textbox', { name: '* Nombre de usuario' })
La solución: configurar el locale en playwright.config.ts:
use: {
trace: 'on-first-retry',
locale: 'es-AR',
},

locale: 'es-AR' en playwright.config.ts. Una línea.Una línea. Playwright ahora abre el browser en español y los locators que generó codegen funcionan.
Por qué esto importa: los locators semánticos de Playwright (getByRole, getByText) buscan por texto visible. Si la app cambia de idioma, los locators se rompen. En Selenium, con By.id('LoginPanel0_Username'), esto no pasaba porque los IDs del HTML no cambian por idioma.
Es una ventaja y una trampa al mismo tiempo: los locators semánticos son más legibles y resilientes a cambios de HTML, pero son sensibles al idioma de la app.
Los tipos de locators que encontré
Estos son los locators que Playwright usó (o que yo usé) contra demo.serenity.is:
Semánticos (lo que recomienda Playwright)
getByRole — busca por rol accesible (button, textbox, link, heading):
page.getByRole('button', { name: 'Iniciar sesión', exact: true })
page.getByRole('textbox', { name: '* Nombre de usuario' })
page.getByRole('link', { name: '📁 Northwind 📁' })
page.getByRole('heading', { name: 'Tablero' })
El más usado. Codegen lo elige primero siempre que puede.
getByText — busca por texto visible:
page.getByText('Maria Anders')
page.getByText('ALFKI')
Útil para verificar contenido en grillas o cualquier texto en pantalla.
getByPlaceholder — busca inputs por placeholder:
page.getByPlaceholder('Username')
Lo usé en el Post 1 contra Saucedemo.
Estructurales (cuando lo semántico no alcanza)
locator() con CSS — equivalente a By.cssSelector() de Selenium:
page.locator('#GridDiv')
page.locator('section')
Para cuando el elemento no tiene rol accesible o el semántico es ambiguo.
filter() + nth() — para refinar búsquedas en grillas:
page.locator('div').filter({ hasText: /^Around the Horn$/ }).nth(2)
Codegen genera esto para filas de grilla. Es más frágil que los semánticos.
Comparación directa con Selenium
| Acción | Selenium + Java | Playwright + TypeScript |
|---|---|---|
| Campo username | By.id("LoginPanel0_Username") |
getByRole('textbox', { name: '* Nombre de usuario' }) |
| Campo password | By.id("LoginPanel0_Password") |
getByRole('textbox', { name: '* Contraseña' }) |
| Botón login | By.cssSelector("button[type='submit']") |
getByRole('button', { name: 'Iniciar sesión' }) |
| Link en menú | By.xpath("//a[contains(.,'Customers')]") |
getByRole('link', { name: '📁 Clientes' }) |
| Título de página | By.xpath("//h1[text()='Dashboard']") |
getByRole('heading', { name: 'Tablero' }) |
| Dato en grilla | By.xpath complejo o iterar lista |
locator('#GridDiv').toContainText('ANTON') |
| Buscar locators | Inspeccionar HTML manualmente | Codegen + Pick Locator |
La diferencia: en Selenium, pensaba en la estructura del HTML. En Playwright, pienso en lo que el usuario ve.
Test final ejecutado
Este es el test que quedó funcionando:
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.goto('https://demo.serenity.is/Account/Login');
await page.getByRole('textbox', { name: '* Nombre de usuario' }).click();
await page.getByRole('textbox', { name: '* Nombre de usuario' }).fill('admin');
await page.getByRole('textbox', { name: '* Contraseña' }).click();
await page.getByRole('textbox', { name: '* Contraseña' }).fill('serenity');
await page.getByRole('button', { name: 'Iniciar sesión', exact: true }).click();
await expect(page.getByRole('heading', { name: 'Tablero' })).toBeVisible();
await page.getByRole('link', { name: '📁 Northwind 📁' }).click();
await page.getByRole('link', { name: '📁 Clientes' }).click();
await expect(page.locator('section')).toBeVisible();
await expect(page.locator('#GridDiv')).toContainText('ANTON');
});
Resultado:
Running 3 tests using 2 workers
3 passed (26.5s)


1 test × 3 browsers (Chromium, Firefox, WebKit) = 3 ejecuciones. Todo verde.
WebKit es el motor de Safari. Playwright permite testear en Safari sin tener una Mac. En Selenium necesitás Safari real corriendo en macOS.
¿Por qué 2 workers y no 3?
Workers son procesos paralelos. Playwright usa 2 basado en los recursos de mi máquina. Dos browsers corren al mismo tiempo, el tercero espera. Por eso al correr con --headed vi 2 ventanas abiertas, no 3.
Otro error real: el título de la página
Mi primer test fue:
await expect(page).toHaveTitle(/Serenity/i);
Falló:
Expected pattern: /Serenity/i
Received string: "Login to your account"
Asumí el título sin verificar. El título real era "Login to your account". Corregí a:
await expect(page).toHaveTitle(/Login to your account/i);
Pasó. Playwright te muestra exactamente qué esperabas y qué recibió. El mensaje de error es claro.
Estado actual del proyecto
Tengo:
- Playwright 1.58.2 con 3 browsers
- Tests del Post 1 (Saucedemo) + test de locators contra demo.serenity.is
- Codegen como herramienta principal para encontrar locators
locale: 'es-AR'configurado enplaywright.config.ts- Reporte HTML funcionando
- Todos los tests corriendo en Chromium, Firefox y WebKit
Próximo paso: Armar Page Object Model para no repetir el login en cada test.
—
🔗 Todo el código de esta serie está en: github.com/cesarbeassuarez/playwright-typescript-framework