Pruebas E2E con Claude Code y Playwright en producción
Diseña E2E con Claude Code y Playwright: móvil, auth state, Trace Viewer, selectores y retries en CI.
Pedir a Claude Code “agrega pruebas Playwright” no basta para proteger un sitio en producción. Puede generar tests que pasan una vez, pero dependen de clases CSS frágiles, repiten el login en cada caso, no guardan capturas móviles y no dejan una traza útil cuando falla CI.
La forma práctica es usar Claude Code como compañero de diseño de pruebas. Tú defines los riesgos de negocio, las rutas, los selectores aceptables y el comando de verificación; Claude Code lee el proyecto y propone la configuración y los specs. En esta guía veremos E2E con Playwright para rutas de monetización, screenshots móviles, bloques de código, estado autenticado, Trace Viewer, retries de CI y cómo evitar tests flaky.
Usa como base la documentación oficial: Claude Code overview, Claude Code common workflows, y en Playwright Locators, Authentication, Screenshots, Trace Viewer, Retries y CI. Para completar en ClaudeCodeLab, lee estrategia de testing, CI/CD setup y diseño responsive.
Elige flujos que merecen E2E
E2E abre un navegador real, así que debe cubrir lo que una prueba unitaria no puede demostrar. Empieza con tres casos concretos:
| Caso de uso | Qué protege | Evidencia con Playwright |
|---|---|---|
| Artículo a productos | El lector llega a /products/ | CTA, URL, interacción móvil |
| Panel con sesión iniciada | El usuario autenticado entra a zonas privadas | storageState, redirect, permisos |
| Layout de artículos técnicos | Tablas y código no rompen el ancho móvil | Screenshot móvil, sin overflow, trace |
flowchart LR
A["Ruta de ingresos o registro"] --> B["Playwright E2E"]
C["Riesgo de layout móvil"] --> B
D["Validación pura"] --> E["Tests unitarios"]
F["Límite API o componente"] --> G["Tests de integración"]
En un sitio monetizado, un CTA oculto en móvil, una línea de código que ensancha la página o una zona de cliente que nunca corre en CI son fallos pequeños con impacto directo.
Pide a Claude Code con límites claros
El prompt debe definir rutas, reglas de selectores, archivos permitidos y comando de verificación.
Lee el sitio Astro existente y agrega pruebas E2E con Playwright.
Objetivos:
- Verificar que `/es/blog/claude-code-playwright-testing/` enlaza a `/products/` y `/training/`
- Revisar a 390px de ancho móvil que artículo, tablas y bloques de código no desborden
- Usar `storageState` para pruebas autenticadas, sin login UI en cada test
- En CI usar 2 retries y `trace: "on-first-retry"`
Restricciones:
- No usar `page.waitForTimeout()`
- Preferir role, label, text o test id antes que cadenas de clases CSS
- Cambiar solo `playwright.config.ts` y `tests/e2e/**`
- Ejecutar `npx playwright test` y explicar fallos con Trace Viewer
Al revisar el resultado, mira si el fallo será explicable, si los selectores representan intención de usuario y si los datos de prueba están controlados.
Configuración copiable
cd site
npm i -D @playwright/test
npx playwright install
mkdir tests/e2e
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
const baseURL = process.env.BASE_URL ?? 'http://127.0.0.1:4321';
const hasAuth = Boolean(process.env.TEST_EMAIL && process.env.TEST_PASSWORD);
const authFile = 'playwright/.auth/user.json';
export default defineConfig({
testDir: './tests/e2e',
timeout: 30_000,
expect: { timeout: 5_000 },
fullyParallel: true,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: process.env.CI ? [['html'], ['github']] : 'html',
use: {
baseURL,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
...(process.env.PLAYWRIGHT_WEB_SERVER === '1'
? {
webServer: {
command: 'npm run preview -- --host 127.0.0.1 --port 4321',
url: baseURL,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
}
: {}),
projects: [
...(hasAuth
? [
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
]
: []),
{
name: 'desktop-chrome',
use: {
...devices['Desktop Chrome'],
storageState: hasAuth ? authFile : undefined,
},
dependencies: hasAuth ? ['setup'] : [],
},
{
name: 'mobile-safari',
use: {
...devices['iPhone 13'],
storageState: hasAuth ? authFile : undefined,
},
dependencies: hasAuth ? ['setup'] : [],
},
],
});
En local no hay retries para que el problema se vea rápido. En CI sí hay retries, pero siempre con trace, screenshots y reporte HTML.
Guarda el estado autenticado
No hagas login por UI en cada test. Es lento y frágil. Usa storageState para guardar cookies y localStorage de un usuario de prueba. Añade playwright/.auth a .gitignore, porque puede contener credenciales de sesión.
// tests/e2e/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';
const authFile = path.resolve('playwright/.auth/user.json');
const email = process.env.TEST_EMAIL;
const password = process.env.TEST_PASSWORD;
setup('save signed-in browser state', async ({ page }) => {
setup.skip(!email || !password, 'Set TEST_EMAIL and TEST_PASSWORD to record auth state.');
await page.goto('/login');
await page.getByLabel(/email|メール|e-mail/i).fill(email!);
await page.getByLabel(/password|パスワード/i).fill(password!);
await page.getByRole('button', { name: /log in|sign in|ログイン/i }).click();
await expect(page).toHaveURL(/dashboard|account|admin/);
await expect(page.locator('body')).toBeVisible();
fs.mkdirSync(path.dirname(authFile), { recursive: true });
await page.context().storageState({ path: authFile });
});
Pide a Claude Code separar las pruebas del login de las pruebas que solo necesitan estar autenticadas. Así un cambio de texto en el botón de login no rompe todo el suite.
QA móvil de screenshots y bloques de código
Los artículos técnicos fallan por detalles: una línea larga desborda, una tabla empuja el viewport o el CTA queda difícil de tocar. Este spec revisa CTA, captura móvil y overflow horizontal.
// tests/e2e/article-quality.spec.ts
import { test, expect } from '@playwright/test';
const articlePath = process.env.ARTICLE_PATH ?? '/es/blog/claude-code-playwright-testing/';
test.describe('article quality checks', () => {
test('article has monetization CTAs', async ({ page }) => {
await page.goto(articlePath);
await expect(page.getByRole('heading', { level: 1 })).toContainText(/Playwright|E2E|Claude Code/i);
await expect(page.locator('a[href="/products/"], a[href="/products"]').first()).toBeVisible();
await expect(page.locator('a[href="/training/"], a[href="/training"]').first()).toBeVisible();
});
test('mobile layout has no horizontal overflow', async ({ page }, testInfo) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(articlePath);
await expect(page.locator('main, article').first()).toBeVisible();
const overflow = await page.evaluate(() => ({
viewport: window.innerWidth,
documentWidth: document.documentElement.scrollWidth,
offenders: Array.from(document.querySelectorAll('pre, table, img, iframe, .prose'))
.filter((node) => {
const rect = node.getBoundingClientRect();
return rect.left < -1 || rect.right > window.innerWidth + 1;
})
.map((node) => {
const rect = node.getBoundingClientRect();
return `${node.tagName.toLowerCase()} ${Math.round(rect.left)}-${Math.round(rect.right)}`;
}),
}));
expect(overflow.documentWidth, JSON.stringify(overflow)).toBeLessThanOrEqual(overflow.viewport + 2);
expect(overflow.offenders).toEqual([]);
await page.screenshot({ path: testInfo.outputPath('article-mobile.png'), fullPage: true });
});
test('code examples are present and copyable', async ({ page }) => {
await page.goto(articlePath);
const blocks = page.locator('pre code');
await expect(blocks.first()).toBeVisible();
expect(await blocks.count()).toBeGreaterThanOrEqual(3);
await expect(blocks.nth(0)).toContainText(/playwright|defineConfig|test/i);
});
});
La captura sirve para revisión humana; la aserción numérica sirve para CI. Esa combinación es más estable que depender solo de snapshots visuales.
Protege CTAs de ingresos y formación
// tests/e2e/revenue-flows.spec.ts
import { test, expect } from '@playwright/test';
const articlePath = process.env.ARTICLE_PATH ?? '/es/blog/claude-code-playwright-testing/';
test.describe('revenue and learning flows', () => {
test('reader can move from article to products', async ({ page }) => {
await page.goto(articlePath);
await page.locator('a[href="/products/"], a[href="/products"]').first().click();
await expect(page).toHaveURL(/\/products\/?$/);
await expect(page.locator('main').first()).toBeVisible();
});
test('training CTA is reachable on mobile', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(articlePath);
await page.locator('a[href="/training/"], a[href="/training"]').first().click();
await expect(page).toHaveURL(/\/training\/?$/);
await expect(page.locator('main').first()).toBeVisible();
});
test('main navigation can open the blog index', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('navigation').first()).toBeVisible();
await page.getByRole('link', { name: /blog|記事|articles/i }).first().click();
await expect(page).toHaveURL(/blog/);
});
});
En sitios multilingües, el texto del CTA cambia. Si la acción es crítica, usa href estable o un data-testid con intención de negocio.
Selectores para evitar flaky tests
| Prioridad | Selector | Ejemplo | Motivo |
|---|---|---|---|
| Alta | Role y nombre | page.getByRole('button', { name: /save/i }) | Cercano a la experiencia accesible |
| Alta | Label | page.getByLabel(/email/i) | Valida semántica de formulario |
| Media | Texto | page.getByText(/Start trial/) | Claro, pero depende del copy |
| Media | Test id | page.getByTestId('checkout-submit') | Bueno para acciones de negocio estables |
| Baja | Estructura CSS | .card:nth-child(3) | Se rompe con cambios de layout |
Evita page.waitForTimeout(). Usa toBeVisible(), toHaveURL() y toContainText() para esperar condiciones reales.
Depura con Trace Viewer
npx playwright test --trace on
npx playwright show-report
npx playwright show-trace test-results/path-to-trace/trace.zip
Al devolver un fallo a Claude Code, incluye el nombre del test, el estado visual en la traza y el comportamiento esperado. Eso orienta la corrección hacia selectores, datos o UI, no hacia esperas arbitrarias.
CI con retries y artefactos
# .github/workflows/playwright.yml
name: Playwright E2E
on:
pull_request:
push:
branches: [main]
jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: site
env:
BASE_URL: http://127.0.0.1:4321
PLAYWRIGHT_WEB_SERVER: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: site/package-lock.json
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run build
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: site/playwright-report
retention-days: 7
Un retry no arregla un test flaky; lo clasifica. Revisa el HTML report, el trace zip y las capturas hasta entender la causa.
Errores frecuentes
El primer error es convertir todo en E2E. Cálculos, validaciones y permisos pequeños pertenecen a unit o integration tests.
El segundo es subir el estado autenticado al repositorio. Usa usuarios de prueba con permisos limitados y excluye playwright/.auth.
El tercero es tapar inestabilidad con retries. Si un test solo pasa al reintentar, sigue siendo un riesgo.
El cuarto es pedir a Claude Code una corrección demasiado amplia. Añade primero el test que falla, mira la traza y después aplica el cambio mínimo.
Para implementarlo por tu cuenta, empieza con las plantillas de ClaudeCodeLab en products. Si lo vas a llevar a un equipo, revisa training para alinear criterios de review, CI y onboarding.
Probé este flujo en una página local estilo ClaudeCodeLab: screenshot a 390px, overflow de bloques de código, navegación a /products/ y /training/, y configuración de retry en CI. Los primeros fallos útiles no fueron de Playwright, sino líneas de código demasiado largas y nombres de enlace ambiguos. Una vez corregidos, Trace Viewer se convirtió en una evidencia clara para el siguiente prompt de Claude Code.
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
Permission receipt para Claude Code: alcance, prueba y rollback
Patrón de permission receipt para Claude Code: acciones permitidas, aprobación, pruebas, rollback y CTA de ingresos.
Agent Harness seguro para Claude Code y Codex: permisos, verificacion y rollback
Diseña un Agent Harness seguro para Claude Code y Codex con permisos, plan, verificaciones y rollback.
Subagentes de Claude Code: guía práctica para delegar trabajo de forma segura
Guía práctica de subagentes en Claude Code para dividir artículos y código: reglas, prompts, riesgos y checklist.