Si tienes 95% de cobertura de unit tests pero nunca probaste el flujo completo desde el registro hasta el pago, tienes una falsa sensación de seguridad. Los bugs que más duelen en producción viven en las integraciones entre componentes, en los caminos de usuario que nadie probó de punta a punta.

Por qué Playwright y no Cypress

Ambos son buenos. Playwright tiene ventajas prácticas: soporte nativo de múltiples pestañas e iframes, ejecución paralela por test, auto-waiting más confiable, y soporte de Chromium, Firefox y WebKit en el mismo setup. Para equipos que ya usan Vitest o Jest, la API de Playwright se integra mejor al ecosistema existente.

// playwright.config.ts
import { defineConfig } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  timeout: 30000,
  retries: process.env.CI ? 2 : 0,
  use: {
    baseURL: 'http://localhost:3000',
    screenshot: 'only-on-failure',
    trace: 'on-first-retry',
  },
  webServer: {
    command: 'pnpm dev',
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
})

Tu primer test E2E

Un test E2E que vale la pena: el flujo completo de login. No es el test más complejo, pero valida capas enteras: frontend, API, autenticación, sesión.

// e2e/login.spec.ts
import { test, expect } from '@playwright/test'

test('user can log in and see dashboard', async ({ page }) => {
  await page.goto('/login')

  await page.fill('[data-testid="email"]', 'user@example.com')
  await page.fill('[data-testid="password"]', 'password123')
  await page.click('[data-testid="submit"]')

  await expect(page).toHaveURL('/dashboard')
  await expect(page.locator('h1')).toContainText('Welcome')
})

Fíjate: data-testid como selectores, no clases CSS ni texto visible. Los selectores estables sobreviven a cambios de UI.

Page Object Model: no repitas selectores

Cuando varios tests usan el mismo flujo de login, los selectores duplicados generan mantenimiento pesado. El Page Objects encapsula la interacción con cada página:

// e2e/pages/LoginPage.ts
import { Page, expect } from '@playwright/test'

export class LoginPage {
  constructor(private readonly page: Page) {}

  async goto() {
    await this.page.goto('/login')
  }

  async login(email: string, password: string) {
    await this.page.fill('[data-testid="email"]', email)
    await this.page.fill('[data-testid="password"]', password)
    await this.page.click('[data-testid="submit"]')
  }

  async expectError(message: string) {
    await expect(this.page.locator('[data-testid="error"]'))
      .toContainText(message)
  }
}

// e2e/login.spec.ts
test('failed login shows error', async ({ page }) => {
  const loginPage = new LoginPage(page)
  await loginPage.goto()
  await loginPage.login('wrong@example.com', 'bad')
  await loginPage.expectError('Credenciales inválidas')
})

Testeando estados de error

Donde E2E agrega más valor es testeando caminos de error que los unit tests no cubren: timeout de API, error de red, estado de loading, comportamiento offline.

test('shows error when API is down', async ({ page }) => {
  // Intercepta la API y simula fallo
  await page.route('**/api/orders', route => route.abort('connectionrefused'))

  await page.goto('/orders')

  await expect(page.locator('[data-testid="error-message"]'))
    .toContainText('No fue posible cargar los pedidos')
  await expect(page.locator('[data-testid="retry-button"]'))
    .toBeVisible()
})

Datos de test: seeding y cleanup

Los tests E2E necesitan datos consistentes. Tus datos de test deben crearse antes de cada test y eliminarse después:

// e2e/fixtures.ts
import { test as base } from '@playwright/test'

export const test = base.extend<{ testUser: User }>({
  testUser: async ({ request }, use) => {
    // Crea usuario de test vía API
    const res = await request.post('/api/test/users', {
      data: { email: `test-${Date.now()}@example.com`, password: 'test123' }
    })
    const user = await res.json()

    await use(user)

    // Cleanup
    await request.delete(`/api/test/users/${user.id}`)
  },
})

Ejecución en CI

Playwright en CI necesita pasos específicos: instalar browsers, correr con --reporter=html y subir artifacts en caso de fallo. En GitHub Actions:

- name: Install Playwright
  run: pnpm exec playwright install --with-deps

- name: Run E2E tests
  run: pnpm test:e2e

- name: Upload test report
  if: failure()
  uses: actions/upload-artifact@v4
  with:
    name: playwright-report
    path: playwright-report/

Dónde parar

No testees todo con E2E. Los tests E2E son lentos, frágiles y caros de mantener. Úsalos para los flujos críticos del usuario: login, checkout, creación de cuenta, flujos de pago. Para el resto, unit tests e integration tests son suficientes.

La regla: si un fallo en ese flujo genera pérdida de ingresos o datos, testea E2E. Si es una feature secundaria, testea con unit o integration.