Cheatsheet Playwright
Framework da Microsoft para testes end-to-end e automação web
Playwright
Instalação e Setup
Iniciar proyecto
npm init playwright@latest
Asistente oficial que crea la estructura completa. Genera playwright.config.ts, la carpeta tests/ y GitHub Actions. Pregunta lenguaje, navegadores y CI.
Estructura de archivos
tests/
login.spec.ts
cart.spec.ts
fixtures/
auth.ts
playwright.config.ts
package.jsonLas pruebas viven en archivos .spec.ts. fixtures/ guarda fixtures personalizados. La config está en la raíz. Cada archivo puede tener múltiples pruebas organizadas por funcionalidad.
Global setup y teardown
export default defineConfig({
globalSetup: "./global-setup.ts",
globalTeardown: "./global-teardown.ts",
});
// global-setup.ts
export default async function () {
// seed BD, crear datos de prueba
}globalSetup se ejecuta una vez antes de todas las pruebas. globalTeardown se ejecuta al final. Ideal para seed de BD o limpiar recursos. Recibe config como argumento.
Instalar navegadores
npx playwright install npx playwright install chromium npx playwright install --with-deps
install descarga Chromium, Firefox y WebKit. --with-deps instala dependencias del sistema (Linux). Puedes instalar solo un navegador específico.
Primera prueba
import { test, expect } from "@playwright/test";
test("la página inicial tiene título", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle(/Mi App/);
});test() define una prueba con nombre y callback. page se inyecta vía fixtures. expect() hace aserciones con auto-retry. goto() navega a la URL (usa baseURL).
TypeScript e imports
import { test, expect, type Page } from "@playwright/test";
async function login(page: Page, user: string) {
await page.goto("/login");
await page.getByLabel("Email").fill(user);
await page.getByRole("button", { name: "Entrar" }).click();
}type Page tipa parámetros en helpers. Las funciones auxiliares reciben page como argumento. TypeScript da autocompletado y detecta errores. Totalmente soportado de forma nativa.
Codegen (grabar acciones)
npx playwright codegen wikipedia.org npx playwright codegen --target=python npx playwright codegen --device="iPhone 13"
codegen abre un navegador y graba las acciones como código. --target cambia el lenguaje de salida. --device simula dispositivos móviles. Ideal para aprender selectores.
Projects (multi-navegador)
export default defineConfig({
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "mobile", use: { ...devices["iPhone 13"] } },
],
});projects ejecuta las pruebas en múltiples navegadores. devices preconfigura viewport y user-agent. Cada project puede tener su propia config. Las pruebas se ejecutan en todos por defecto.
Entornos y variables
// .env
BASE_URL=http://localhost:3000
// playwright.config.ts
use: {
baseURL: process.env.BASE_URL || "http://localhost:3000",
}
// ejecutar con env:
// BASE_URL=https://staging.example.com npx playwright testprocess.env lee variables de entorno. Permite probar contra staging o producción. dotenv puede cargar archivos .env. Nunca hagas commit de credenciales.
Archivo de configuración
// playwright.config.ts
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
timeout: 30000,
retries: 2,
workers: 4,
use: {
baseURL: "http://localhost:3000",
headless: true,
},
});defineConfig tipa la configuración. testDir define dónde están las pruebas. timeout es el límite por prueba. use configura opciones compartidas por todas las pruebas.
Servidor web automático
export default defineConfig({
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
});webServer inicia la app antes de las pruebas. command es el comando de arranque. reuseExistingServer evita reiniciar en dev. En CI, siempre fuerza un servidor nuevo.
Extensiones y plugins
// eslint-plugin-playwright
// .eslintrc
{
"plugins": ["playwright"],
"extends": ["plugin:playwright/recommended"]
}
// @axe-core/playwright (accesibilidad)
import AxeBuilder from "@axe-core/playwright";eslint-plugin-playwright aplica buenas prácticas vía lint. @axe-core/playwright prueba accesibilidad. La comunidad tiene plugins para reporting, visuales y más. Instala vía npm.
Estrutura de Testes
Prueba simple
test("inicia sesión con éxito", async ({ page }) => {
await page.goto("/login");
await page.getByLabel("Email").fill("ana@site.com");
await page.getByLabel("Password").fill("123456");
await page.getByRole("button", { name: "Entrar" }).click();
await expect(page).toHaveURL("/dashboard");
});Cada test() recibe fixtures vía destructuring. page es el contexto de navegador aislado. La prueba falla si algún expect() no pasa. Los nombres deben describir el comportamiento.
Fixtures disponibles
test("ejemplo", async ({
page, // Page - página principal
context, // BrowserContext - contexto aislado
browser, // Browser - instancia del navegador
request, // APIRequestContext - peticiones HTTP
}) => {
// ...
});page es la página con todas las interacciones. context permite crear múltiples páginas. browser es la instancia de bajo nivel. request hace llamadas API directas.
Pruebas paralelas
test.describe.configure({ mode: "parallel" });
test("prueba A", async ({ page }) => { });
test("prueba B", async ({ page }) => { });
test("prueba C", async ({ page }) => { });mode: "parallel" ejecuta las pruebas del grupo simultáneamente. Por defecto, las pruebas del mismo archivo son secuenciales. Cada prueba tiene un navegador aislado. Acelera suites grandes.
Soft assertions
test("verifica múltiples campos", async ({ page }) => {
await expect.soft(page.getByText("Nombre")).toHaveText("Ana");
await expect.soft(page.getByText("Email")).toHaveText("ana@site.com");
await expect.soft(page.getByText("Edad")).toHaveText("30");
// todas se verifican aunque una falle
});expect.soft() no detiene la prueba al fallar. Todas las aserciones se ejecutan. El informe muestra todas las fallas. Ideal para verificar múltiples campos de una vez.
Agrupar con describe
test.describe("Carrito de compras", () => {
test("añade ítem", async ({ page }) => { });
test("elimina ítem", async ({ page }) => { });
test("calcula el total", async ({ page }) => { });
});test.describe() agrupa pruebas relacionadas. El nombre aparece como prefijo en el informe. Puede anidarse. Útil para organizar por funcionalidad o módulo.
Fixtures personalizados
import { test as base } from "@playwright/test";
export const test = base.extend({
loggedInPage: async ({ page }, use) => {
await page.goto("/login");
await page.getByLabel("Email").fill("admin@site.com");
await page.getByRole("button").click();
await use(page);
},
});
// uso: test("...", async ({ loggedInPage }) => { });base.extend() crea fixtures personalizados. El código antes de use() es el setup. use(page) lo entrega a la prueba. El código después es teardown. Reutilizable en múltiples archivos.
Pruebas seriales
test.describe.configure({ mode: "serial" });
test("crea cuenta", async ({ page }) => { });
test("inicia sesión", async ({ page }) => { });
test("verifica perfil", async ({ page }) => { });mode: "serial" garantiza el orden y se detiene si una falla. Útil cuando las pruebas dependen del estado anterior. Si la primera falla, el resto se omiten. Evita una cascada de errores.
Hooks beforeEach/afterEach
test.describe("Panel admin", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/admin/login");
await login(page);
});
test.afterEach(async ({ page }) => {
await page.evaluate(() => localStorage.clear());
});
});beforeEach se ejecuta antes de cada prueba del grupo. afterEach se ejecuta después. Ideal para setup repetitivo como login. Reduce la duplicación de código entre pruebas.
Opciones por archivo (test.use)
test.use({
baseURL: "http://localhost:8080",
viewport: { width: 1920, height: 1080 },
locale: "pt-PT",
timezoneId: "Europe/Lisbon",
});test.use() configura todas las pruebas del archivo. viewport define la resolución. locale y timezoneId prueban internacionalización. Sobrescribe la config global.
Retries
// config global:
export default defineConfig({
retries: process.env.CI ? 2 : 0,
});
// por prueba:
test("operación inestable", async ({ page }) => { });
test.describe.configure({ retries: 3 });retries re-ejecuta las pruebas que fallan. En CI usa 2, en local 0. Las pruebas flaky pasan en el retry. El informe muestra los intentos. No abuses — corrige la causa raíz.
Hooks beforeAll/afterAll
test.describe("API tests", () => {
test.beforeAll(async () => {
await seedDatabase();
});
test.afterAll(async () => {
await cleanupDatabase();
});
});beforeAll se ejecuta una vez antes de todas las pruebas del grupo. afterAll se ejecuta una vez al final. Para setup costoso (seed BD, crear servidor). No recibe page por defecto.
Skip y only
test.skip("prueba rota", async ({ page }) => { });
test.only("enfoca esta", async ({ page }) => { });
test.fixme("necesita arreglo", async ({ page }) => { });
// condicional:
test.skip(process.env.CI === "true", "Solo local");test.skip() ignora la prueba. test.only() ejecuta solo esa (debug). test.fixme() la marca como known broken. El skip condicional acepta un booleano y una razón.
Anotaciones y metadata
test("checkout", async ({ page }) => {
test.slow(); // triplica el timeout
test.setTimeout(60000); // timeout personalizado
});
// anotaciones en el informe:
test.info().annotations.push({
type: "issue",
description: "https://github.com/app/issues/42",
});test.slow() triplica el timeout para pruebas lentas. setTimeout() define un límite personalizado. annotations añade metadata al informe HTML. Útil para enlazar a issues.
Localizadores
Por role (recomendado)
page.getByRole("button", { name: "Enviar" })
page.getByRole("heading", { name: "Bienvenido" })
page.getByRole("link", { name: "Acerca de" })
page.getByRole("checkbox", { name: "Acepto" })getByRole() localiza por rol de accesibilidad. Es el método recomendado por el equipo de Playwright. name filtra por el texto accesible. Refleja cómo los usuarios y lectores de pantalla ven la página.
Por test id
page.getByTestId("btn-submit")
page.getByTestId("nav-menu")
// en el HTML:
// <button data-testid="btn-submit">Enviar</button>getByTestId() usa el atributo data-testid. Un selector estable que no cambia con CSS o texto. Configura el atributo en la config si es diferente. Un último recurso cuando role/text no bastan.
Posición y navegación
list.first()
list.last()
list.nth(2)
// navegar por la jerarquía:
row.locator("span") // descendiente
page.locator("div").locator("p") // encadenarfirst(), last() y nth() seleccionan por posición. locator() dentro de otro localiza descendientes. Encadenar reduce progresivamente. El índice es 0-based.
Locators vs ElementHandle
// Recomendado: Locator (lazy, auto-wait)
const btn = page.getByRole("button", { name: "OK" });
await btn.click();
// Evitar: ElementHandle (eager, sin retry)
const el = await page.$(".button");
await el?.click();Locator es lazy y se reevalúa en cada acción. Hace auto-wait y retry automáticamente. ElementHandle es eager y puede quedar stale. Prefiere siempre Locators — son más robustos.
Por texto
page.getByText("Bienvenido")
page.getByText("Bienvenido", { exact: true })
page.getByText(/bienvenido/i)getByText() localiza por el texto visible. Sin exact, hace match parcial. exact: true exige correspondencia total. Acepta regex para patrones flexibles.
Por alt y title
page.getByAltText("Logo de la empresa")
page.getByTitle("Ajustes")getByAltText() localiza imágenes por su alt. getByTitle() localiza por el atributo title (tooltip). Ambos son accesibles y descriptivos. Prefiere alt para imágenes.
Localizar por contenido
// elemento que contiene otro:
page.locator("div", { has: page.getByText("Precio") })
// elemento con texto específico:
page.locator("li", { hasText: "Producto A" })
// layout: ítem a la derecha de otro
page.getByText("Total").locator("..")has localiza un padre que contiene un hijo específico. hasText filtra por texto interno. .. sube al elemento padre. Útil para localizar contenedores sin test-id.
Por label
page.getByLabel("Email")
page.getByLabel("Password", { exact: true })getByLabel() localiza inputs por su label asociado. Ideal para formularios. Funciona con <label> y aria-label. Más resiliente que los selectores CSS.
CSS y XPath
page.locator(".button.primary")
page.locator("#login-form")
page.locator("xpath=//div[@class='card']")
page.locator("div.card >> text=Detalles")locator() acepta selectores CSS y XPath. xpath= prefija un XPath explícito. >> encadena selectores. Úsalo como último recurso — prefiere locators semánticos.
Múltiples elementos
const items = page.getByRole("listitem");
const count = await items.count();
for (let i = 0; i < count; i++) {
const text = await items.nth(i).textContent();
console.log(text);
}
// o: allTextContents()
const texts = await items.allTextContents();count() devuelve cuántos elementos coinciden. nth(i) accede a cada uno. allTextContents() extrae el texto de todos. Los locators son lazy — se evalúan al interactuar.
Por placeholder
page.getByPlaceholder("nombre@ejemplo.com")
page.getByPlaceholder("Buscar...")getByPlaceholder() localiza por el atributo placeholder. Útil cuando no hay label visible. Menos robusto que getByLabel(). El placeholder puede cambiar con rediseños.
Filtrar localizadores
const rows = page.getByRole("listitem");
rows.filter({ hasText: "Activo" })
rows.filter({ has: page.getByRole("button") })
rows.filter({ hasText: "Ana" }).filter({ hasText: "Admin" })filter() refina un localizador existente. hasText filtra por texto contenido. has filtra por un elemento hijo. Puedes encadenar múltiples filtros para precisión.
Esperar por un localizador
await page.getByText("Cargando").waitFor({ state: "hidden" });
await page.getByRole("dialog").waitFor({ state: "visible" });
await locator.waitFor({ timeout: 10000 });waitFor() espera hasta que el elemento esté en el estado deseado. state: "visible" espera a que aparezca. state: "hidden" espera a que desaparezca. Un timeout personalizado sobrescribe el global.
Ações e Interações
Clic
await locator.click();
await locator.click({ button: "right" });
await locator.click({ clickCount: 2 });
await locator.click({ modifiers: ["Control"] });click() hace auto-wait por visibilidad y estabilidad. button: "right" es clic derecho. clickCount: 2 es doble clic. modifiers simula Ctrl/Shift+clic.
Select / dropdown
await select.selectOption("pt");
await select.selectOption({ label: "Portugués" });
await select.selectOption({ value: "pt" });
await select.selectOption(["a", "b"]); // multi-selectselectOption() elige por value, label o index. Para multi-select, pasa un array. Dispara un evento change. Funciona con <select> nativos.
Scroll
await locator.scrollIntoViewIfNeeded(); await page.mouse.wheel(0, 500); await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
scrollIntoViewIfNeeded() hace visible el elemento. mouse.wheel() hace scroll por píxeles. evaluate() ejecuta JS nativo para scroll. Las acciones de clic ya hacen scroll automático.
Acciones en secuencia
await page.getByLabel("Nombre").fill("Ana");
await page.getByLabel("Email").fill("ana@site.com");
await page.getByRole("combobox").selectOption("PT");
await page.getByRole("checkbox").check();
await page.getByRole("button", { name: "Registrar" }).click();Cada acción hace auto-wait antes de ejecutarse. No necesitas sleep() ni waits manuales. La secuencia es determinista. Si un elemento no aparece, la prueba falla con un timeout claro.
Rellenar y limpiar
await input.fill("texto nuevo");
await input.clear();
await input.fill(""); // también limpia
await input.type("lento", { delay: 100 });fill() define el valor al instante. clear() limpia el campo. type() simula escritura tecla a tecla con delay. fill() dispara eventos de input/change.
Hover y foco
await locator.hover();
await locator.focus();
await locator.blur();
// hover con posición:
await locator.hover({ position: { x: 10, y: 5 } });hover() mueve el ratón sobre el elemento (tooltips, menús). focus() da foco de teclado. blur() quita el foco. position especifica el punto exacto del hover.
Acción + navegación
await Promise.all([
page.waitForURL("**/dashboard"),
button.click(),
]);
// o con waitForNavigation:
await Promise.all([
page.waitForNavigation(),
link.click(),
]);Promise.all() espera navegación y clic simultáneamente. Evita race conditions. waitForURL() espera una URL específica. Patrón esencial para clics que navegan.
Teclas y atajos
await input.press("Enter");
await input.press("Tab");
await page.keyboard.press("Control+A");
await page.keyboard.type("Hola mundo");
await page.keyboard.down("Shift");
await page.keyboard.up("Shift");press() simula una tecla en el elemento. keyboard.press() la envía globalmente. Combinaciones con + (Control+A). down()/up() para mantenerla pulsada.
Drag and drop
await source.dragTo(target); // o manualmente: await source.hover(); await page.mouse.down(); await target.hover(); await page.mouse.up();
dragTo() arrastra un elemento sobre otro. Para control fino usa mouse.down()/mouse.up(). Funciona con bibliotecas de DnD. Hace auto-wait por ambos elementos.
Evaluate (JavaScript)
const title = await page.evaluate(() => document.title);
await page.evaluate((el) => el.classList.add("active"), locator);
const data = await page.evaluate(() => JSON.parse(localStorage.getItem("app")));evaluate() ejecuta JavaScript en el navegador. Devuelve valores serializables. Puedes pasar elementos como argumento. Úsalo cuando no hay API de Playwright para la acción.
Checkbox y radio
await checkbox.check(); await checkbox.uncheck(); await radio.check(); // selecciona // verificar el estado: const checked = await checkbox.isChecked();
check() marca (idempotente). uncheck() desmarca. Para radio buttons, check() selecciona la opción. isChecked() devuelve el estado actual como booleano.
Subida de archivos
await input.setInputFiles("foto.png");
await input.setInputFiles(["a.png", "b.png"]);
// sin input visible (filechooser):
const [fileChooser] = await Promise.all([
page.waitForEvent("filechooser"),
button.click(),
]);
await fileChooser.setFiles("doc.pdf");setInputFiles() define archivos en un input file. Para subir vía botón, usa el evento filechooser. Acepta paths o buffers. Múltiples archivos vía array.
Touch y mobile
await locator.tap(); await page.touchscreen.tap(100, 200); // swipe: await page.touchscreen.tap(50, 300); // usar el ratón para simular un swipe await page.mouse.move(50, 300); await page.mouse.down(); await page.mouse.move(300, 300); await page.mouse.up();
tap() simula un toque en mobile. touchscreen.tap() con coordenadas. Para un swipe usa una secuencia de ratón. Configura hasTouch: true en el project para emular touch.
Asserções
Visibilidad
await expect(locator).toBeVisible(); await expect(locator).toBeHidden(); await expect(locator).toBeAttached(); await expect(locator).not.toBeAttached();
toBeVisible() verifica que está visible en el viewport. toBeHidden() verifica oculto o ausente. toBeAttached() verifica presencia en el DOM (aunque esté oculto). Auto-retry hasta timeout.
Recuento
await expect(items).toHaveCount(3); await expect(items).not.toHaveCount(0);
toHaveCount() verifica el número de elementos coincidentes. Auto-retry hasta que el recuento se estabilice. Útil para listas dinámicas tras filtros o adiciones.
Negación
await expect(locator).not.toBeVisible();
await expect(locator).not.toHaveText("Error");
await expect(page).not.toHaveURL("/login");.not niega cualquier aserción. También hace auto-retry (espera la condición negativa). Útil para verificar que los errores no aparecen o que los modales se han cerrado.
Aserciones en secuencia
const card = page.getByTestId("product-1");
await expect(card).toBeVisible();
await expect(card.getByRole("heading")).toHaveText("Producto A");
await expect(card.getByText("Precio")).toContainText("19,99€");
await expect(card.getByRole("button")).toBeEnabled();Encadena aserciones para verificar componentes completos. Cada expect() espera de forma independiente. Si una falla, el test se detiene (excepto soft). Organiza por orden visual para debugging fácil.
Texto
await expect(locator).toHaveText("Hola Mundo");
await expect(locator).toContainText("Mundo");
await expect(locator).toHaveText(/hola/i);
await expect(list).toHaveText(["A", "B", "C"]);toHaveText() verifica texto exacto (normaliza espacios). toContainText() verifica substring. Acepta regex. Para listas, pasa un array para verificar todos los ítems.
Atributos y CSS
await expect(link).toHaveAttribute("href", "/about");
await expect(el).toHaveClass(/active/);
await expect(el).toHaveCSS("color", "rgb(255, 0, 0)");
await expect(el).toHaveId("main-content");toHaveAttribute() verifica atributos HTML. toHaveClass() verifica clases (acepta regex). toHaveCSS() verifica estilos computados. toHaveId() verifica el ID.
Auto-retry y timeout
// timeout global: 5s por defecto
// timeout custom por aserción:
await expect(locator).toBeVisible({ timeout: 10000 });
// las aserciones reintentan automáticamente
// hasta el timeout — nunca uses sleep()Las aserciones hacen polling automático hasta pasar o expirar. Un timeout custom sobrescribe el global (5s). Elimina la necesidad de waitForTimeout(). Hace los tests deterministas.
Valor de input
await expect(input).toHaveValue("abc");
await expect(input).toHaveValue(/@site\.com$/);
await expect(input).toBeEmpty();toHaveValue() verifica el valor actual del input. Acepta una string exacta o regex. toBeEmpty() verifica un campo vacío. Funciona con inputs text, number y date.
Página (URL y título)
await expect(page).toHaveURL("/dashboard");
await expect(page).toHaveURL(/.*\/dashboard/);
await expect(page).toHaveTitle("Panel - App");
await expect(page).toHaveTitle(/Panel/);toHaveURL() verifica la URL actual (acepta regex). toHaveTitle() verifica el título de la página. Auto-retry espera a que la navegación se complete. Esencial tras redirects.
Aserciones genéricas
import { expect } from "@playwright/test";
expect(2 + 2).toBe(4);
expect([1, 2, 3]).toContain(2);
expect({ name: "Ana" }).toEqual({ name: "Ana" });
expect("hola mundo").toMatch(/mundo/);El expect de Playwright incluye aserciones genéricas. toBe() compara valores. toContain() verifica inclusión. toEqual() compara objetos. Sin auto-retry (son síncronas).
Estado de elementos
await expect(checkbox).toBeChecked(); await expect(checkbox).not.toBeChecked(); await expect(button).toBeEnabled(); await expect(button).toBeDisabled(); await expect(input).toBeFocused(); await expect(input).toBeEditable();
toBeChecked() para checkboxes/radios. toBeEnabled()/toBeDisabled() verifican interactividad. toBeFocused() verifica foco activo. toBeEditable() verifica que acepta input.
Comparación de screenshots
await expect(page).toHaveScreenshot("home.png");
await expect(locator).toHaveScreenshot("card.png");
await expect(page).toHaveScreenshot("home.png", {
maxDiffPixelRatio: 0.01,
});toHaveScreenshot() compara contra un screenshot de referencia. En la primera ejecución crea el baseline. maxDiffPixelRatio define la tolerancia. Detecta regresiones visuales automáticamente.
API response assertions
const response = await request.get("/api/users");
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.users).toHaveLength(5);El fixture request hace llamadas HTTP directas. ok() verifica status 2xx. status() devuelve el código exacto. json() parsea el body. Ideal para probar APIs sin UI.
Navegação e Página
Ir a URL
await page.goto("https://site.com");
await page.goto("/login"); // usa baseURL
await page.goto("/admin", { waitUntil: "networkidle" });goto() navega a una URL. Con baseURL configurado, los paths relativos funcionan. waitUntil controla cuándo se considera cargado: load, domcontentloaded o networkidle.
URL y título actuales
const url = page.url(); const title = await page.title(); const content = await page.content();
page.url() devuelve la URL actual (síncrono). page.title() devuelve el título (async). page.content() devuelve el HTML completo. Útiles para debugging y aserciones custom.
Emulación de dispositivo
import { devices } from "@playwright/test";
test.use({ ...devices["iPhone 13"] });
// o inline:
test.use({
viewport: { width: 375, height: 812 },
userAgent: "Mozilla/5.0 (iPhone...)",
hasTouch: true,
isMobile: true,
});devices tiene presets de viewport, UA y touch. hasTouch activa eventos touch. isMobile activa comportamiento mobile. Prueba la responsividad sin CSS hacks.
Atrás / adelante / recargar
await page.goBack();
await page.goForward();
await page.reload();
await page.reload({ waitUntil: "networkidle" });goBack() y goForward() navegan por el historial. reload() recarga la página actual. Todos aceptan waitUntil. Útiles para probar la persistencia de estado.
Estado de carga
await page.waitForLoadState("load");
await page.waitForLoadState("domcontentloaded");
await page.waitForLoadState("networkidle");load espera recursos (imágenes, CSS). domcontentloaded espera el DOM parseado. networkidle espera 500ms sin peticiones de red. El más estricto es networkidle pero puede ser lento.
Geolocalización y permisos
test.use({
geolocation: { latitude: 38.72, longitude: -9.14 },
permissions: ["geolocation"],
});
// en el test:
await page.goto("/mapa");
await expect(page.getByText("Lisbon")).toBeVisible();geolocation simula coordenadas GPS. permissions concede permisos sin dialog. Funciona con camera, microphone, notifications. Prueba features location-based.
Esperar URL
await page.waitForURL("**/dashboard");
await page.waitForURL(/.*\/users\/\d+/);
await page.waitForURL("**/login", { timeout: 5000 });waitForURL() espera hasta que la URL coincida. Acepta glob (**) o regex. Esencial tras redirects asíncronos. Un timeout custom para operaciones lentas.
Frames e iframes
const frame = page.frameLocator("iframe#editor");
await frame.getByRole("button", { name: "Guardar" }).click();
// frame por name o URL:
const frame2 = page.frameLocator('iframe[name="pagado"]');frameLocator() accede a contenido dentro de iframes. Todas las operaciones funcionan normalmente dentro del frame. Encadena para iframes anidados. Esencial para embeds de pagado.
Emulación de red
test.use({
// simular red lenta:
contextOptions: {
// offline:
},
});
// offline:
await context.setOffline(true);
await page.reload();
await expect(page.getByText("Sin conexión")).toBeVisible();
await context.setOffline(false);setOffline(true) simula una pérdida de conexión. Prueba estados offline y retry. setOffline(false) lo restaura. Esencial para PWAs y apps con modo offline.
Nueva página / pestaña
const newPage = await context.newPage();
await newPage.goto("/");
// capturar popup:
const [popup] = await Promise.all([
context.waitForEvent("page"),
page.getByRole("link", { name: "Abrir" }).click(),
]);
await popup.waitForLoadState();context.newPage() abre una nueva pestaña. waitForEvent("page") captura popups abiertos por clics. Cada página es independiente. Los popups comparten el mismo context (cookies).
Cookies y storage
// cookies:
await context.addCookies([{ name: "theme", value: "dark", url: "/" }]);
const cookies = await context.cookies();
// localStorage:
await page.evaluate(() => localStorage.setItem("token", "abc"));
const value = await page.evaluate(() => localStorage.getItem("token"));addCookies() inyecta cookies en el context. cookies() las lista todas. evaluate() accede a localStorage/sessionStorage. Útil para preparar estado sin login manual.
Múltiples contextos
const userA = await browser.newContext();
const userB = await browser.newContext();
const pageA = await userA.newPage();
const pageB = await userB.newPage();
// sesiones independientes (cookies separadas)
await pageA.goto("/login");
await pageB.goto("/login");browser.newContext() crea sesiones aisladas. Las cookies y el storage son independientes. Ideal para probar chat, permisos multiusuario. Cada context es como un browser nuevo.
Avançado
Traces (depuración)
// config: grabar trace en fallos
export default defineConfig({
use: {
trace: "on-first-retry",
// "on" | "off" | "retain-on-failure"
},
});
// manual:
await context.tracing.start({ screenshots: true, snapshots: true });
await context.tracing.stop({ path: "trace.zip" });trace graba screenshots, DOM snapshots y red. on-first-retry graba solo en el retry. Ábrelo con npx playwright show-trace trace.zip. Muestra la timeline completa del test.
Descargas
const [download] = await Promise.all([
page.waitForEvent("download"),
page.getByRole("button", { name: "Descargar" }).click(),
]);
const path = await download.path();
const name = download.suggestedFilename();
await download.saveAs("downloads/" + name);waitForEvent("download") captura la descarga. suggestedFilename() da el nombre original. saveAs() la guarda en disco. path() devuelve el path temporal. Combínalo con Promise.all.
Clock y timers
await page.clock.install({ time: new Date("2025-01-01") });
await page.clock.pauseAt(new Date("2025-06-15"));
await page.clock.fastForward("30:00");
await page.clock.runFor(5000);
await page.clock.resume();clock.install() controla el tiempo en el navegador. pauseAt() lo congela en una fecha. fastForward() avanza sin esperar. Prueba countdowns, sesiones expiradas y agendamientos.
Sharding (CI)
// dividir tests en 4 máquinas: npx playwright test --shard=1/4 npx playwright test --shard=2/4 npx playwright test --shard=3/4 npx playwright test --shard=4/4 // merge reports: npx playwright merge-reports ./reports
--shard divide la suite en partes para CI paralelo. Cada shard se ejecuta en una máquina diferente. merge-reports combina los resultados. Reduce el tiempo total en pipelines CI/CD.
Screenshots
await page.screenshot({ path: "full.png", fullPage: true });
await locator.screenshot({ path: "componente.png" });
// config: screenshot en fallo
use: {
screenshot: "only-on-failure",
}screenshot() captura la página o un elemento. fullPage: true incluye el scroll completo. only-on-failure en el config lo guarda solo en fallo. Adjuntado al reporte HTML.
Comparación visual
await expect(page).toHaveScreenshot("homepage.png", {
maxDiffPixels: 100,
mask: [page.locator(".dynamic-data")],
});
// actualizar baselines:
// npx playwright test --update-snapshotstoHaveScreenshot() compara píxel a píxel. mask oculta áreas dinámicas (fechas, avatars). --update-snapshots regenera baselines. Detecta regresiones CSS automáticamente.
Aria snapshots
await expect(page.getByRole("list")).toMatchAriaSnapshot(`
- listitem: Producto A
- listitem: Producto B
- listitem:
- text: Producto C
- button: Eliminar
`);toMatchAriaSnapshot() verifica el árbol de accesibilidad. Más resiliente que HTML. Ignora detalles de implementación. Ideal para verificar la estructura semántica de la página.
Vídeo
// config:
use: {
video: "on-first-retry",
// "on" | "off" | "retain-on-failure"
}
// manual:
const video = page.video();
await video.saveAs("test.webm");
await video.delete();video graba la ejecución del test. on-first-retry graba solo en el retry (ahorra espacio). Archivos en test-results/. Útil para debug visual de fallos intermitentes.
Accesibilidad (a11y)
import AxeBuilder from "@axe-core/playwright";
test("sin errores de accesibilidad", async ({ page }) => {
await page.goto("/");
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});@axe-core/playwright integra Axe con Playwright. analyze() verifica WCAG automáticamente. violations lista los problemas encontrados. Puedes filtrar por tags (wcag2a, wcag2aa).
Error handling en tests
test("graceful failure", async ({ page }) => {
await page.goto("/");
// capturar errores de consola:
const errors: string[] = [];
page.on("console", (msg) => {
if (msg.type() === "error") errors.push(msg.text());
});
page.on("pageerror", (err) => errors.push(err.message));
await page.getByRole("button").click();
expect(errors).toHaveLength(0);
});page.on("console") captura logs del navegador. page.on("pageerror") captura excepciones JS. Verifica cero errores tras las acciones. Detecta bugs silenciosos que no rompen la UI.
Diálogos (alert/confirm)
page.on("dialog", (dialog) => {
console.log(dialog.message());
dialog.accept(); // OK
// dialog.dismiss(); // Cancel
});
// prompt con un valor:
page.on("dialog", (d) => d.accept("respuesta"));page.on("dialog") registra un handler para alerts. accept() hace clic en OK. dismiss() hace clic en Cancel. Para prompts, accept(valor) lo rellena. Sin handler, los dialogs se descartan.
Component testing
// playwright-ct.config.ts
import { defineConfig } from "@playwright/experimental-ct-react";
// test de componente:
import { test, expect } from "@playwright/experimental-ct-react";
import { Button } from "./Button";
test("el botón renderiza", async ({ mount }) => {
const component = await mount(<Button label="Click" />);
await expect(component).toContainText("Click");
});experimental-ct-react prueba componentes aislados. mount() renderiza sin la app completa. Soporta React, Vue y Svelte. Más rápido que E2E para probar UI components.
Reporting custom
// config:
reporter: [
["html", { open: "never" }],
["json", { outputFile: "results.json" }],
["junit", { outputFile: "results.xml" }],
["list"],
],reporter acepta múltiples formatos simultáneos. html genera un reporte visual interactivo. json y junit para CI. list muestra el progreso en la terminal.
CLI e Configuração
Ejecutar tests
npx playwright test npx playwright test tests/login.spec.ts npx playwright test --project=chromium npx playwright test -g "hace login"
El comando base ejecuta todos los tests. Especifica un archivo para ejecutar solo uno. --project filtra por browser. -g filtra por nombre del test (grep). Combina filtros libremente.
Reportes
npx playwright show-report
npx playwright show-report --port=9000
// formatos en el config:
reporter: [["html"], ["list"], ["json", { outputFile: "r.json" }]]show-report abre el reporte HTML en el browser. Muestra tests, fallos, traces y screenshots. --port cambia el puerto. El reporte es autocontenido (puedes compartir la carpeta).
Filtrar y repetir
npx playwright test --grep "login|registro" npx playwright test --grep-invert "lento" npx playwright test --repeat-each=3 npx playwright test --max-failures=5
--grep filtra por regex en el nombre. --grep-invert excluye. --repeat-each repite cada test N veces (detecta flaky). --max-failures para tras N fallos.
Modo UI
npx playwright test --ui
--ui abre una interfaz gráfica interactiva. Muestra la timeline, DOM snapshots y red. Permite ejecutar tests individuales o filtrar. El watch mode reejecuta al guardar el archivo. Esencial para el desarrollo.
Ver trace
npx playwright show-trace trace.zip npx playwright show-trace test-results/trace.zip
show-trace abre el Trace Viewer. Muestra una timeline con screenshots, DOM, red y consola. Puedes navegar paso a paso. Esencial para depurar fallos en CI (donde no hay browser visible).
Codegen avanzado
npx playwright codegen --target=python npx playwright codegen --device="Pixel 5" npx playwright codegen --viewport-size=1920,1080 npx playwright codegen --save-storage=auth.json npx playwright codegen --load-storage=auth.json
--target genera en Python, Java, C#. --device emula mobile. --save-storage graba cookies al cerrar. --load-storage inicia autenticado. Óptimo para prototipar tests.
Modo debug
npx playwright test --debug npx playwright test login.spec.ts --debug
--debug abre el Playwright Inspector. Un browser visible con controles paso a paso. Muestra selectores sugeridos al hover. Permite ejecutar acciones una a una. Ideal para crear y depurar tests.
Workers y paralelismo
npx playwright test --workers=4
npx playwright test --workers=50%
// config:
export default defineConfig({
workers: process.env.CI ? 4 : undefined,
fullyParallel: true,
});workers define los procesos paralelos. fullyParallel: true paraleliza tests dentro de los archivos. Más workers = más rápido (hasta el límite de CPU). En CI usa 4, en local auto.
CI (GitHub Actions)
# .github/workflows/playwright.yml
- uses: actions/setup-node@v4
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/Instala los browsers con --with-deps en CI. Ejecuta tests headless (por defecto). Sube el reporte en fallo para debug. El init de Playwright genera el workflow automáticamente.
Headed y slowMo
npx playwright test --headed
npx playwright test --headed --slow-mo=500
// config:
use: {
headless: false,
launchOptions: { slowMo: 1000 },
}--headed muestra el browser durante la ejecución. --slow-mo retrasa las acciones (ms). Útil para observar lo que hace el test. En CI usa headless (por defecto) para velocidad.
Timeouts
export default defineConfig({
timeout: 30000, // por test
expect: { timeout: 5000 }, // por aserción
use: {
actionTimeout: 10000, // por acción
navigationTimeout: 15000,
},
});timeout es el límite total por test. expect.timeout para aserciones. actionTimeout para clics/fills. navigationTimeout para goto/waitForURL. Ajústalo según la app.
Extensiones y output
npx playwright test --output=test-results npx playwright test --config=staging.config.ts npx playwright test --list npx playwright test --last-failed
--output define la carpeta de artefactos. --config usa un config alternativo (staging). --list lista los tests sin ejecutarlos. --last-failed reejecuta solo los que fallaron. Muy útil en el día a día.
Rede e Mocking
Interceptar peticiones
await page.route("**/api/**", (route) => {
console.log(route.request().url());
route.continue();
});page.route() intercepta peticiones que coinciden con el pattern. route.continue() las deja pasar normalmente. Permite logging, modificación o bloqueo. El pattern usa glob con **.
Modificar respuesta
await page.route("**/api/config", async (route) => {
const response = await route.fetch();
const json = await response.json();
json.featureFlag = true;
route.fulfill({ json });
});route.fetch() hace la petición real y captura la respuesta. Modifica el JSON y devuélvelo con fulfill(). Ideal para activar feature flags o cambiar datos sin un mock completo.
Mock de error
await page.route("**/api/data", (route) => {
route.fulfill({ status: 500, body: "Error interno" });
});
await page.goto("/dashboard");
await expect(page.getByText("Error al cargar")).toBeVisible();Simula errores HTTP para probar el manejo. status: 500 simula un error de servidor. Verifica los mensajes de error en la UI. Prueba la resiliencia sin romper el backend.
Mock de respuesta
await page.route("**/api/users", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
json: [{ id: 1, name: "Ana" }],
});
});route.fulfill() devuelve una respuesta simulada. json serializa automáticamente. status define el código HTTP. Elimina la dependencia del backend en tests de UI.
Esperar respuesta
const [response] = await Promise.all([
page.waitForResponse("**/api/login"),
page.getByRole("button", { name: "Entrar" }).click(),
]);
expect(response.status()).toBe(200);
const body = await response.json();waitForResponse() espera una respuesta específica. Combínalo con Promise.all() y la acción que la dispara. Verifica el status y el body. Esencial para probar flows con API.
HAR replay
// grabar:
await context.routeFromHAR("har/grabacion.har", {
update: true,
});
// reproducir:
await context.routeFromHAR("har/grabacion.har", {
notFound: "fallback",
});routeFromHAR() reproduce respuestas grabadas en un HAR. update: true graba nuevas peticiones. notFound: "fallback" pasa al servidor si no existe. Mock realista sin mantenimiento.
Bloquear peticiones
await page.route("**/*.{png,jpg,jpeg,gif}", (route) => route.abort());
await page.route("**/analytics/**", (route) => route.abort());
await page.route("**/*.css", (route) => route.abort());route.abort() cancela la petición. Bloquear imágenes acelera los tests. Bloquear analytics evita ruido. Acepta patterns glob con extensiones. Útil para tests centrados en funcionalidad.
Esperar petición
const [request] = await Promise.all([
page.waitForRequest("**/api/checkout"),
buyButton.click(),
]);
expect(request.method()).toBe("POST");
const postData = request.postDataJSON();
expect(postData.total).toBe(49.99);waitForRequest() captura la petición enviada. method() verifica el verbo HTTP. postDataJSON() parsea el body. Valida que el frontend envía datos correctos.
API testing directo
test("API crea un usuario", async ({ request }) => {
const response = await request.post("/api/users", {
data: { name: "Ana", email: "ana@site.com" },
});
expect(response.ok()).toBeTruthy();
const user = await response.json();
expect(user.id).toBeDefined();
});El fixture request hace llamadas HTTP sin browser. data envía JSON en el body. Ideal para setup de tests o probar APIs puras. Más rápido que los tests con UI.
Modificar petición
await page.route("**/api/**", (route) => {
route.continue({
headers: {
...route.request().headers(),
"X-Custom": "test",
},
});
});route.continue() con opciones modifica la petición. Puedes cambiar headers, method, postData. Útil para inyectar tokens o simular condiciones. La petición va al servidor real.
Mock con delay
await page.route("**/api/slow", async (route) => {
await new Promise((r) => setTimeout(r, 3000));
route.fulfill({ json: { data: "ok" } });
});
// probar el loading state:
await expect(spinner).toBeVisible();
await expect(spinner).toBeHidden({ timeout: 5000 });Añade un delay artificial para probar loading states. setTimeout simula latencia. Verifica que los spinners aparecen y desaparecen. Prueba la UX bajo condiciones lentas.
Eliminar routes
const handler = (route) => route.abort();
await page.route("**/analytics/**", handler);
// más tarde, eliminarlo:
await page.unroute("**/analytics/**", handler);
// eliminar todos:
await page.unrouteAll();unroute() elimina un interceptor específico. unrouteAll() los elimina todos. Útil cuando un mock solo es necesario para parte del test. Evita interferencia entre tests.
Autenticação e Estado
Login vía UI
async function login(page: Page) {
await page.goto("/login");
await page.getByLabel("Email").fill("admin@site.com");
await page.getByLabel("Password").fill("password");
await page.getByRole("button", { name: "Entrar" }).click();
await page.waitForURL("/dashboard");
}Una función helper reutilizable para login. waitForURL() confirma el éxito. Puede usarse en beforeEach o en un fixture. Un enfoque simple pero lento si se repite muchas veces.
Múltiples usuarios
// auth/user.setup.ts → user.json
// auth/admin.setup.ts → admin.json
test.use({ storageState: "auth/admin.json" });
test("acceso admin", async ({ page }) => {
await page.goto("/admin");
await expect(page.getByText("Panel")).toBeVisible();
});Genera un storageState por tipo de usuario. Cada archivo tiene las cookies de un rol. Los tests admin usan admin.json, los tests user usan user.json. Limpio y escalable.
Permisos y roles
test.describe("Admin", () => {
test.use({ storageState: "auth/admin.json" });
test("accede a /admin", async ({ page }) => {
await page.goto("/admin");
await expect(page.getByRole("heading")).toHaveText("Administración");
});
});
test.describe("User", () => {
test.use({ storageState: "auth/user.json" });
test("no accede a /admin", async ({ page }) => {
await page.goto("/admin");
await expect(page).toHaveURL("/");
});
});Prueba permisos con diferentes estados de auth. El admin accede, el user es redirigido. test.use() por grupo define el storageState. Cobertura completa de RBAC.
Guardar estado (storageState)
// global-setup.ts: guardar la sesión
const browser = await chromium.launch();
const page = await browser.newPage();
await login(page);
await page.context().storageState({ path: "auth.json" });
await browser.close();storageState() guarda cookies y localStorage en JSON. Ejecútalo una vez en el global setup. El archivo auth.json es reutilizado por todos los tests. Evita logins repetidos.
Login vía API (rápido)
// setup: login sin UI
test("autenticar", async ({ request }) => {
const response = await request.post("/api/login", {
data: { email: "admin@site.com", password: "pass" },
});
const { token } = await response.json();
// guardar token en storageState
});Login vía request es mucho más rápido que vía UI. No abre browser. Ideal para generar tokens en el setup. Combínalo con storageState para inyectar cookies.
Tokens y headers
// inyectar un token en todas las peticiones:
await context.route("**/api/**", (route) => {
route.continue({
headers: {
...route.request().headers(),
Authorization: `Bearer ${token}`,
},
});
});Intercepta peticiones API para inyectar Authorization. Útil cuando el token no está en cookies. route.continue() con headers modificados. Una alternativa al storageState para APIs token-based.
Reutilizar autenticación
// playwright.config.ts:
export default defineConfig({
use: {
storageState: "auth.json",
},
});
// o por archivo:
test.use({ storageState: "auth.json" });storageState en el config carga cookies/localStorage. Todos los tests empiezan autenticados. Sin login en cada test — mucho más rápido. El archivo se genera en el global setup.
Logout y sesión inválida
test("logout redirecciona", async ({ page }) => {
await page.goto("/dashboard");
await page.getByRole("button", { name: "Salir" }).click();
await expect(page).toHaveURL("/login");
});
test("sesión expirada", async ({ page }) => {
await context.clearCookies();
await page.goto("/dashboard");
await expect(page).toHaveURL("/login");
});clearCookies() simula una sesión expirada. Verifica los redirects a login. Prueba que las áreas protegidas no son accesibles. Esencial para la seguridad de la aplicación.
2FA y verificación
test("login con 2FA", async ({ page }) => {
await page.goto("/login");
await page.getByLabel("Email").fill("user@site.com");
await page.getByLabel("Password").fill("pass");
await page.getByRole("button").click();
await page.getByLabel("Código").fill("123456");
await page.getByRole("button", { name: "Verificar" }).click();
await expect(page).toHaveURL("/dashboard");
});Prueba el flow completo de two-factor. Rellena el código TOTP (mock o real). Verifica el redirect tras la verificación. En CI, usa códigos fijos o una API para generar TOTP.
Setup project (dependencias)
export default defineConfig({
projects: [
{ name: "setup", testMatch: /.*\.setup\.ts/ },
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
storageState: "auth.json",
},
dependencies: ["setup"],
},
],
});dependencies garantiza que setup se ejecute antes. El project setup genera auth.json. Los otros projects usan el estado guardado. Patrón oficial de Playwright para auth.
OAuth y SSO
// mock del provider OAuth:
await page.route("**/oauth/authorize**", (route) => {
const url = new URL(route.request().url());
const redirect = url.searchParams.get("redirect_uri");
route.fulfill({
status: 302,
headers: { Location: `${redirect}?code=test-code` },
});
});Hacer mock del redirect OAuth evita el provider real. Intercepta /oauth/authorize y devuelve un redirect con code. El flow continúa como si fuera real. Prueba la integración sin dependencias externas.
Aislamiento de sesión
// cada test tiene un contexto limpio por defecto:
test("test A", async ({ page }) => {
// cookies vacías, sin storage
});
// compartir contexto entre tests (cuidado):
test.describe("secuencia", () => {
test.describe.configure({ mode: "serial" });
let context: BrowserContext;
test.beforeAll(async ({ browser }) => {
context = await browser.newContext();
});
});Por defecto cada test está aislado (context limpio). Para secuencias con estado compartido usa serial + un context manual. Prefiere el aislamiento — evita tests flaky y acoplamiento.