Playwright avanzado
Gestiona autenticación, pruebas de API, simulación de rutas, escenarios con varias pestañas, viewports móviles y auditorías de accesibilidad.
Receta
Tarjeta de referencia rápida — lista para copiar y pegar.
import { test, expect } from "@playwright/test";
// Shared authentication state
test.use({ storageState: "e2e/.auth/user.json" });
// API testing
const response = await request.get("/api/users");
expect(response.ok()).toBe(true);
// Mock API routes
await page.route("/api/products", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([{ id: 1, name: "Widget" }]),
});
});
// Mobile viewport
test.use({ ...devices["iPhone 14"] });
// Accessibility testing
import AxeBuilder from "@axe-core/playwright";
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);Cuándo usarlo: Cuando tus pruebas E2E necesitan gestionar sesiones de login, probar API directamente, simular respuestas de red o verificar accesibilidad.
Ejemplo funcional
// e2e/auth.setup.ts
import { test as setup, expect } from "@playwright/test";
const authFile = "e2e/.auth/user.json";
setup("authenticate", async ({ page }) => {
// Navigate to login page
await page.goto("/login");
// Fill in credentials
await page.getByLabel("Email").fill("test@example.com");
await page.getByLabel("Password").fill("password123");
await page.getByRole("button", { name: /sign in/i }).click();
// Wait for redirect to dashboard
await expect(page).toHaveURL("/dashboard");
// Save authentication state (cookies, localStorage)
await page.context().storageState({ path: authFile });
});// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
projects: [
// Setup project runs first to authenticate
{
name: "setup",
testMatch: /.*\.setup\.ts/,
},
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
storageState: "e2e/.auth/user.json",
},
dependencies: ["setup"],
},
{
name: "mobile",
use: {
...devices["iPhone 14"],
storageState: "e2e/.auth/user.json",
},
dependencies: ["setup"],
},
],
});// e2e/dashboard.spec.ts
import { test, expect } from "@playwright/test";
// All tests in this file are authenticated (via project config)
test.describe("Authenticated Dashboard", () => {
test("shows user name in header", async ({ page }) => {
await page.goto("/dashboard");
await expect(page.getByTestId("user-menu")).toContainText("test@example.com");
});
test("displays recent orders", async ({ page }) => {
await page.goto("/dashboard");
await expect(page.getByRole("table")).toBeVisible();
const rows = page.getByRole("row");
await expect(rows).toHaveCount(6); // 1 header + 5 data rows
});
test("can update profile", async ({ page }) => {
await page.goto("/settings/profile");
await page.getByLabel("Display name").fill("Test User Updated");
await page.getByRole("button", { name: /save/i }).click();
await expect(page.getByRole("status")).toContainText("Profile updated");
});
});// e2e/api.spec.ts
import { test, expect } from "@playwright/test";
test.describe("API Tests", () => {
test("GET /api/products returns products", async ({ request }) => {
const response = await request.get("/api/products");
expect(response.ok()).toBe(true);
const products = await response.json();
expect(products.length).toBeGreaterThan(0);
expect(products[0]).toHaveProperty("id");
expect(products[0]).toHaveProperty("name");
expect(products[0]).toHaveProperty("price");
});
test("POST /api/products creates a product", async ({ request }) => {
const response = await request.post("/api/products", {
data: {
name: "New Widget",
price: 29.99,
},
});
expect(response.status()).toBe(201);
const product = await response.json();
expect(product.name).toBe("New Widget");
});
test("GET /api/products/:id returns 404 for missing product", async ({ request }) => {
const response = await request.get("/api/products/nonexistent");
expect(response.status()).toBe(404);
});
});// e2e/mocked.spec.ts
import { test, expect } from "@playwright/test";
test.describe("With Mocked API", () => {
test("shows empty state when no products", async ({ page }) => {
// Mock the API to return empty array
await page.route("/api/products", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([]),
});
});
await page.goto("/products");
await expect(page.getByText(/no products found/i)).toBeVisible();
});
test("handles API error gracefully", async ({ page }) => {
await page.route("/api/products", (route) => {
route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({ error: "Internal Server Error" }),
});
});
await page.goto("/products");
await expect(page.getByRole("alert")).toContainText(/error/i);
});
test("modifies API response", async ({ page }) => {
await page.route("/api/products", async (route) => {
// Fetch real response and modify it
const response = await route.fetch();
const json = await response.json();
const modified = json.map((p: { price: number }) => ({
...p,
price: 0, // everything free!
}));
await route.fulfill({ response, body: JSON.stringify(modified) });
});
await page.goto("/products");
await expect(page.getByText("$0.00").first()).toBeVisible();
});
});// e2e/accessibility.spec.ts
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test.describe("Accessibility", () => {
test("home page has no a11y violations", async ({ page }) => {
await page.goto("/");
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa"])
.analyze();
expect(results.violations).toEqual([]);
});
test("form page has no a11y violations", async ({ page }) => {
await page.goto("/contact");
const results = await new AxeBuilder({ page })
.include("#contact-form")
.analyze();
expect(results.violations).toEqual([]);
});
test("reports specific violations", async ({ page }) => {
await page.goto("/products");
const results = await new AxeBuilder({ page }).analyze();
// Log violations for debugging
for (const violation of results.violations) {
console.log(`${violation.id}: ${violation.description}`);
for (const node of violation.nodes) {
console.log(` ${node.html}`);
}
}
expect(results.violations).toEqual([]);
});
});Qué demuestra esto:
- Proyecto de configuración de autenticación que se ejecuta una vez y guarda el estado
- Todos los demás proyectos dependen de setup y reutilizan el estado de autenticación
- Pruebas de API con el fixture
requestde Playwright - Simulación de rutas con
page.route()para escenarios controlados - Pruebas de accesibilidad con axe-playwright
- Configuración de viewport móvil mediante presets de dispositivos
Profundización
Cómo funciona
storageStateguarda y restaura cookies y localStorage, así que las pruebas autenticadas no repiten el login- El proyecto de configuración se ejecuta antes que los proyectos dependientes — Playwright respeta el array
dependencies page.route()intercepta solicitudes de red que coinciden con un patrón de URL y te permite cumplirlas, abortarlas o modificarlasrequestes un fixture de Playwright para hacer solicitudes HTTP directamente — útil para pruebas de API sin una página del navegadorAxeBuilderejecuta el motor de accesibilidad axe-core dentro de la página y devuelve violaciones con metadatos detallados
Variaciones
Probar en varias pestañas:
test("opens product in new tab", async ({ page, context }) => {
await page.goto("/products");
// Listen for new page (tab)
const newPagePromise = context.waitForEvent("page");
await page.getByRole("link", { name: /widget/i }).click({
modifiers: ["Meta"], // Cmd+Click opens new tab
});
const newPage = await newPagePromise;
await newPage.waitForLoadState();
await expect(newPage).toHaveURL(/\/products\/widget/);
await expect(newPage.getByRole("heading")).toHaveText("Widget");
});Probar con distintos viewports:
test.describe("mobile navigation", () => {
test.use({
viewport: { width: 375, height: 667 },
isMobile: true,
hasTouch: true,
});
test("shows hamburger menu", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("button", { name: /menu/i })).toBeVisible();
await expect(page.getByRole("navigation")).not.toBeVisible();
});
});Varios roles de autenticación:
// playwright.config.ts
projects: [
{ name: "admin-setup", testMatch: /admin\.setup\.ts/ },
{ name: "user-setup", testMatch: /user\.setup\.ts/ },
{
name: "admin-tests",
use: { storageState: "e2e/.auth/admin.json" },
dependencies: ["admin-setup"],
testMatch: /.*admin.*\.spec\.ts/,
},
{
name: "user-tests",
use: { storageState: "e2e/.auth/user.json" },
dependencies: ["user-setup"],
testMatch: /.*user.*\.spec\.ts/,
},
],Notas de TypeScript
// Typed route handler
await page.route("/api/users", async (route: Route) => {
const method = route.request().method();
if (method === "GET") {
await route.fulfill({ body: JSON.stringify([]) });
} else {
await route.continue();
}
});
// Typed API response
interface Product {
id: number;
name: string;
price: number;
}
const response = await request.get("/api/products");
const products: Product[] = await response.json();Errores comunes
-
Archivo
storageStateen git — El archivo de estado de autenticación contiene tokens de sesión. Solución: Añadee2e/.auth/a.gitignore. -
Expiración del estado de autenticación — Si las sesiones expiran rápido, la configuración puede necesitar ejecutarse con más frecuencia. Solución: Usa tokens de prueba de larga duración o ejecuta setup antes de cada archivo de prueba con
test.beforeEach. -
page.routedebe llamarse antes de la navegación — Las rutas definidas después depage.goto()no interceptan las solicitudes de carga inicial de la página. Solución: Llama apage.route()antes depage.goto(). -
Falsos positivos de axe-core — Algunas violaciones pueden ser intencionales (p. ej., contraste de color en elementos de marca). Solución: Usa
.exclude()para excepciones conocidas o.disableRules()para reglas específicas:new AxeBuilder(\{ page \}) .exclude("#brand-banner") .disableRules(["color-contrast"]) .analyze(); -
Pruebas de API y autenticación — El fixture
requestno usastorageStatepor defecto. Solución: Usarequest.newContext({ storageState: "e2e/.auth/user.json" })o configúralo en la configuración del proyecto.
Alternativas
| Alternativa | Usar cuando | No usar cuando |
|---|---|---|
| Cypress intercept | Usas Cypress y necesitas simulación de rutas | Usas Playwright |
| Lighthouse CI | Necesitas auditorías de rendimiento y a11y en CI | Quieres aserciones granulares de violaciones de a11y |
| jest-axe | Quieres pruebas de a11y en tests unitarios (jsdom) | Necesitas renderizado en navegador real para resultados precisos |
| Supertest | Quieres tests de integración de API sin navegador | Necesitas contexto del navegador (cookies, autenticación) |
FAQs
¿Cómo funciona el uso compartido del estado de autenticación en Playwright?
- Un proyecto de configuración inicia sesión una vez y guarda cookies/localStorage en un archivo JSON vía
storageState. - Otros proyectos declaran una dependencia en setup y cargan ese archivo.
- Las pruebas se ejecutan ya autenticadas sin repetir el login.
Trampa: ¿Debe el archivo de autenticación storageState subirse a git?
No. Contiene tokens de sesión. Añade e2e/.auth/ a .gitignore.
¿Cómo simulo respuestas de API en Playwright?
await page.route("/api/products", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([{ id: 1, name: "Widget" }]),
});
});Llama a page.route() antes de page.goto().
¿Para qué sirve el fixture request?
Hace solicitudes HTTP directamente sin una página del navegador — útil para pruebas de API:
const response = await request.get("/api/products");
expect(response.ok()).toBe(true);¿Cómo ejecuto auditorías de accesibilidad con Playwright?
Instala @axe-core/playwright y usa:
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa"])
.analyze();
expect(results.violations).toEqual([]);¿Cómo pruebo con varios roles de autenticación (admin frente a usuario)?
Crea proyectos de configuración y archivos de estado de almacenamiento separados para cada rol. Usa patrones testMatch para dirigir las pruebas al proyecto correcto:
{ name: "admin-tests", use: { storageState: "e2e/.auth/admin.json" } }Trampa: ¿Por qué debe llamarse a page.route() antes de page.goto()?
Las rutas definidas después de la navegación no interceptan las solicitudes de carga inicial de la página. Registra siempre los mocks de ruta antes de navegar.
¿Cómo pruebo un escenario con varias pestañas?
const newPagePromise = context.waitForEvent("page");
await page.getByRole("link").click({ modifiers: ["Meta"] });
const newPage = await newPagePromise;
await newPage.waitForLoadState();¿Cómo pruebo viewports móviles?
test.use({
viewport: { width: 375, height: 667 },
isMobile: true,
hasTouch: true,
});O usa presets de dispositivos: test.use({ ...devices["iPhone 14"] }).
¿Cómo gestiono los falsos positivos de axe-core?
Excluye elementos específicos o desactiva reglas:
new AxeBuilder({ page })
.exclude("#brand-banner")
.disableRules(["color-contrast"])
.analyze();¿Cómo tipifico manejadores de ruta en TypeScript?
await page.route("/api/users", async (route: Route) => {
const method = route.request().method();
if (method === "GET") {
await route.fulfill({ body: JSON.stringify([]) });
} else {
await route.continue();
}
});¿El fixture request usa storageState por defecto?
No. Usa request.newContext({ storageState: "e2e/.auth/user.json" }) o configúralo en los ajustes del proyecto.
Relacionado
- Configuración E2E con Playwright — instalación y configuración básica
- Patrones E2E con Playwright — Page Object Model y localizadores
- Estrategia de pruebas y mejores prácticas — pirámide de pruebas y pipeline de CI
- Simulación en pruebas — patrones de simulación en tests unitarios