Saltar al contenido

Estrategias de pruebas end-to-end con Playwright y TypeScript

Suites end-to-end fiables con Playwright: modelos de página, aislamiento, intercepción de red, regresión visual e integración CI sin fallos aleatorios.

4 min de lectura
Un ejecutor de pruebas de Playwright mostrando marcas de verificación verdes para múltiples escenarios de navegador ejecutándose en paralelo

Por qué siguen importando las pruebas E2E

Las pruebas unitarias verifican funciones. Las pruebas de integración verifican módulos. Ninguna te dice si un usuario puede realmente completar una compra y pagar. Las pruebas end-to-end llenan ese vacío ejercitando la aplicación completa, desde el navegador hasta la base de datos. La compensación es velocidad e inestabilidad, pero la arquitectura de Playwright minimiza ambas.

Configuración del proyecto

Playwright admite Chromium, Firefox y WebKit desde una sola API. Configúralo para ejecutar los tres en CI pero solo Chromium localmente por velocidad.

tstypescript
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
 
export default defineConfig({
  testDir: "./e2e",
  timeout: 30_000,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  reporter: process.env.CI
    ? [["html"], ["github"]]
    : [["list"]],
 
  use: {
    baseURL: "http://localhost:3000",
    screenshot: "only-on-failure",
    trace: "retain-on-failure",
    video: "retain-on-failure",
  },
 
  projects: [
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
    ...(process.env.CI
      ? [
          { name: "firefox", use: { ...devices["Desktop Firefox"] } },
          { name: "webkit", use: { ...devices["Desktop Safari"] } },
        ]
      : []),
  ],
 
  webServer: {
    command: "npm run dev",
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
});

Modelo de página

Los selectores crudos dispersos entre archivos de pruebas se vuelven imposibles de mantener. Los objetos de página encapsulan la estructura de la página y exponen acciones como métodos.

tstypescript
// ❌ Selectors repeated across tests — one UI change breaks dozens of tests
// await page.click('[data-testid="add-to-cart"]');
// await page.fill('[data-testid="email-input"]', 'user@example.com');
 
// ✅ Page object encapsulates selectors and actions
class CheckoutPage {
  constructor(private page: Page) {}
 
  private get emailInput() {
    return this.page.getByLabel("Email address");
  }
 
  private get cartItems() {
    return this.page.getByTestId("cart-item");
  }
 
  private get placeOrderButton() {
    return this.page.getByRole("button", { name: "Place order" });
  }
 
  private get orderConfirmation() {
    return this.page.getByTestId("order-confirmation");
  }
 
  async fillEmail(email: string): Promise<void> {
    await this.emailInput.fill(email);
  }
 
  async getCartItemCount(): Promise<number> {
    return this.cartItems.count();
  }
 
  async placeOrder(): Promise<void> {
    await this.placeOrderButton.click();
    await this.orderConfirmation.waitFor({ state: "visible" });
  }
 
  async getOrderId(): Promise<string> {
    const text = await this.orderConfirmation.textContent();
    const match = text?.match(/Order #(\w+)/);
    if (!match) throw new Error("Order ID not found in confirmation");
    return match[1];
  }
}

Aislamiento de pruebas con fixtures

Cada prueba debe partir de un estado limpio. Los fixtures de Playwright te permiten preparar y limpiar el estado por prueba sin efectos secundarios globales.

tstypescript
import { test as base, expect } from "@playwright/test";
 
interface TestFixtures {
  authenticatedPage: Page;
  testUser: { email: string; password: string };
}
 
const test = base.extend<TestFixtures>({
  testUser: async ({}, use) => {
    // Create user via API before test
    const email = `test-${Date.now()}@example.com`;
    const password = "SecureTestPass123!";
 
    const response = await fetch("http://localhost:3000/api/test/users", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email, password }),
    });
 
    if (!response.ok) throw new Error("Failed to create test user");
 
    await use({ email, password });
 
    // Cleanup after test
    await fetch("http://localhost:3000/api/test/users", {
      method: "DELETE",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email }),
    });
  },
 
  authenticatedPage: async ({ page, testUser }, use) => {
    await page.goto("/login");
    await page.getByLabel("Email").fill(testUser.email);
    await page.getByLabel("Password").fill(testUser.password);
    await page.getByRole("button", { name: "Sign in" }).click();
    await page.waitForURL("/dashboard");
 
    await use(page);
  },
});
 
test("authenticated user can view dashboard", async ({
  authenticatedPage,
}) => {
  await expect(
    authenticatedPage.getByRole("heading", { name: "Dashboard" })
  ).toBeVisible();
});

Intercepción de red

Simula APIs externas para hacer las pruebas deterministas. La interceptación de rutas de Playwright se encarga de esto sin un servidor mock separado.

tstypescript
test("displays payment error gracefully", async ({ page }) => {
  // Intercept payment API to simulate failure
  await page.route("**/api/payments", (route) => {
    route.fulfill({
      status: 422,
      contentType: "application/json",
      body: JSON.stringify({
        error: "card_declined",
        message: "Your card was declined",
      }),
    });
  });
 
  await page.goto("/checkout");
  await page.getByLabel("Card number").fill("4242424242424242");
  await page.getByRole("button", { name: "Pay" }).click();
 
  await expect(
    page.getByText("Your card was declined")
  ).toBeVisible();
 
  // Verify the error state does not break the page
  await expect(
    page.getByRole("button", { name: "Pay" })
  ).toBeEnabled();
});
 
test("handles slow API responses", async ({ page }) => {
  await page.route("**/api/products", async (route) => {
    // Simulate 3-second delay
    await new Promise((r) => setTimeout(r, 3000));
    await route.continue();
  });
 
  await page.goto("/products");
 
  // Loading state should appear
  await expect(page.getByTestId("loading-skeleton")).toBeVisible();
 
  // Products should eventually load
  await expect(page.getByTestId("product-card").first()).toBeVisible({
    timeout: 10_000,
  });
});

Pruebas de regresión visual

Detecta cambios visuales no intencionales comparando capturas de pantalla contra líneas base.

tstypescript
test("product card renders correctly", async ({ page }) => {
  await page.goto("/products");
 
  const productCard = page.getByTestId("product-card").first();
  await expect(productCard).toHaveScreenshot("product-card.png", {
    maxDiffPixelRatio: 0.01,
  });
});
 
test("responsive layout at mobile viewport", async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 812 });
  await page.goto("/");
 
  // Full page screenshot comparison
  await expect(page).toHaveScreenshot("homepage-mobile.png", {
    fullPage: true,
    maxDiffPixelRatio: 0.02,
  });
});

Depuración de pruebas fallidas

El visor de trazas de Playwright muestra cada acción, solicitud de red e instantánea del DOM de una prueba fallida. Configura las trazas para capturarse automáticamente al fallar, y luego ábrelas localmente.

tstypescript
// View trace from a failed CI run
// npx playwright show-trace trace.zip
 
test("complex checkout flow", async ({ page, context }) => {
  // Start tracing for this specific test
  await context.tracing.start({
    screenshots: true,
    snapshots: true,
    sources: true,
  });
 
  try {
    await page.goto("/checkout");
    // ... test steps
  } finally {
    await context.tracing.stop({
      path: "checkout-trace.zip",
    });
  }
});

Conclusiones clave

Las pruebas end-to-end deben cubrir los caminos críticos del usuario: autenticación, compra, ingreso de datos y recuperación de errores. Usa objetos de página para aislar los cambios de selectores de la lógica de pruebas. Crea un estado fresco por prueba con fixtures: el estado compartido entre pruebas es la principal fuente de inestabilidad.

Intercepta las solicitudes de red para probar estados de error, respuestas lentas y casos extremos que son imposibles de reproducir con un backend real. Usa pruebas de regresión visual para componentes donde la precisión a nivel de píxel importa, pero mantén el umbral razonable para evitar falsos positivos por diferencias de renderizado de fuentes. Habilita trazas y capturas de pantalla al fallar en CI: depurar una prueba E2E fallida sin una traza es como depurar un incidente en producción sin logs.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX