Playwright para testing E2E: guía práctica
Playwright para testing E2E: guía práctica
El testing end-to-end (E2E) sigue siendo uno de los pilares fundamentales para garantizar que tus aplicaciones web funcionen correctamente desde la perspectiva del usuario. Playwright, desarrollado por Microsoft, se ha consolidado como una de las herramientas más potentes y modernas del ecosistema. En esta guía práctica vas a aprender a implementar tests E2E robustos, rápidos y confiables con Playwright.
¿Por qué elegir Playwright?
Antes de escribir código, conviene entender qué diferencia a Playwright de alternativas como Selenium, Cypress o Puppeteer:
- Multi-navegador nativo: Chromium, Firefox y WebKit funcionan con el mismo API, sin adaptaciones.
- Ejecución paralela eficiente: Tests aislados por procesos que no comparten estado contaminado.
- Auto-waiting inteligente: Las acciones esperan automáticamente a que los elementos estén listos, reduciendo flaky tests.
- Modo headless y headed: Ejecución invisible para CI, visible para depuración local.
- API moderno: Soporte nativo para iframes, múltiples pestañas, geolocalización, permisos y más.
- Trace viewer y reportes HTML: Depuración visual exhaustiva cuando falla un test.
Instalación y configuración inicial
Comenzá con un proyecto nuevo o existente. Playwright funciona con cualquier aplicación web, independientemente de su stack.
npm init -y
npm install -D @playwright/test
npx playwright install
El último comando descarga los navegadores binarios necesarios. No los incluye en node_modules para no inflar el tamaño del paquete.
Playwright genera automáticamente el archivo playwright.config.ts. Esta configuración es suficiente para empezar, pero la vas a personalizar según tus necesidades:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
La clave baseURL centraliza el dominio de tu aplicación. Todos los page.goto('/') se resuelven automáticamente contra esta URL.
Estructura de un test E2E con Playwright
Playwright usa el patrón Arrange-Act-Assert de forma natural. Veamos un test completo para un flujo de login:
import { test, expect } from '@playwright/test';
test.describe('Autenticación de usuarios', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('login exitoso redirige al dashboard', async ({ page }) => {
// Arrange
const emailInput = page.getByLabel('Correo electrónico');
const passwordInput = page.getByLabel('Contraseña');
const submitButton = page.getByRole('button', { name: 'Iniciar sesión' });
// Act
await emailInput.fill('usuario@ejemplo.com');
await passwordInput.fill('contraseñaSegura123');
await submitButton.click();
// Assert
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText('Bienvenido, usuario@ejemplo.com')).toBeVisible();
});
test('credenciales inválidas muestran error', async ({ page }) => {
await page.getByLabel('Correo electrónico').fill('invalido@test.com');
await page.getByLabel('Contraseña').fill('wrong');
await page.getByRole('button', { name: 'Iniciar sesión' }).click();
await expect(page.getByRole('alert')).toContainText('Credenciales incorrectas');
});
});
Fijate que no usamos page.click('#login-btn') ni selectores CSS frágiles. Playwright incentiva localizadores semánticos basados en roles, etiquetas y texto accesible, lo que hace tus tests más resilientes a cambios de estructura HTML.
Selectores estratégicos: user-facing vs. test IDs
Playwright prioriza localizadores que el usuario percibe. Esta es la jerarquía recomendada:
page.getByRole()— roles ARIA comobutton,heading,navigationpage.getByLabel()— etiquetas de formulariospage.getByPlaceholder()— textos de placeholderpage.getByText()— contenido textual visiblepage.getByTestId()— atributosdata-testidpara casos específicos
Cuando ningún localizador user-facing funciona, los test IDs son la alternativa controlada:
// En tu componente React/Vue/Angular
<button data-testid="submit-order">Confirmar compra</button>
// En tu test
await page.getByTestId('submit-order').click();
Evitá selectores tipo .btn-primary:nth-child(3). Se rompen con cualquier refactorización mínima del DOM.
Tests de flujos complejos: checkout de e-commerce
Los tests E2E ganan valor cuando verifican flujos completos que atraviesan múltiples páginas y estados. Este ejemplo simula una compra completa:
import { test, expect } from '@playwright/test';
test('flujo completo de compra', async ({ page }) => {
// Catálogo: agregar producto al carrito
await page.goto('/productos');
await page.getByRole('heading', { name: 'Teclado Mecánico Pro' }).click();
await page.getByRole('button', { name: 'Agregar al carrito' }).click();
// Verificar notificación de éxito
await expect(page.getByRole('status')).toContainText('Producto agregado');
// Navegar al carrito y proceder
await page.getByRole('link', { name: 'Carrito' }).click();
await page.getByRole('button', { name: 'Finalizar compra' }).click();
// Formulario de envío
await page.getByLabel('Nombre completo').fill('María González');
await page.getByLabel('Dirección').fill('Av. Siempreviva 742');
await page.getByLabel('Ciudad').fill('Springfield');
await page.getByLabel('Código postal').fill('12345');
await page.getByRole('button', { name: 'Continuar al pago' }).click();
// Simular pago (entorno de sandbox)
await page.getByLabel('Número de tarjeta').fill('4242424242424242');
await page.getByLabel('Fecha de vencimiento').fill('12/25');
await page.getByLabel('CVC').fill('123');
await page.getByRole('button', { name: 'Pagar' }).click();
// Confirmación final
await expect(page).toHaveURL(/\/orden\/confirmacion/);
await expect(page.getByRole('heading', { name: '¡Gracias por tu compra!' })).toBeVisible();
const orderNumber = await page.getByTestId('order-number').textContent();
expect(orderNumber).toMatch(/ORD-\d{6}/);
});
Este test valida la integración de catálogo, carrito, checkout, procesamiento de pagos y confirmación. Un test unitario nunca podría cubrir esta interacción transversal.
Manejo de autenticación: evitar repetir login en cada test
Autenticarse antes de cada test es lento y agrega flaky points. Playwright resuelve esto con storage state:
// auth.setup.ts — test independiente que genera el estado
import { test as setup } from '@playwright/test';
setup('autenticar usuario', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Correo electrónico').fill('test@ejemplo.com');
await page.getByLabel('Contraseña').fill('testpass123');
await page.getByRole('button', { name: 'Iniciar sesión' }).click();
await page.waitForURL('/dashboard');
// Guardar cookies y localStorage para reutilizar
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
// playwright.config.ts — configurar proyecto de setup
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'authenticated',
testMatch: /.*\.spec\.ts/,
dependencies: ['setup'],
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
},
],
Los tests del proyecto authenticated arrancan ya logueados, saltando el formulario de login. El tiempo de ejecución se reduce drásticamente en suites grandes.
API requests: interceptar y mockear respuestas
Para tests deterministas, necesitás controlar respuestas de APIs externas o inestables:
test('lista de productos con datos mockeados', async ({ page }) => {
// Interceptar la llamada a la API
await page.route('**/api/products', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
products: [
{ id: 1, name: 'Producto Test', price: 99.99, stock: 5 },
{ id: 2, name: 'Otro Producto', price: 149.50, stock: 0 },
],
}),
});
});
await page.goto('/productos');
await expect(page.getByText('Producto Test')).toBeVisible();
await expect(page.getByText('$99.99')).toBeVisible();
// Verificar estado de agotado
const outOfStockButton = page.getByRole('button', { name: 'Sin stock' });
await expect(outOfStockButton).toBeDisabled();
});
page.route() te da control total sobre el network layer. Es útil también para simular errores de red (500, timeout) y verificar el comportamiento de tu app en condiciones adversas.
Visual testing con screenshots
Playwright incluye comparación visual de snapshots para detectar regresiones de UI:
test('página de perfil sin cambios visuales', async ({ page }) => {
await page.goto('/perfil');
await page.getByRole('heading', { name: 'Mi Perfil' }).waitFor();
// Capturar y comparar contra baseline
await expect(page).toHaveScreenshot('perfil-page.png', {
maxDiffPixels: 100, // tolerancia para cambios mínimos
});
});
La primera ejecución genera el baseline. Ejecutá npx playwright test --update-snapshots cuando los cambios sean intencionales.
Ejecución y debugging
| Comando | Propósito |
|---|---|
npx playwright test | Ejecutar todos los tests en modo headless |
npx playwright test --headed | Ver la ejecución en navegador visible |
npx playwright test --ui | Modo UI interactivo con time-travel debugging |
npx playwright test --debug | Pausar en cada paso para inspección paso a paso |
npx playwright test --project=chromium | Filtrar por proyecto/navegador |
npx playwright show-report | Abrir reporte HTML con traces y screenshots |
El Trace Viewer es particularmente valioso. Captura DOM, network, console logs y screenshots en cada acción del test. Cuando falla en CI, descargás el trace.zip y lo inspeccionás localmente como si estuvieras reproduciendo la sesión.
Integración continua con GitHub Actions
Playwright proporciona una acción oficial optimizada:
name: Playwright Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run build
- run: npm start &
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
La bandera --with-deps instala dependencias del sistema operativo que los navegadores requieren. El artefacto solo se sube si hay fallos, evitando almacenamiento innecesario.
Mejores prácticas para tests E2E mantenibles
- Tests independientes: Nunca dependas del estado de otro test. Cada uno debe poder ejecutarse aislado.
- Datos de test controlados: Usá APIs o seeds para crear el estado necesario, no asumas datos existentes.
- Timeouts explícitos para operaciones lentas:
await page.waitForResponse('**/api/heavy')antes de aserciones. - Page Object Model (POM): Abstraé selectores repetidos en clases reutilizables.
- Evitá tests que validen solo contenido estático: Eso corresponde a tests de componente o visual, no E2E.
Page Object Model en práctica
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorAlert: Locator;
constructor(private page: Page) {
this.emailInput = page.getByLabel('Correo electrónico');
this.passwordInput = page.getByLabel('Contraseña');
this.submitButton = page.getByRole('button', { name: 'Iniciar sesión' });
this.errorAlert = page.getByRole('alert');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string) {
await expect(this.errorAlert).toContainText(message);
}
}
// Uso en test
import { test } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
test('login con POM', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('test@ejemplo.com', 'wrong');
await loginPage.expectError('Credenciales incorrectas');
});
Conclusión
Playwright representa el estado del arte en testing E2E para aplicaciones web modernas. Su API expressiva, ejecución confiable y herramientas de debugging superan a las alternativas legacy en velocidad y mantenibilidad.
Empezá con tests de los flujos críticos de negocio: registro, login, pagos, procesos que no pueden fallar. Expandí gradualmente la cobertura hacia casos edge case. La inversión en una suite E2E sólida con Playwright se paga con intereses en reducción de bugs en producción y confianza en cada deploy.
Instalá Playwright hoy, ejecutá tu primer test, y experimentá la diferencia de una herramienta diseñada para desarrolladores que valoran la velocidad sin sacrificar confiabilidad.