Pruebas E2E con Claude Code: guía Playwright/Cypress y CI
Diseña pruebas E2E Playwright con Claude Code: CI, errores reales, ejemplos ejecutables y CTA.
Define primero qué debe proteger la prueba E2E
Que Claude Code genere una prueba de inicio de sesión y todo salga verde no significa que el producto esté protegido. Una prueba E2E valida un flujo de usuario en un navegador real, por eso es más lenta y más cara de depurar que una prueba unitaria. Si conviertes cada detalle visual en E2E, el CI se vuelve ruidoso y el equipo deja de creer en los fallos.
Elige los flujos que cuestan dinero cuando se rompen: inicio de sesión, compra, formulario de consulta, acciones peligrosas de administración y CTA que llevan de un artículo a un producto o a una sesión de formación. En ClaudeCodeLab, Masa vio un caso claro: la página se renderizaba bien, pero el enlace final hacia la formación estaba roto. Ese tipo de fallo no siempre aparece en una captura, pero sí rompe la ruta de monetización.
Esta guía se basa en la documentación actual de instalación de Playwright, locators, CI y Trace Viewer. Para completar la estrategia, revisa también automatización de pruebas de API y la guía de estrategias de testing.
flowchart LR
A["Pedir a Claude Code"] --> B["Elegir tres flujos críticos"]
B --> C["Implementar pruebas Playwright"]
C --> D["Depurar con trace e informe"]
D --> E["Bloquear CI si falla"]
C --> F["Verificar CTA e ingresos"]
Un prompt más útil para Claude Code
Incluye alcance, criterios de éxito, criterios de fallo, archivos permitidos y comandos de verificación. Sin esa información, Claude Code suele escribir pruebas frágiles basadas en clases CSS. Playwright recomienda locators como getByRole, getByLabel, getByText y getByTestId, porque funcionan bien con la espera automática y las aserciones reintentables.
Objetivo: pruebas E2E para inicio de sesión, compra y newsletter.
Herramienta: Playwright Test. Añade una comparación breve con Cypress.
Reglas:
- Priorizar role, label, text o testid antes que clases CSS.
- Cubrir errores, no solo el camino feliz.
- Usar workers: 1 en CI y trace: on-first-retry.
- Verificar que el href del CTA de monetización existe.
Archivos permitidos:
- tests/e2e/**/*.spec.ts
- playwright.config.ts
Verificación:
- npx playwright test
- npx playwright test --headed para depuración local
También conviene decir qué queda fuera. Pagos reales, envío real de correos, clics en anuncios o escrituras en CRM no deberían ejecutarse en cada E2E. Usa modo de prueba, mocks o pruebas de contrato de API. Para esos bordes, combina esta guía con webhooks y analítica.
Comandos de instalación
En un proyecto nuevo, empieza con estos comandos. La documentación oficial explica que cada versión de Playwright usa binarios concretos de navegador, así que después de actualizar Playwright también debes reinstalar navegadores. En CI Linux, usa --with-deps para instalar dependencias del sistema.
npm init playwright@latest
npx playwright install
npx playwright test
npx playwright test --headed
npx playwright test --ui
npx playwright show-report
--headed abre un navegador visible para investigar fallos locales. --ui abre el modo interactivo. En CI normalmente se ejecuta headless. Si algo pasa en local y falla en CI, revisa animaciones, tamaño de pantalla, fuentes, zona horaria, velocidad de CPU o esperas de API externas.
Ejemplo Playwright ejecutable
El siguiente spec es autónomo. Crea una página pequeña con page.setContent, por lo que no necesitas levantar una aplicación. Copia el archivo en tests/e2e/claude-code-e2e.spec.ts después de instalar Playwright. En una aplicación real, reemplaza renderDemoApp(page) por page.goto('/login') y ajusta etiquetas o data-testid.
// tests/e2e/claude-code-e2e.spec.ts
import { test, expect, type Page } from '@playwright/test';
async function renderDemoApp(page: Page) {
await page.setContent(`
<!doctype html>
<main>
<form id="login-form" aria-label="Login form">
<label>Email <input id="email" aria-label="Email" /></label>
<label>Password <input id="password" aria-label="Password" type="password" /></label>
<button type="submit">Log in</button>
<p id="login-error" role="alert" hidden></p>
</form>
<section id="dashboard" hidden>
<h1>Dashboard</h1>
<a data-testid="training-cta" href="/training/">Book Claude Code training</a>
<button id="add-plan">Add Pro plan to cart</button>
<span data-testid="cart-count">0</span>
<button id="buy">Complete purchase</button>
<p data-testid="order-status" role="status"></p>
<label>Newsletter email <input id="newsletter-email" aria-label="Newsletter email" /></label>
<button id="newsletter-button" type="button">Join newsletter</button>
<p id="newsletter-error" role="alert" hidden></p>
<p data-testid="newsletter-status" role="status"></p>
</section>
</main>
<script>
const state = { cart: 0 };
document.querySelector('#login-form').addEventListener('submit', (event) => {
event.preventDefault();
const email = document.querySelector('#email').value;
const password = document.querySelector('#password').value;
const error = document.querySelector('#login-error');
if (email === 'masa@example.com' && password === 'password123') {
document.querySelector('#login-form').hidden = true;
document.querySelector('#dashboard').hidden = false;
error.hidden = true;
return;
}
error.textContent = 'Authentication failed';
error.hidden = false;
});
document.querySelector('#add-plan').addEventListener('click', () => {
state.cart += 1;
document.querySelector('[data-testid="cart-count"]').textContent = String(state.cart);
});
document.querySelector('#buy').addEventListener('click', () => {
const status = document.querySelector('[data-testid="order-status"]');
status.textContent = state.cart === 0 ? 'Cart is empty' : 'Order ORD-1001 completed';
});
document.querySelector('#newsletter-button').addEventListener('click', () => {
const email = document.querySelector('#newsletter-email').value;
const error = document.querySelector('#newsletter-error');
const status = document.querySelector('[data-testid="newsletter-status"]');
if (!email.includes('@')) {
error.textContent = 'Enter a valid email';
error.hidden = false;
status.textContent = '';
return;
}
error.hidden = true;
status.textContent = 'Thanks, we will send the checklist.';
});
</script>
`);
}
async function loginAsDemoUser(page: Page) {
await page.getByLabel('Email').fill('masa@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Log in' }).click();
}
test.describe('Claude Code E2E starter', () => {
test('use case 1: login shows dashboard and keeps the training CTA', async ({ page }) => {
await renderDemoApp(page);
await loginAsDemoUser(page);
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
await expect(page.getByTestId('training-cta')).toHaveAttribute('href', '/training/');
});
test('use case 2: checkout flow creates an order number', async ({ page }) => {
await renderDemoApp(page);
await loginAsDemoUser(page);
await page.getByRole('button', { name: 'Add Pro plan to cart' }).click();
await expect(page.getByTestId('cart-count')).toHaveText('1');
await page.getByRole('button', { name: 'Complete purchase' }).click();
await expect(page.getByTestId('order-status')).toContainText('ORD-1001');
});
test('use case 3: newsletter validation blocks invalid leads', async ({ page }) => {
await renderDemoApp(page);
await loginAsDemoUser(page);
await page.getByRole('button', { name: 'Join newsletter' }).click();
await expect(page.getByRole('alert')).toContainText('Enter a valid email');
await page.getByLabel('Newsletter email').fill('reader@example.com');
await page.getByRole('button', { name: 'Join newsletter' }).click();
await expect(page.getByTestId('newsletter-status')).toContainText('Thanks');
});
});
Este archivo cubre tres casos concretos: inicio de sesión, compra y captación de leads. Además verifica el href del CTA de formación, un detalle que muchas suites superficiales no revisan.
Configuración práctica de Playwright
La configuración debe dejar evidencia cuando algo falla. La guía de CI de Playwright recomienda workers: 1 en CI para priorizar estabilidad y reproducibilidad.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
use: {
baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
trace: 'on-first-retry' permite ver qué estaba visible, qué clic falló y qué aserción agotó el tiempo. Con ese material, Claude Code puede investigar mucho mejor que con un error genérico de CI.
GitHub Actions para CI
En CI, muchos fallos vienen de navegadores o dependencias Linux, no de la prueba. Empieza con una configuración simple y añade caché o particionado solo cuando haga falta.
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run E2E tests
run: npx playwright test
- uses: actions/upload-artifact@v5
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Si pruebas una URL de vista previa, pasa BASE_URL al flujo. Si la aplicación debe arrancar dentro de CI, añade webServer en playwright.config.ts.
Casos de uso que sí merecen E2E
| Caso | Por qué probarlo con E2E | Expectativas clave |
|---|---|---|
| Inicio de sesión a panel | Une autenticación, redirección y permisos | Título, estado de usuario, salida, error |
| Compra o formulario de consulta | Afecta ingresos o leads | Número de orden, confirmación, no duplicar envío |
| Artículo hacia PDF o formación | Protege monetización más allá de anuncios | href del CTA, evento, visibilidad móvil |
| Acción peligrosa de admin | Evita borrados y escaladas de permiso | Modal, log de auditoría, control por rol |
| Páginas localizadas | La traducción rompe enlaces con frecuencia | URL con locale, texto natural, enlaces externos |
Entrega esta tabla a Claude Code antes de pedir archivos. Así evita crear pruebas que solo pulsan botones sin demostrar nada importante.
Fallos frecuentes
Evita waitForTimeout(3000). Las esperas fijas pasan en una máquina y fallan en CI. Espera el estado real: elemento visible, URL cambiada o mensaje de estado.
Evita selectores como .btn-primary:nth-child(2). Un cambio de diseño los rompe. Prioriza role, label, text y data-testid estable.
No compartas estado entre pruebas. El carrito, Cookie o registro de base de datos creado por una prueba no debe ser requisito para la siguiente.
No ignores la diferencia entre headed y headless. Fuentes, GPU, scroll, portapapeles, subida de archivos y viewport pueden cambiar en CI. Usa headless en CI y --headed o --ui para investigar.
No llames pagos, correos, anuncios o CRM reales en cada ejecución. Usa modo de prueba, mocks o pruebas de contrato de API.
Playwright o Cypress
| Aspecto | Playwright | Cypress |
|---|---|---|
| Navegadores | Chromium, Firefox, WebKit | Familia Chrome, Firefox, Edge |
| Paralelización | Incluida en Playwright Test | Suele diseñarse con Dashboard |
| Varias pestañas y contextos | Soporte fuerte | Más limitado |
| Depuración | Trace Viewer, UI mode, HTML report | GUI interactiva muy amigable |
| Mejor ajuste | CI, varios navegadores, auth aislada | Equipos frontend con Cypress existente |
Cypress sigue siendo una buena opción. Si ya tienes cobertura, no migres por moda. Añade Playwright a un flujo crítico y compara flakiness, calidad de trace y tiempo de CI. Confirma siempre con la documentación oficial de Playwright y Cypress.
CTA de monetización y resultado
E2E también protege ingresos. Un enlace de consulta, una tarjeta de producto, un formulario gratuito o un CTA de formación roto puede costar más que un bug visual. Para empezar solo, usa la chuleta gratuita de Claude Code. Para prompts reutilizables, revisa productos y plantillas. Para equipos que necesitan CI, CLAUDE.md, reglas de revisión y responsabilidad de E2E, usa formación y consultoría de Claude Code.
Probé el ejemplo extrayendo el spec y la configuración como fragmentos TypeScript para validación de sintaxis. El hábito más útil fue nombrar la expectativa de negocio en la prueba: mantener el CTA de formación. En un proyecto real, empieza con tres pruebas: login, compra o lead, y CTA del artículo. Amplía después de estabilizarlas en CI.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Workflow de Obsidian a CLAUDE.md con Claude Code
Convierte notas de trabajo de Obsidian en notas operativas de CLAUDE.md para no repetir contexto.
Claude Code Revenue CTA Routing: de artículos a PDF, Gumroad y consulta
Un flujo con Claude Code para dirigir lectores a PDF gratis, Gumroad o consulta según intención.
Reglas de handoff para equipos con Claude Code: evidencia, permisos, rollback e ingresos
Formato práctico para entregar trabajo de Claude Code con pruebas, permisos, rollback, PDF gratis, Gumroad y consulta.