Skills Development Playwright E2E Testing Patterns and Best Practices

Playwright E2E Testing Patterns and Best Practices

v20260704
e2e-testing
A comprehensive guide to building robust, reliable, and maintainable End-to-End (E2E) test suites using Playwright. This includes implementing the Page Object Model (POM), configuring advanced Playwright settings for CI/CD, managing artifacts (screenshots, traces), and strategies for handling flaky tests to ensure high test stability.
Get Skill
374 downloads
Overview

Patrones de Pruebas E2E

Patrones completos de Playwright para construir suites de pruebas E2E estables, rápidas y mantenibles.

Organización de Archivos de Prueba

tests/
├── e2e/
│   ├── auth/
│   │   ├── login.spec.ts
│   │   ├── logout.spec.ts
│   │   └── register.spec.ts
│   ├── features/
│   │   ├── browse.spec.ts
│   │   ├── search.spec.ts
│   │   └── create.spec.ts
│   └── api/
│       └── endpoints.spec.ts
├── fixtures/
│   ├── auth.ts
│   └── data.ts
└── playwright.config.ts

Page Object Model (POM)

import { Page, Locator } from '@playwright/test'

export class ItemsPage {
  readonly page: Page
  readonly searchInput: Locator
  readonly itemCards: Locator
  readonly createButton: Locator

  constructor(page: Page) {
    this.page = page
    this.searchInput = page.locator('[data-testid="search-input"]')
    this.itemCards = page.locator('[data-testid="item-card"]')
    this.createButton = page.locator('[data-testid="create-btn"]')
  }

  async goto() {
    await this.page.goto('/items')
    await this.page.waitForLoadState('networkidle')
  }

  async search(query: string) {
    await this.searchInput.fill(query)
    await this.page.waitForResponse(resp => resp.url().includes('/api/search'))
    await this.page.waitForLoadState('networkidle')
  }

  async getItemCount() {
    return await this.itemCards.count()
  }
}

Estructura de Pruebas

import { test, expect } from '@playwright/test'
import { ItemsPage } from '../../pages/ItemsPage'

test.describe('Item Search', () => {
  let itemsPage: ItemsPage

  test.beforeEach(async ({ page }) => {
    itemsPage = new ItemsPage(page)
    await itemsPage.goto()
  })

  test('should search by keyword', async ({ page }) => {
    await itemsPage.search('test')

    const count = await itemsPage.getItemCount()
    expect(count).toBeGreaterThan(0)

    await expect(itemsPage.itemCards.first()).toContainText(/test/i)
    await page.screenshot({ path: 'artifacts/search-results.png' })
  })

  test('should handle no results', async ({ page }) => {
    await itemsPage.search('xyznonexistent123')

    await expect(page.locator('[data-testid="no-results"]')).toBeVisible()
    expect(await itemsPage.getItemCount()).toBe(0)
  })
})

Configuración de Playwright

import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html', { outputFolder: 'playwright-report' }],
    ['junit', { outputFile: 'playwright-results.xml' }],
    ['json', { outputFile: 'playwright-results.json' }]
  ],
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    actionTimeout: 10000,
    navigationTimeout: 30000,
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
    { name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120000,
  },
})

Patrones de Pruebas Inestables (Flaky Tests)

Cuarentena

test('flaky: complex search', async ({ page }) => {
  test.fixme(true, 'Flaky - Issue #123')
  // código de prueba...
})

test('conditional skip', async ({ page }) => {
  test.skip(process.env.CI, 'Flaky in CI - Issue #123')
  // código de prueba...
})

Identificar Inestabilidad

npx playwright test tests/search.spec.ts --repeat-each=10
npx playwright test tests/search.spec.ts --retries=3

Causas Comunes y Soluciones

Condiciones de carrera:

// Mal: asume que el elemento está listo
await page.click('[data-testid="button"]')

// Bien: locator con auto-espera
await page.locator('[data-testid="button"]').click()

Timing de red:

// Mal: timeout arbitrario
await page.waitForTimeout(5000)

// Bien: esperar condición específica
await page.waitForResponse(resp => resp.url().includes('/api/data'))

Timing de animación:

// Mal: hacer clic durante la animación
await page.click('[data-testid="menu-item"]')

// Bien: esperar estabilidad
await page.locator('[data-testid="menu-item"]').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')
await page.locator('[data-testid="menu-item"]').click()

Gestión de Artefactos

Capturas de Pantalla

await page.screenshot({ path: 'artifacts/after-login.png' })
await page.screenshot({ path: 'artifacts/full-page.png', fullPage: true })
await page.locator('[data-testid="chart"]').screenshot({ path: 'artifacts/chart.png' })

Trazas

await browser.startTracing(page, {
  path: 'artifacts/trace.json',
  screenshots: true,
  snapshots: true,
})
// ... acciones de prueba ...
await browser.stopTracing()

Video

// En playwright.config.ts
use: {
  video: 'retain-on-failure',
  videosPath: 'artifacts/videos/'
}

Integración CI/CD

# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]

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
        env:
          BASE_URL: ${{ vars.STAGING_URL }}
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

Plantilla de Reporte de Pruebas

# Reporte de Pruebas E2E

**Fecha:** YYYY-MM-DD HH:MM
**Duración:** Xm Ys
**Estado:** PASANDO / FALLANDO

## Resumen
- Total: X | Pasaron: Y (Z%) | Fallaron: A | Inestables: B | Omitidas: C

## Pruebas Fallidas

### nombre-de-prueba
**Archivo:** `tests/e2e/feature.spec.ts:45`
**Error:** Expected element to be visible
**Captura:** artifacts/failed.png
**Corrección Recomendada:** [descripción]

## Artefactos
- Reporte HTML: playwright-report/index.html
- Capturas: artifacts/*.png
- Videos: artifacts/videos/*.webm
- Trazas: artifacts/*.zip

Pruebas de Wallet / Web3

test('wallet connection', async ({ page, context }) => {
  // Mock del proveedor de wallet
  await context.addInitScript(() => {
    window.ethereum = {
      isMetaMask: true,
      request: async ({ method }) => {
        if (method === 'eth_requestAccounts')
          return ['0x1234567890123456789012345678901234567890']
        if (method === 'eth_chainId') return '0x1'
      }
    }
  })

  await page.goto('/')
  await page.locator('[data-testid="connect-wallet"]').click()
  await expect(page.locator('[data-testid="wallet-address"]')).toContainText('0x1234')
})

Pruebas de Flujos Financieros / Críticos

test('trade execution', async ({ page }) => {
  // Omitir en producción — dinero real
  test.skip(process.env.NODE_ENV === 'production', 'Skip on production')

  await page.goto('/markets/test-market')
  await page.locator('[data-testid="position-yes"]').click()
  await page.locator('[data-testid="trade-amount"]').fill('1.0')

  // Verificar preview
  const preview = page.locator('[data-testid="trade-preview"]')
  await expect(preview).toContainText('1.0')

  // Confirmar y esperar blockchain
  await page.locator('[data-testid="confirm-trade"]').click()
  await page.waitForResponse(
    resp => resp.url().includes('/api/trade') && resp.status() === 200,
    { timeout: 30000 }
  )

  await expect(page.locator('[data-testid="trade-success"]')).toBeVisible()
})
Info
Category Development
Name e2e-testing
Version v20260704
Size 8.12KB
Updated At 2026-07-09
Language