Patrones E2E con Playwright
Aplica Page Object Model, estrategias robustas de locators y patrones de aserciones para pruebas E2E mantenibles.
Busca en todas las páginas de la documentación
Aplica Page Object Model, estrategias robustas de locators y patrones de aserciones para pruebas E2E mantenibles.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de referencia rápida - lista para copiar y pegar.
import { test, expect, Page } from "@playwright/test";
// Locator strategies (prefer accessible queries)
page.getByRole("button", { name: /submit/i });
page.getByLabel("Email");
page.getByText("Welcome back");
page.getByPlaceholder("Search...");
page.getByTestId("checkout-form");
// Navigation and waiting
await page.goto("/products");
await page.waitForURL("/products/**");
await page.getByRole("link", { name: /details/i }).click();
// Form interaction
await page.getByLabel("Email").fill("alice@example.com");
await page.getByLabel("Password").fill("password123");
await page.getByRole("button", { name: /sign in/i }).click();
// Assertions (auto-retry)
await expect(page).toHaveURL("/dashboard");
await expect(page.getByRole("heading")).toHaveText("Dashboard");
await expect(page.getByRole("alert")).toBeVisible();
await expect(page.getByRole("button")).toBeEnabled();
// Screenshots
await page.screenshot({ path: "screenshots/dashboard.png" });
await expect(page).toHaveScreenshot("dashboard.png");Cuándo usarlo: Cuando escribes pruebas E2E que deben ser mantenibles, legibles y resistentes a cambios menores de la UI.
// e2e/pages/checkout-page.ts
import { Page, Locator, expect } from "@playwright/test";
export class CheckoutPage {
readonly page: Page;
readonly emailInput: Locator;
readonly nameInput: Locator;
readonly addressInput: Locator;
readonly cityInput: Locator;
readonly zipInput: Locator;
readonly cardNumberInput: Locator;
readonly submitButton: Locator;
readonly orderConfirmation: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel("Email");
this.nameInput = page.getByLabel("Full name");
this.addressInput = page.getByLabel("Address");
this.cityInput = page.getByLabel("City");
this.zipInput = page.getByLabel("ZIP code");
this.cardNumberInput = page.getByLabel("Card number");
this.submitButton = page.getByRole("button", { name: /place order/i });
this.orderConfirmation = page.getByRole("heading", { name: /order confirmed/i });
this.errorMessage = page.getByRole("alert");
}
async goto() {
await this.page.goto("/checkout");
}
async fillShippingInfo(info: {
email: string;
name: string;
address: string;
city: string;
zip: string;
}) {
await this.emailInput.fill(info.email);
await this.nameInput.fill(info.name);
await this.addressInput.fill(info.address);
await this.cityInput.fill(info.city);
await this.zipInput.fill(info.zip);
}
async fillPayment(cardNumber: string) {
await this.cardNumberInput.fill(cardNumber);
}
async submitOrder() {
await this.submitButton.click();
}
async expectOrderConfirmed() {
await expect(this.orderConfirmation).toBeVisible();
}
async expectError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}// e2e/pages/product-page.ts
import { Page, Locator, expect } from "@playwright/test";
export class ProductPage {
readonly page: Page;
readonly addToCartButton: Locator;
readonly cartCount: Locator;
readonly quantityInput: Locator;
constructor(page: Page) {
this.page = page;
this.addToCartButton = page.getByRole("button", { name: /add to cart/i });
this.cartCount = page.getByTestId("cart-count");
this.quantityInput = page.getByLabel("Quantity");
}
async goto(productSlug: string) {
await this.page.goto(`/products/${productSlug}`);
}
async setQuantity(quantity: number) {
await this.quantityInput.fill(String(quantity));
}
async addToCart() {
await this.addToCartButton.click();
}
async expectCartCount(count: number) {
await expect(this.cartCount).toHaveText(String(count));
}
}// e2e/checkout.spec.ts
import { test, expect } from "@playwright/test";
import { ProductPage } from "./pages/product-page";
import { CheckoutPage } from "./pages/checkout-page";
test.describe("Checkout Flow", () => {
test("complete purchase from product page to confirmation", async ({ page }) => {
// Add product to cart
const productPage = new ProductPage(page);
await productPage.goto("premium-widget");
await productPage.setQuantity(2);
await productPage.addToCart();
await productPage.expectCartCount(2);
// Navigate to checkout
await page.getByRole("link", { name: /checkout/i }).click();
await expect(page).toHaveURL("/checkout");
// Fill checkout form
const checkoutPage = new CheckoutPage(page);
await checkoutPage.fillShippingInfo({
email: "alice@example.com",
name: "Alice Johnson",
address: "123 Main St",
city: "Springfield",
zip: "62701",
});
await checkoutPage.fillPayment("4242424242424242");
await checkoutPage.submitOrder();
// Verify confirmation
await checkoutPage.expectOrderConfirmed();
await expect(page).toHaveURL(/\/orders\/[a-z0-9]+/);
});
test("shows validation errors for empty form", async ({ page }) => {
const checkoutPage = new CheckoutPage(page);
await checkoutPage.goto();
await checkoutPage.submitOrder();
await checkoutPage.expectError("Email is required");
});
test("shows error for invalid card", async ({ page }) => {
const checkoutPage = new CheckoutPage(page);
await checkoutPage.goto();
await checkoutPage.fillShippingInfo({
email: "alice@example.com",
name: "Alice Johnson",
address: "123 Main St",
city: "Springfield",
zip: "62701",
});
await checkoutPage.fillPayment("0000000000000000");
await checkoutPage.submitOrder();
await checkoutPage.expectError("Invalid card number");
});
});Qué demuestra esto:
expect() reintentan automáticamente hasta pasar o agotar el tiempo de espera (5 segundos por defecto) - no necesitas esperas manualesfill() limpia el input primero y luego escribe el valor - a diferencia de type(), que añade carácter por carácterclick() en un enlace espera a que la navegación termineComparación de estrategias de locators:
| Estrategia | Ejemplo | Cuándo usarlo |
|---|---|---|
getByRole | page.getByRole("button", { name: /enviar/i }) | El elemento tiene un rol ARIA (preferido) |
getByLabel | page.getByLabel("Correo electrónico") | Inputs de formulario con etiquetas |
getByText | page.getByText("¡Hola!") | Contenido de texto estático |
getByPlaceholder | page.getByPlaceholder("Buscar...") | Placeholders de inputs |
getByTestId | page.getByTestId("sidebar") | Ninguna consulta semántica funciona |
locator | page.locator(".custom-dropdown") | Respaldo con selector CSS |
Pruebas de regresión visual:
test("product page visual", async ({ page }) => {
await page.goto("/products/widget");
// Full page screenshot comparison
await expect(page).toHaveScreenshot("product-page.png", {
maxDiffPixelRatio: 0.01,
});
// Element-level screenshot
const card = page.getByTestId("product-card");
await expect(card).toHaveScreenshot("product-card.png");
});
// First run creates baseline screenshots in __screenshots__/
// Subsequent runs compare against baselines
// Update baselines: npx playwright test --update-snapshotsEstrategias de espera:
// Wait for network idle (all requests finished)
await page.goto("/dashboard", { waitUntil: "networkidle" });
// Wait for a specific response
const response = await page.waitForResponse("/api/products");
expect(response.status()).toBe(200);
// Wait for element state
await page.getByRole("button").waitFor({ state: "visible" });
await page.getByText("Loading").waitFor({ state: "hidden" });// Page objects benefit from strict typing
interface ShippingInfo {
email: string;
name: string;
address: string;
city: string;
zip: string;
}
// Extend test fixtures for page objects
import { test as base } from "@playwright/test";
type Fixtures = {
checkoutPage: CheckoutPage;
productPage: ProductPage;
};
export const test = base.extend<Fixtures>({
checkoutPage: async ({ page }, use) => {
await use(new CheckoutPage(page));
},
productPage: async ({ page }, use) => {
await use(new ProductPage(page));
},
});
// Use in tests
test("checkout", async ({ checkoutPage, productPage }) => {
await productPage.goto("widget");
// ...
});Selectores CSS frágiles - page.locator(".btn-primary.mt-4") se rompe cuando cambian las clases. Solución: Usa getByRole, getByLabel o getByTestId en su lugar.
No esperar la navegación - Hacer clic en un enlace y afirmar de inmediato puede fallar si la página no ha cargado. Solución: Playwright espera automáticamente en click, pero añade await expect(page).toHaveURL(...) para verificación explícita.
Líneas base de capturas entre sistemas operativos - Las fuentes se renderizan distinto en macOS, Linux y Windows. Solución: Ejecuta las pruebas de capturas en Docker o solo en un SO en CI.
Page objects excesivamente granulares - Crear un page object para cada sección pequeña añade indirección. Solución: Un page object por página o sección principal. Mantén la practicidad.
Usar page.waitForTimeout() - Las esperas codificadas hacen las pruebas lentas e inestables. Solución: Usa aserciones con espera automática (expect(...).toBeVisible()) o waitForResponse en su lugar.
| Alternativa | Cuándo usarlo | Cuándo no usarlo |
|---|---|---|
| Locators inline (sin POM) | Suites de prueba pequeñas con pocas páginas | Las pruebas superan una docena de archivos |
| Page objects basados en fixtures | Quieres configuración automática de page objects mediante fixtures de test | La construcción simple de POM en el cuerpo del test es suficiente |
| Cypress | Prefieres la depuración con viaje en el tiempo de Cypress o pruebas de componentes | Necesitas soporte multi-navegador |
| Percy / Chromatic | Necesitas regresión visual empresarial con flujo de revisión | La comparación de capturas integrada es suficiente |
POM encapsula locators e interacciones de una página en una clase. Cuando la UI cambia, actualizas el page object una vez en lugar de cada prueba que referencia esa página.
getByRole - rol ARIA accesible (preferido)getByLabel - etiquetas de inputs de formulariogetByText - contenido de texto estáticogetByPlaceholder - placeholders de inputsgetByTestId - último recursolocator() - respaldo con selector CSSfill() limpia el input primero y luego establece el valor.type() añade caracteres uno por uno.fill() para la mayoría de pruebas de formularios.await expect(page).toHaveScreenshot("dashboard.png", {
maxDiffPixelRatio: 0.01,
});La primera ejecución crea capturas base. Las siguientes comparan contra ellas. Actualiza con --update-snapshots.
Las fuentes se renderizan distinto en macOS, Linux y Windows. Ejecuta las pruebas de capturas en Docker o restríngelas a un solo SO en CI.
const response = await page.waitForResponse("/api/products");
expect(response.status()).toBe(200);export const test = base.extend<Fixtures>({
checkoutPage: async ({ page }, use) => {
await use(new CheckoutPage(page));
},
});
test("checkout", async ({ checkoutPage }) => { /* ... */ });Las esperas codificadas hacen las pruebas lentas e inestables. Usa aserciones con espera automática como expect(...).toBeVisible() o waitForResponse en su lugar.
interface ShippingInfo {
email: string;
name: string;
address: string;
city: string;
zip: string;
}
async fillShippingInfo(info: ShippingInfo) { /* ... */ }Para suites de prueba pequeñas con pocas páginas, los locators inline son más simples. POM añade indirección - adóptalo solo cuando las pruebas superen una docena de archivos o las páginas se reutilicen en muchas pruebas.
Usan patrones de consulta accesibles similares (getByRole, getByLabel, getByText), pero los locators de Playwright son perezosos - se evalúan cuando se ejecuta una acción o aserción, no al crearlos.
const card = page.getByTestId("product-card");
await expect(card).toHaveScreenshot("product-card.png");Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥