← Volver al blog

Playwright para testing E2E: guía práctica

2026-06-21

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:

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:

  1. page.getByRole() — roles ARIA como button, heading, navigation
  2. page.getByLabel() — etiquetas de formularios
  3. page.getByPlaceholder() — textos de placeholder
  4. page.getByText() — contenido textual visible
  5. page.getByTestId() — atributos data-testid para 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

ComandoPropósito
npx playwright testEjecutar todos los tests en modo headless
npx playwright test --headedVer la ejecución en navegador visible
npx playwright test --uiModo UI interactivo con time-travel debugging
npx playwright test --debugPausar en cada paso para inspección paso a paso
npx playwright test --project=chromiumFiltrar por proyecto/navegador
npx playwright show-reportAbrir 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

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.