Playwright Tutorial: guía completa para automatización web moderna
Playwright se ha convertido en una de las herramientas más fuertes para automatización web moderna porque no sólo permite interactuar con el navegador, sino que además incorpora un test runner, assertions, aislamiento entre pruebas, paralelización y tooling de depuración en un mismo stack. La documentación oficial lo presenta como un framework de testing end-to-end para aplicaciones web modernas, con soporte para Chromium, Firefox y WebKit, ejecución local o en CI, modo headless o headed, e incluso emulación móvil nativa.
En otras palabras: con Playwright no montas sólo “scripts de navegador”, sino una base real para pruebas E2E mantenibles. Y para la mayoría de proyectos de testing end-to-end, la propia documentación recomienda usar @playwright/test en lugar de la librería playwright a secas, porque Playwright Test ya trae la experiencia completa de ejecución, reportes y configuración. Si vienes de otros ecosistemas, notarás que es una alternativa muy sólida a lo que ofrecen herramientas como Cypress.
En esta guía vas a aprender cómo instalar Playwright tutorial paso a paso, cómo escribir pruebas robustas, qué tipo de locators usar, cómo reutilizar autenticación, cómo combinar UI y API testing, cómo depurar fallos con Trace Viewer y cómo dejar una suite lista para CI. Todo el enfoque está orientado a calidad técnica y a reducir el clásico problema de tests frágiles o flaky. Además, exploraremos cómo la QA con IA está transformando la forma en que escribimos y mantenemos estos scripts.
Por qué Playwright destaca frente a otras opciones
Uno de los puntos más importantes de Playwright es que los playwright locators son el centro del auto-waiting y de la capacidad de reintento. Eso significa que la herramienta espera automáticamente a que el elemento esté en condiciones de ser interactuado, en lugar de obligarte a llenar tu suite de pausas manuales y sleeps innecesarios. Para casos donde los selectores de texto o rol no son suficientes, siempre puedes apoyarte en XPath para localizar elementos complejos basándote en la estructura del DOM.
Ese diseño reduce una gran parte de la fragilidad típica en automatización web. A esto se suman las web-first assertions, que esperan y reintentan hasta que el estado esperado se cumple, en lugar de hacer verificaciones prematuras. La documentación oficial de Playwright insiste precisamente en esta filosofía: acciones sobre la UI, más expectativas declarativas y menos sincronización manual.
Cuándo conviene usar Playwright
Playwright encaja especialmente bien cuando necesitas una suite E2E moderna con buen soporte multi-browser, debugging visual, integración con CI y una experiencia sólida en playwright typescript o JavaScript. También es muy útil cuando quieres combinar pruebas de interfaz con preparación de datos por API o mocking de red sin salir del mismo ecosistema.
Si lo que buscas es un stack moderno, rápido de adoptar y con menos fricción que un enfoque puramente WebDriver, Playwright suele ser una elección excelente. Esa valoración es una inferencia práctica basada en que Playwright empaqueta runner, assertions, aislamiento, tooling y soporte de navegación en una sola solución.
Instalación de Playwright paso a paso
La forma más rápida de empezar con Playwright es usar el inicializador oficial:
npm init playwright@latest
Ese flujo te crea la estructura básica del proyecto, un test de ejemplo y la configuración necesaria para empezar a ejecutar pruebas.
npx playwright test
npx playwright show-report
Estructura mínima recomendada del proyecto
Una estructura sencilla y mantenible puede verse así:
playwright-project/
├─ tests/
│ ├─ auth.setup.ts
│ ├─ home.spec.ts
│ ├─ tasks.spec.ts
├─ page-objects/
│ ├─ LoginPage.ts
│ ├─ DashboardPage.ts
├─ playwright.config.ts
├─ package.json
└─ tsconfig.json
Configuración inicial recomendada
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
reporter: [['html'], ['list']],
use: {
baseURL: 'https://tu-app.com',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
headless: true,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
Tu primer test en Playwright
import { test, expect } from '@playwright/test';
test('homepage shows main heading', async ({ page }) => {
await page.goto('https://example.com');
await expect(page.getByRole('heading')).toBeVisible();
});
Locators: la forma correcta de seleccionar elementos
Mal enfoque
await page.click('.btn-primary');
await page.fill('#email-input', 'user@example.com');
await page.click('div.card:nth-child(3) button');
Mejor enfoque
await page.getByLabel('Email').fill('user@example.com');
await page.getByRole('button', { name: 'Iniciar sesión' }).click();
await page.getByTestId('save-task').click();
Assertions web-first: menos flakiness, más estabilidad
En Playwright, lo correcto es apoyarte en assertions que esperen hasta que el estado deseado se materialice:
await expect(page.getByRole('status')).toHaveText(/guardado/i);
await expect(page).toHaveURL(/dashboard/);
await expect(page.getByTestId('task-item')).toHaveCount(3);
Por qué no deberías usar waitForTimeout() salvo casos excepcionales
Una prueba mejor escrita sería así:
import { test, expect } from '@playwright/test';
test('user creates a task', async ({ page }) => {
await page.goto('/tasks');
await page.getByRole('button', { name: 'Nueva tarea' }).click();
await page.getByLabel('Título').fill('Escribir mejor artículo sobre Playwright');
await page.getByRole('button', { name: 'Guardar' }).click();
await expect(
page.getByRole('listitem').filter({ hasText: 'Escribir mejor artículo sobre Playwright' })
).toBeVisible();
await expect(page.getByRole('status')).toHaveText(/guardado/i);
});
Autenticación reutilizable con storageState
Hacer login al inicio de cada test suele ser una mala idea. La recomendación oficial actual es crear un proyecto de setup para playwright auth.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
use: {
baseURL: 'https://tu-app.com',
trace: 'on-first-retry',
},
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
Test de autenticación
import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill(process.env.E2E_PASSWORD || '');
await page.getByRole('button', { name: 'Iniciar sesión' }).click();
await expect(page).toHaveURL(/dashboard/);
await page.context().storageState({ path: authFile });
});
Fixtures y reutilización inteligente
import { test as base, expect } from '@playwright/test';
type AppFixtures = {
openDashboard: () => Promise;
};
export const test = base.extend({
openDashboard: async ({ page }, use) => {
await use(async () => {
await page.goto('/dashboard');
await expect(
page.getByRole('heading', { name: /dashboard/i })
).toBeVisible();
});
},
});
export { expect };
Y luego en tu spec:
import { test, expect } from './fixtures/app';
test('dashboard displays user menu', async ({ page, openDashboard }) => {
await openDashboard();
await expect(page.getByRole('button', { name: /perfil/i })).toBeVisible();
});
Cuándo usar Page Object Model
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly email: Locator;
readonly password: Locator;
readonly submit: Locator;
constructor(page: Page) {
this.page = page;
this.email = page.getByLabel('Email');
this.password = page.getByLabel('Password');
this.submit = page.getByRole('button', { name: 'Iniciar sesión' });
}
async login(email: string, password: string) {
await this.email.fill(email);
await this.password.fill(password);
await this.submit.click();
await expect(this.page).toHaveURL(/dashboard/);
}
}
API testing dentro del mismo stack
import { test, expect } from '@playwright/test';
test('creates item by API and validates it in UI', async ({ page }) => {
const response = await page.request.post('/api/tasks', {
data: { title: 'Task created by API' },
});
await expect(response).toBeOK();
await page.goto('/tasks');
await expect(page.getByText('Task created by API')).toBeVisible();
});
Mocking de red para reducir dependencia externa
import { test, expect } from '@playwright/test';
test('shows mocked user profile', async ({ page }) => {
await page.route('**/api/profile', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
name: 'Néstor',
role: 'Admin',
}),
});
});
await page.goto('/profile');
await expect(page.getByText('Néstor')).toBeVisible();
await expect(page.getByText('Admin')).toBeVisible();
});
Debugging real: UI Mode, reports y Trace Viewer
npx playwright test --ui
npx playwright show-report
use: {
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
}
CI con GitHub Actions
Para playwright ci, GitHub Actions es excelente.
name: Playwright Tests
on:
push:
branches: [main, master]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
Accesibilidad en Playwright
import { test, expect } from '@playwright/test';
test('login form is accessible at basic interaction level', async ({ page }) => {
await page.goto('/login');
await expect(page.getByLabel('Email')).toBeVisible();
await expect(page.getByLabel('Password')).toBeVisible();
await expect(page.getByRole('button', { name: 'Iniciar sesión' })).toBeEnabled();
});
Playwright vs Selenium vs Cypress
Playwright vs Selenium: Selenium sigue siendo una referencia histórica en automatización. WebDriver conduce el navegador de forma nativa. Playwright suele destacar cuando buscas una combinación de runner integrado + multi-browser + tooling potente en un mismo stack.
Playwright vs Cypress: Cypress se enfoca en testing de aplicaciones que corren en navegador y ofrece su propia app. Playwright usa contextos de navegador aislados y no inyecta código dentro de la misma ventana de la app.
Buenas prácticas para una suite Playwright mantenible
- Usa @playwright/test para E2E
- Prioriza getByRole, getByLabel, getByText y getByTestId
- Evita waitForTimeout() salvo casos excepcionales
- Reutiliza autenticación con storageState
- Usa fixtures y, cuando tenga sentido, Page Object Model
- Activa trace, screenshots y vídeo sólo cuando aporten valor
- Combina UI testing con API testing y mocking
Conclusión
Playwright no destaca sólo porque automatiza navegadores. Destaca porque ofrece una forma moderna de construir una suite E2E completa: runner integrado, locators robustos, assertions reintentables, aislamiento por contexto, autenticación reutilizable, mocking, API testing, debugging visual, reportes y ejecución en CI. Todo eso está alineado con la filosofía oficial del framework y explica por qué se ha convertido en una de las opciones más sólidas para testing web moderno.