Saltar al contenido
Buscar
Ver todos los artículos
Playwright

Por qué tus tests end-to-end fallan sin que cambie nada

Playwright es un framework de testing end-to-end que espera a que los elementos estén listos antes de tocarlos y reintenta las aserciones en vez de fallar al primer parpadeo. Sirve para que una suite falle por bugs reales, no por timing.

El problema no es Playwright, son tus supuestos sobre timing

Cuando un equipo analizó por qué sus suites de Playwright se volvían flaky con el tiempo, encontró que las fallas se agrupaban en tres causas: selectores frágiles, sobrecarga de autenticación repetida en cada test, y timeouts mal configurados. Ninguna de las tres es un bug del framework. Son decisiones que parecían razonables cuando la suite tenía cuatro tests y se vuelven caras cuando tiene cuatrocientos. El caso más común es el selector por CSS que se rompe apenas alguien renombra una clase en un refactor visual, sin que la funcionalidad haya cambiado un bit. Antes de tocar código de test porque "a veces falla", vale la pena preguntarse cuál de estas tres causas es la real, en vez de agregar un retry a ciegas.

Auto-wait: por qué Playwright reintenta antes de fallar

Playwright funciona como un usuario rápido: espera a que un elemento sea interactuable antes de hacer click, y cuando hacés una aserción no la evalúa una sola vez, sino que la reintenta hasta que se cumple o se agota el timeout (5 segundos por defecto). Eso es lo que evita gran parte del flakiness clásico de Selenium, donde tenías que agregar sleeps manuales o esperas explícitas para cada elemento. En la práctica, escribís la aserción como si el elemento ya estuviera ahí, y Playwright se encarga de la espera. El ejemplo de abajo completa un formulario y valida el mensaje de éxito sin ningún wait manual.

typescript
import { test, expect } from '@playwright/test';
​
test('envía el formulario de contacto', async ({ page }) => {
await page.goto('https://lahumada.com/');
await page.getByPlaceholder('Nombre completo').fill('Luca Ahumada');
await page.getByPlaceholder('Correo electrónico').fill('prueba@correodeprueba.com');
await page.getByPlaceholder('Escribe tu mensaje').fill('Este es un mensaje de prueba.');
await page.getByRole('button', { name: 'ENVIAR MENSAJE' }).click();
await expect(page.getByText('Gracias por dejarme tu mensaje!')).toBeVisible();
});

De cero a tu primer test grabado

La instalación no tiene vueltas: creás la carpeta, corrés el instalador oficial y queda un proyecto en TypeScript con la carpeta ./tests lista. Lo que sí cambia la experiencia es el Code Generator: en vez de escribir selectores a mano y adivinar si van a aguantar, grabás la interacción en el navegador y Playwright te devuelve los localizadores que considera más estables para ese sitio, no cualquier selector que matchee. Después de correr la suite una vez, tenés un reporte HTML por defecto (y podés exportarlo a JSON o JUnit si tu CI lo necesita).

bash
mkdir playwright-training
cd playwright-training
npm init playwright@latest
​
# corre la suite con el navegador visible
npx playwright test --headed
​
# UI Mode: debugging, watch mode, retries paso a paso
npx playwright test --ui
​
# grabar un caso nuevo interactuando con el sitio
npx playwright codegen

La jerarquía de selectores que sostiene la suite

No todos los selectores envejecen igual. La recomendación es usar getByRole primero porque ancla el match en el árbol de accesibilidad, no en la implementación: si un diseñador mueve el botón o le cambia la clase CSS, el test sigue pasando. Si no hay un rol claro, el siguiente escalón es getByLabel o getByPlaceholder para inputs, y getByText para copy visible única. getByTestId es el fallback explícito para cuando nada de lo anterior aplica, no la primera opción. CSS y XPath quedan al final de la lista porque acoplan el test a detalles de implementación —nombres de clase, profundidad del DOM— que cambian por razones que no tienen nada que ver con el comportamiento que estás probando. El mismo flujo de login escrito con getByRole sobrevive a un rename de componente; escrito en CSS, no.

typescript
import { test, expect } from '@playwright/test';
​
test.describe('Selector strategy comparison', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
​
test('role locator — survives renames', async ({ page }) => {
const loginButton = page.getByRole('button', { name: 'Log in' });
await expect(loginButton).toBeVisible();
await loginButton.click();
await expect(page).toHaveURL(/.*dashboard/);
await expect(
page.getByRole('heading', { name: /welcome/i })
).toBeVisible();
});
​
test('CSS selector — breaks on component rename', async ({ page }) => {
// acoplado al nombre de la clase: si el componente
// pasa de "LoginButton" a "AuthButton", esto se rompe
const loginButton = page.locator('button.LoginButton__primary');
await expect(loginButton).toBeVisible();
await loginButton.click();
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.locator('h1.Dashboard__welcome')).toBeVisible();
});
});

Soft assertions: ver todos los fallos, no solo el primero

Por defecto, una aserción que falla corta el test ahí mismo, aunque tuvieras diez pasos más para chequear. Eso está bien para casos simples, pero en un flujo con varios checks seguidos (un perfil con cinco campos, una página con tres widgets) te esconde el resto de los problemas detrás del primero que explotó. Las soft assertions marcan el test como fallido igual, pero dejan que la ejecución siga, así que en una sola corrida ves todo lo que está roto en vez de arreglar uno, correr de nuevo, y encontrarte con el siguiente.

typescript
await expect.soft(page.getByText('Gracias por dejarme tu mensaje!')).toBeVisible();
await page.getByRole('button', { name: 'PUBLICACIONES' }).click();
// el test sigue corriendo aunque la aserción anterior haya fallado

Qué puedes construir con esto

Tres proyectos que se hacen en una tarde y terminan en algo que corre.

  1. Suite E2E de un formulario propio, grabada y refinada

    Grabar con codegen el flujo de contacto o login de un sitio propio, y después reemplazar los selectores generados por getByRole y getByPlaceholder siguiendo la jerarquía de estabilidad.

    Para qué sirve — Terminás con una suite que corta cuando el flujo real se rompe, no cada vez que alguien retoca el CSS.

  2. El mismo test escrito cuatro veces, para ver cuál aguanta

    Escribir la misma interacción de login con getByRole, getByTestId, CSS y XPath, y después renombrar una clase del componente para ver cuáles tests sobreviven.

    Para qué sirve — Es más convincente comprobar en tu propio código por qué CSS y XPath se rompen que leerlo en un artículo.

  3. Reporte de una página con múltiples checks y soft assertions

    Armar un test que valide cinco o seis condiciones de una misma página con expect.soft y generar el reporte HTML de la corrida completa.

    Para qué sirve — Ver todos los fallos de una vez es más rápido que arreglar de a uno y volver a correr la suite entera cada vez.

Qué te llevas

  • La mayoría del flakiness no es un bug de Playwright: es un selector frágil, un timeout mal puesto o autenticación repetida que se puede evitar.

  • getByRole primero, CSS y XPath al final: la jerarquía de selectores no es una preferencia de estilo, es la diferencia entre un test que sobrevive a un refactor y uno que no.

  • El auto-wait de Playwright reemplaza los sleeps manuales, pero no reemplaza pensar qué estás esperando y por cuánto tiempo tiene sentido esperarlo.

  • Las soft assertions sirven para diagnosticar, no para ignorar fallos: el test sigue marcado como roto, solo te deja ver todo lo que falló en una corrida.

Este artículo se escribió leyendo la documentación oficial que está enlazada abajo. El código sale de ahí, no de la memoria de un modelo.

De dónde salió esto