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.
Busca en todas las páginas de la documentación
Gestiona autenticación, pruebas de API, simulación de rutas, escenarios con varias pestañas, viewports móviles y auditorías de accesibilidad.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
// 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:
request de Playwrightpage.route() para escenarios controladosstorageState guarda y restaura cookies y localStorage, así que las pruebas autenticadas no repiten el logindependenciespage.route() intercepta solicitudes de red que coinciden con un patrón de URL y te permite cumplirlas, abortarlas o modificarlasrequest es un fixture de Playwright para hacer solicitudes HTTP directamente - útil para pruebas de API sin una página del navegadorAxeBuilder ejecuta el motor de accesibilidad axe-core dentro de la página y devuelve violaciones con metadatos detalladosProbar 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/,
},
],// 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();Archivo storageState en git - El archivo de estado de autenticación contiene tokens de sesión. Solución: Añade e2e/.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.route debe llamarse antes de la navegación - Las rutas definidas después de page.goto() no interceptan las solicitudes de carga inicial de la página. Solución: Llama a page.route() antes de page.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 request no usa storageState por defecto. Solución: Usa request.newContext({ storageState: "e2e/.auth/user.json" }) o configúralo en la configuración del proyecto.
| 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) |
storageState.No. Contiene tokens de sesión. Añade e2e/.auth/ a .gitignore.
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().
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);Instala @axe-core/playwright y usa:
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa"])
.analyze();
expect(results.violations).toEqual([]);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" } }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.
const newPagePromise = context.waitForEvent("page");
await page.getByRole("link").click({ modifiers: ["Meta"] });
const newPage = await newPagePromise;
await newPage.waitForLoadState();test.use({
viewport: { width: 375, height: 667 },
isMobile: true,
hasTouch: true,
});O usa presets de dispositivos: test.use({ ...devices["iPhone 14"] }).
Excluye elementos específicos o desactiva reglas:
new AxeBuilder({ page })
.exclude("#brand-banner")
.disableRules(["color-contrast"])
.analyze();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();
}
});No. Usa request.newContext({ storageState: "e2e/.auth/user.json" }) o configúralo en los ajustes del proyecto.
Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥