Guia Practica de Playwright E2E Testing - Automatizacion Moderna de Pruebas de Navegador

Intermedio | 60 min de lectura | 2025.12.02

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

  1. Configuracion del entorno de Playwright
  2. Como escribir pruebas basicas
  3. Implementacion de Page Object Model
  4. Pruebas de flujo de autenticacion
  5. Mocks e intercepcion de API
  6. 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

  1. Localizadores: Priorizar localizadores desde perspectiva del usuario (getByRole, getByLabel)
  2. Page Object: Adoptar POM para mantenibilidad en pruebas a gran escala
  3. Autenticacion: Reutilizar estado de autenticacion con storageState
  4. Mock de API: Pruebas independientes del backend con route()
  5. Integracion CI: Pruebas automaticas con GitHub Actions

Proximos pasos

Las pruebas E2E mejoran significativamente la calidad del desarrollo. Aprovecha las poderosas caracteristicas de Playwright para construir pruebas confiables.

Enlaces de referencia

← Volver a la lista