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.

Codegen de Playwright mostrando Pick Locator sobre fila de grilla de Clientes en demo.serenity.is con locator filter hasText Berglunds snabbköp
Pick Locator en acción: paso el mouse sobre una fila y Playwright me muestra el locator. Para grillas, cae en 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:


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
Codegen de Playwright mostrando Pick Locator sobre fila de grilla de Clientes en demo.serenity.is con locator filter hasText Berglunds snabbköp
Pick Locator en acción: paso el mouse sobre una fila y Playwright me muestra el locator. Para grillas, cae en 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:

  1. Login con admin/serenity
  2. Verificar que llegué al dashboard
  3. Navegar a Northwind → Clientes
  4. 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:

Toolbar de codegen de Playwright con botones de Record, Pick Locator, Assert visibility, Assert text, Assert value y Assert ARIA
Toolbar de codegen. De izquierda a derecha: mover, grabar, Pick Locator, assert visibility, assert text (ab), assert value, assert ARIA snapshot.

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.

Codegen de Playwright mostrando assertions generadas automáticamente contra grilla de Clientes incluyendo toBeVisible toContainText y toMatchAriaSnapshot
Assertions generadas directo desde codegen. No solo graba clicks — graba validaciones.

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.

Pick Locator de Playwright resaltando dropdown Ciudad con locator CSS largo basado en ID de Serenity CustomerGrid QuickFilter City
Pick Locator sobre el dropdown de Ciudad. El locator que genera es un CSS selector largo basado en IDs internos de Serenity. Acá lo semántico no alcanza.

Ú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".

Dropdown Target de Playwright Inspector mostrando opciones de lenguaje Node.js Test Runner Python Pytest .NET C# Java JUnit y Debugger JSONL
Target controla el lenguaje del código generado. Para esta serie: Test Runner (Playwright + TypeScript).

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).

UI Mode de Playwright mostrando test pasado con timeline de screenshots, acciones Click Fill Expect, browser con grilla de Clientes y página de login de demo.serenity.is
UI Mode: timeline arriba, acciones a la izquierda, browser al centro, código abajo. Todo el test visible en un solo lugar.

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.

Reporte HTML de Playwright mostrando 3 tests pasados en Chromium Firefox y WebKit contra serenity-locators.spec.ts en 26.5 segundos
Reporte HTML. 1 test × 3 browsers = 3 ejecuciones. Todo verde.

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',
},
Archivo playwright.config.ts mostrando configuración locale es-AR agregada en bloque use junto a trace on-first-retry
La solución al error de idioma: 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)
VS Code con serenity-locators.spec.ts y terminal mostrando 3 passed en 26.5 segundos con npx playwright test y npx playwright show-report
Test final: código, ejecución en terminal y reporte HTML. 3 passed en 26.5s.
Reporte HTML final de Playwright con 3 tests pasados en Chromium 9s Firefox 12.5s y WebKit 9.3s contra demo.serenity.is
Reporte final. Chromium y WebKit ~9s, Firefox ~12s. Los 3 browsers pasan.

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 en playwright.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