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.

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.
// 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.
// ❌ 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.
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.
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.
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.
// 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.


