Playwright es una herramienta de automatizacion de pruebas de navegador de ultima generacion desarrollada por Microsoft. Soporta Chromium, Firefox y WebKit, permitiendo pruebas E2E (End-to-End) rapidas y confiables. En este articulo, aprenderemos desde los fundamentos de Playwright hasta patrones de prueba practicos a traves de codigo real.
Lo que aprenderas en este articulo
- Configuracion del entorno de Playwright
- Como escribir pruebas basicas
- Implementacion de Page Object Model
- Pruebas de flujo de autenticacion
- Mocks e intercepcion de API
- Integracion con CI/CD
Configuracion del entorno
Instalacion
# Para nuevo proyecto
npm init playwright@latest
# Agregar a proyecto existente
npm install -D @playwright/test
npx playwright install
Durante la inicializacion, se mostraran las siguientes opciones.
? Do you want to use TypeScript or JavaScript? › TypeScript
? Where to put your end-to-end tests? › tests
? Add a GitHub Actions workflow? › true
? Install Playwright browsers? › true
Estructura del proyecto
project/
├── playwright.config.ts # Archivo de configuracion
├── tests/
│ ├── example.spec.ts # Archivo de pruebas
│ └── fixtures/ # Fixtures personalizados
│ └── auth.ts
├── pages/ # Page Objects
│ ├── login.page.ts
│ └── dashboard.page.ts
├── test-results/ # Resultados de pruebas (auto-generado)
└── playwright-report/ # Reporte HTML (auto-generado)
Archivo de configuracion
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// Directorio de pruebas
testDir: './tests',
// Ejecucion paralela
fullyParallel: true,
// Reintentos en entorno CI
retries: process.env.CI ? 2 : 0,
// Numero de workers
workers: process.env.CI ? 1 : undefined,
// Configuracion de reporteros
reporter: [
['html', { open: 'never' }],
['list'],
],
// Configuracion comun
use: {
// URL base
baseURL: 'http://localhost:3000',
// Configuracion de capturas de pantalla
screenshot: 'only-on-failure',
// Configuracion de trazas
trace: 'on-first-retry',
// Grabacion de video
video: 'on-first-retry',
},
// Configuracion de navegadores
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Dispositivos moviles
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 13'] },
},
],
// Inicio del servidor de desarrollo
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Como escribir pruebas basicas
Primera prueba
// tests/example.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Pagina de inicio', () => {
test('El titulo se muestra correctamente', async ({ page }) => {
await page.goto('/');
// Verificar titulo
await expect(page).toHaveTitle(/My App/);
});
test('La navegacion funciona', async ({ page }) => {
await page.goto('/');
// Hacer clic en enlace
await page.getByRole('link', { name: 'About' }).click();
// Verificar URL
await expect(page).toHaveURL('/about');
// Verificar contenido
await expect(page.getByRole('heading', { level: 1 })).toHaveText('About Us');
});
});
Estrategias de localizadores
En Playwright, es importante usar localizadores (metodos de identificacion de elementos) robustos.
// Recomendado: Localizadores desde perspectiva del usuario
page.getByRole('button', { name: 'Enviar' }); // ARIA role
page.getByLabel('Correo electronico'); // Etiqueta
page.getByPlaceholder('example@email.com'); // Placeholder
page.getByText('Inicio de sesion exitoso'); // Texto
page.getByTestId('submit-button'); // data-testid
// No recomendado: Localizadores dependientes de implementacion
page.locator('#submit-btn'); // ID (puede cambiar)
page.locator('.btn-primary'); // Clase (se rompe con cambios de estilo)
page.locator('div > button:nth-child(2)'); // Dependiente de estructura
Aserciones
import { test, expect } from '@playwright/test';
test('Varias aserciones', async ({ page }) => {
await page.goto('/products');
// Visibilidad
await expect(page.getByTestId('loading')).toBeVisible();
await expect(page.getByTestId('loading')).not.toBeVisible();
// Contenido de texto
await expect(page.getByRole('heading')).toHaveText('Lista de productos');
await expect(page.getByRole('heading')).toContainText('productos');
// Atributos
await expect(page.getByRole('link')).toHaveAttribute('href', '/cart');
// Conteo
await expect(page.getByTestId('product-card')).toHaveCount(10);
// Habilitado/Deshabilitado
await expect(page.getByRole('button', { name: 'Comprar' })).toBeEnabled();
await expect(page.getByRole('button', { name: 'Comprar' })).toBeDisabled();
// Valores de formulario
await expect(page.getByLabel('Cantidad')).toHaveValue('1');
});
Pruebas de formularios
Entrada y envio
test.describe('Formulario de contacto', () => {
test('El envio del formulario es exitoso', async ({ page }) => {
await page.goto('/contact');
// Entrada del formulario
await page.getByLabel('Nombre').fill('Tanaka Taro');
await page.getByLabel('Correo electronico').fill('tanaka@example.com');
await page.getByLabel('Asunto').selectOption('inquiry');
await page.getByLabel('Mensaje').fill('Escriba el contenido de su consulta aqui.');
// Checkbox
await page.getByLabel('Acepto la politica de privacidad').check();
// Enviar
await page.getByRole('button', { name: 'Enviar' }).click();
// Verificar mensaje de exito
await expect(page.getByRole('alert')).toHaveText('Envio completado');
});
test('Se muestran errores de validacion', async ({ page }) => {
await page.goto('/contact');
// Enviar vacio
await page.getByRole('button', { name: 'Enviar' }).click();
// Verificar mensajes de error
await expect(page.getByText('El nombre es obligatorio')).toBeVisible();
await expect(page.getByText('El correo electronico es obligatorio')).toBeVisible();
});
});
Page Object Model
Para pruebas a gran escala, usa el patron Page Object Model para mejorar la mantenibilidad.
Creacion de Page Object
// pages/login.page.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Correo electronico');
this.passwordInput = page.getByLabel('Contrasena');
this.submitButton = page.getByRole('button', { name: 'Iniciar sesion' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
// pages/dashboard.page.ts
import { Page, Locator } from '@playwright/test';
export class DashboardPage {
readonly page: Page;
readonly welcomeMessage: Locator;
readonly logoutButton: Locator;
readonly userMenu: Locator;
constructor(page: Page) {
this.page = page;
this.welcomeMessage = page.getByTestId('welcome-message');
this.logoutButton = page.getByRole('button', { name: 'Cerrar sesion' });
this.userMenu = page.getByTestId('user-menu');
}
async logout() {
await this.userMenu.click();
await this.logoutButton.click();
}
}
Uso en pruebas
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
import { DashboardPage } from '../pages/dashboard.page';
test.describe('Funcionalidad de inicio de sesion', () => {
test('Puede iniciar sesion con credenciales correctas', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
// Verificar redireccion al dashboard
await expect(page).toHaveURL('/dashboard');
await expect(dashboardPage.welcomeMessage).toContainText('Bienvenido');
});
test('Muestra error con credenciales incorrectas', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'wrongpassword');
await expect(loginPage.errorMessage).toHaveText('Correo electronico o contrasena incorrectos');
await expect(page).toHaveURL('/login');
});
});
Gestion del estado de autenticacion
Guardar y reutilizar estado de autenticacion
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import path from 'path';
const authFile = path.join(__dirname, '../playwright/.auth/user.json');
setup('Autenticacion', async ({ page }) => {
// Proceso de login
await page.goto('/login');
await page.getByLabel('Correo electronico').fill('user@example.com');
await page.getByLabel('Contrasena').fill('password123');
await page.getByRole('button', { name: 'Iniciar sesion' }).click();
// Verificar login exitoso
await expect(page).toHaveURL('/dashboard');
// Guardar estado de autenticacion
await page.context().storageState({ path: authFile });
});
// playwright.config.ts
export default defineConfig({
projects: [
// Configuracion de autenticacion
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
// Pruebas autenticadas
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
Fixtures personalizados
// tests/fixtures/auth.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../../pages/login.page';
import { DashboardPage } from '../../pages/dashboard.page';
type AuthFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
authenticatedPage: DashboardPage;
};
export const test = base.extend<AuthFixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
dashboardPage: async ({ page }, use) => {
const dashboardPage = new DashboardPage(page);
await use(dashboardPage);
},
authenticatedPage: async ({ page }, use) => {
// Auto-login
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
const dashboardPage = new DashboardPage(page);
await use(dashboardPage);
},
});
export { expect } from '@playwright/test';
// tests/dashboard.spec.ts
import { test, expect } from './fixtures/auth';
test('Usuario autenticado puede acceder al dashboard', async ({ authenticatedPage }) => {
await expect(authenticatedPage.welcomeMessage).toBeVisible();
});
Mocks e intercepcion de API
Mock de respuestas de API
test.describe('Lista de productos', () => {
test('Obtiene y muestra datos desde API', async ({ page }) => {
// Mock de respuesta de API
await page.route('**/api/products', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Producto A', price: 1000 },
{ id: 2, name: 'Producto B', price: 2000 },
{ id: 3, name: 'Producto C', price: 3000 },
]),
});
});
await page.goto('/products');
// Verificar que se muestran los datos mockeados
await expect(page.getByTestId('product-card')).toHaveCount(3);
await expect(page.getByText('Producto A')).toBeVisible();
});
test('Muestra mensaje de error cuando falla la API', async ({ page }) => {
await page.route('**/api/products', async route => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' }),
});
});
await page.goto('/products');
await expect(page.getByText('Error al obtener los datos')).toBeVisible();
});
});
Intercepcion y verificacion de requests
test('Envia datos correctos a la API al enviar formulario', async ({ page }) => {
let capturedRequest: any = null;
// Interceptar request
await page.route('**/api/contact', async route => {
capturedRequest = route.request().postDataJSON();
await route.fulfill({ status: 200, body: JSON.stringify({ success: true }) });
});
await page.goto('/contact');
await page.getByLabel('Nombre').fill('Tanaka Taro');
await page.getByLabel('Correo electronico').fill('tanaka@example.com');
await page.getByRole('button', { name: 'Enviar' }).click();
// Verificar datos enviados
expect(capturedRequest).toEqual({
name: 'Tanaka Taro',
email: 'tanaka@example.com',
});
});
Pruebas de regresion visual
Comparacion de capturas de pantalla
test('Captura de pantalla de pagina de inicio', async ({ page }) => {
await page.goto('/');
// Captura de pantalla de pagina completa
await expect(page).toHaveScreenshot('homepage.png');
});
test('Captura de pantalla de componente', async ({ page }) => {
await page.goto('/components');
// Solo elemento especifico
const card = page.getByTestId('feature-card');
await expect(card).toHaveScreenshot('feature-card.png');
});
test('Diseno responsive', async ({ page }) => {
await page.goto('/');
// Desktop
await page.setViewportSize({ width: 1280, height: 720 });
await expect(page).toHaveScreenshot('homepage-desktop.png');
// Tablet
await page.setViewportSize({ width: 768, height: 1024 });
await expect(page).toHaveScreenshot('homepage-tablet.png');
// Movil
await page.setViewportSize({ width: 375, height: 667 });
await expect(page).toHaveScreenshot('homepage-mobile.png');
});
Integracion CI/CD
Configuracion de GitHub Actions
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results
path: test-results/
retention-days: 30
Tecnicas de depuracion
Depuracion en modo UI
# Ejecutar en modo UI
npx playwright test --ui
# Depurar prueba especifica
npx playwright test --debug tests/login.spec.ts
Visor de trazas
// playwright.config.ts
use: {
trace: 'on-first-retry', // Traza solo en fallos
// trace: 'on', // Siempre trazar
}
# Abrir archivo de traza
npx playwright show-trace test-results/example-spec-ts/trace.zip
Mostrar console.log
test('Salida de depuracion', async ({ page }) => {
// Capturar console.log del navegador
page.on('console', msg => console.log('Browser log:', msg.text()));
await page.goto('/');
// Pause de Playwright (depurador)
await page.pause();
});
Comandos de ejecucion de pruebas
# Ejecutar todas las pruebas
npx playwright test
# Archivo especifico
npx playwright test tests/login.spec.ts
# Navegador especifico
npx playwright test --project=chromium
# Modo con interfaz (mostrar navegador)
npx playwright test --headed
# Control de paralelismo
npx playwright test --workers=4
# Re-ejecutar solo pruebas fallidas
npx playwright test --last-failed
# Filtrar por tag
npx playwright test --grep @smoke
# Abrir reporte HTML
npx playwright show-report
Resumen
Resumimos los puntos clave de las pruebas E2E con Playwright.
Mejores practicas
- Localizadores: Priorizar localizadores desde perspectiva del usuario (getByRole, getByLabel)
- Page Object: Adoptar POM para mantenibilidad en pruebas a gran escala
- Autenticacion: Reutilizar estado de autenticacion con storageState
- Mock de API: Pruebas independientes del backend con route()
- Integracion CI: Pruebas automaticas con GitHub Actions
Proximos pasos
- Documentacion oficial de Playwright
- Uso combinado con Testing Library
- Uso de Component Testing
Las pruebas E2E mejoran significativamente la calidad del desarrollo. Aprovecha las poderosas caracteristicas de Playwright para construir pruebas confiables.