CultivaFlow — Suite E2E Playwright
pruebas-e2e-playwright · Next.js 14 · Supabase · Vercel
✓ 47 passed ✗ 3 failed ⏱ 2m 14s
47
Passed
3
Failed
0
Skipped
2m14s
Duration
94%
Pass rate
Cobertura E2E — CultivaFlow 94.0%
47 passed
3 failed (mocking API edge cases)
3 navegadores × 50 tests
Trazas capturadas en CI
Autenticación compartida
🔐 tests/auth.setup.ts
✓ 1 passed 1.2s
authenticate → guarda storageState para todos los tests
setup
1.2s
tests/auth.setup.ts typescript
import { test as setup } from '@playwright/test'; import path from 'path'; const authFile = path.join(__dirname, '.auth/user.json'); setup('authenticate', async ({ page }) => { // 1. Navegar al login de CultivaFlow await page.goto('/login'); await page.getByLabel('Email').fill('admin@cultivaflow.io'); await page.getByLabel('Contraseña').fill('Cultiva2024!'); await page.getByRole('button', { name: 'Iniciar sesión' }).click(); // 2. Verificar redireccion al dashboard await expect(page).toHaveURL('/dashboard'); await expect(page.getByText('Bienvenido de vuelta')).toBeVisible(); // 3. Persistir estado de sesion (evitar re-login en cada test) await page.context().storageState({ path: authFile }); });
🔑 tests/login.spec.ts
✓ 8 passed 18.4s
Login › login exitoso redirige a /dashboard
chrome firefox webkit
1.8s
Login › credenciales incorrectas muestran alerta
chrome firefox webkit
2.1s
Login › formulario vacio muestra validaciones inline
chrome firefox webkit
1.4s
Login › OAuth Google abre popup y completa auth
chrome firefox webkit
3.2s
Login › sesion persiste tras refresh (localStorage)
chrome firefox webkit
2.6s
tests/pages/login.page.ts — Page Object typescript
import { Page, Locator, expect } from '@playwright/test'; export class LoginPage { readonly emailInput: Locator; readonly passwordInput: Locator; readonly submitButton: Locator; readonly errorAlert: Locator; readonly googleButton: Locator; constructor(private page: Page) { this.emailInput = page.getByLabel('Email'); this.passwordInput = page.getByLabel('Contraseña'); this.submitButton = page.getByRole('button', { name: 'Iniciar sesión' }); this.errorAlert = page.getByRole('alert'); this.googleButton = page.getByRole('button', { name: /Continuar con Google/i }); } 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(); } async expectError(msg: string) { await expect(this.errorAlert).toContainText(msg); } async expectValidationErrors() { await expect(this.page.getByText('El email es requerido')).toBeVisible(); await expect(this.page.getByText('La contraseña es requerida')).toBeVisible(); } }
📊 tests/dashboard.spec.ts — API Mocking
✓ 12 passed 31.7s
Dashboard › muestra listado de campañas con datos reales
chromefirefoxwebkit
2.3s
Dashboard › filtro por estado "Activa" reduce resultados
chromefirefoxwebkit
1.9s
Dashboard › API mock › error 500 muestra "No se pudieron cargar campañas"
chromefirefoxwebkit
1.6s
Dashboard › API mock › array vacio muestra empty state + CTA crear
chromefirefoxwebkit
1.4s
Dashboard › paginacion → pagina 2 carga siguientes 10 campañas
chromefirefoxwebkit
2.8s
Dashboard › mock plan Enterprise muestra features premium
chromefirefoxwebkit
2.1s
tests/dashboard.spec.ts — Mocking de API typescript
import { test, expect } from '@playwright/test'; import { DashboardPage } from './pages/dashboard.page'; import { mockCampaigns } from './fixtures/campaigns'; test.describe('Dashboard — API mocking', () => { test('error 500 muestra estado de error con botón Reintentar', async ({ page }) => { // Interceptar API antes de navegar await page.route('**/api/campaigns', (route) => route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ error: 'Internal server error' }), })); await page.goto('/dashboard'); await expect(page.getByText('No se pudieron cargar las campañas')).toBeVisible(); await expect(page.getByRole('button', { name: 'Reintentar' })).toBeVisible(); }); test('array vacío muestra empty state + CTA crear campaña', async ({ page }) => { await page.route('**/api/campaigns', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [], total: 0 }), })); await page.goto('/dashboard'); await expect(page.getByText('Todavía no tienes campañas')).toBeVisible(); await expect(page.getByRole('link', { name: 'Crear primera campaña' })).toBeVisible(); }); test('plan Enterprise desbloquea features avanzadas', async ({ page }) => { // Modificar respuesta existente (patrón pass-through) await page.route('**/api/user/me', async (route) => { const response = await route.fetch(); const json = await response.json(); json.plan = 'enterprise'; await route.fulfill({ response, json }); }); await page.goto('/dashboard'); await expect(page.getByText('Plan Enterprise')).toBeVisible(); await expect(page.getByTestId('analytics-advanced')).toBeVisible(); }); });
📋 tests/campaigns.spec.ts — Form multi-step
✓ 11 passed ✗ 3 failed 47.2s
Crear campaña › step 1 Brief: todos los campos requeridos
chromefirefoxwebkit
3.1s
Crear campaña › step 2 Audiencia: segmentos seleccionables
chromefirefoxwebkit
2.7s
Crear campaña › step 3 Presupuesto: slider + input sincronizan
chromefirefoxwebkit
2.4s
Crear campaña › step 4 Revisar: resumen correcto antes de publicar
chromefirefoxwebkit
4.2s
Crear campaña › validacion presupuesto negativo → webkit timeout
chromefirefoxwebkit
10.0s
Crear campaña › navegar Atrás conserva datos del step anterior → flaky
chromefirefoxwebkit
8.3s
Crear campaña › submit API timeout muestra error + opción guardar borrador
chromefirefoxwebkit
10.0s
Regresión Visual y Accesibilidad
Dashboard — WCAG 2.1 AA completo
WCAG2AA 0 violations
Login — contraste, labels, keyboard-nav
WCAG2A 0 violations
Crear campaña — form roles, aria-required
WCAG2AA 0 violations
Configuración — modal foco atrapado correctamente
WCAG2AA 0 violations
Snapshot visual — campaigns list (fullPage)
screenshot diff 0.3%
tests/a11y.spec.ts — Accesibilidad con axe-core typescript
import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; const pages = ['/login', '/dashboard', '/campaigns/new', '/settings']; for (const route of pages) { test(`no hay violaciones WCAG2AA en ${route}`, async ({ page }) => { await page.goto(route); await page.waitForLoadState('networkidle'); const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa', 'best-practice']) .exclude('#third-party-widget') // widget externo sin control .analyze(); expect(results.violations).toEqual([]); }); } test('regresion visual — lista de campañas (fullPage)', async ({ page }) => { await page.goto('/dashboard'); await page.waitForSelector('[data-testid="campaigns-grid"]'); await expect(page).toHaveScreenshot('campaigns-list.png', { fullPage: true, maxDiffPixelRatio: 0.01, mask: [page.getByTestId('date-dynamic')], // enmascarar fechas variables }); });
GitHub Actions — Matriz CI/CD
.github/workflows/e2e.yml yaml
name: E2E Tests — CultivaFlow on: push: branches: [main, develop] pull_request: branches: [main] jobs: e2e: runs-on: ubuntu-latest strategy: matrix: browser: [chromium, firefox, webkit] fail-fast: false # continuar en otros browsers si uno falla steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 20, cache: npm } - run: npm ci - run: npx playwright install --with-deps ${{ matrix.browser }} - name: Run E2E tests run: npx playwright test --project=${{ matrix.browser }} env: BASE_URL: https://app-staging.cultivaflow.io SUPABASE_URL: ${{ secrets.SUPABASE_URL }} - uses: actions/upload-artifact@v4 if: always() with: name: playwright-report-${{ matrix.browser }} path: playwright-report/ retention-days: 30
Resultado última ejecucion CI — branch: feature/campaigns-v2
Archivo
Chromium
Firefox
WebKit
auth.setup.ts
✓ 1/1
✓ 1/1
✓ 1/1
login.spec.ts
✓ 5/5
✓ 5/5
✓ 5/5
dashboard.spec.ts
✓ 6/6
✓ 6/6
✓ 6/6
campaigns.spec.ts
✗ 6/7 — API timeout
✗ 6/7 — flaky back-nav
✗ 6/7 — slider timeout
account.spec.ts
✓ 5/5
✓ 5/5
✓ 5/5
visual.spec.ts
✓ 3/3
✓ 3/3
✓ 3/3
a11y.spec.ts
✓ 5/5
✓ 5/5
✓ 5/5
playwright.config.ts — Configuracion completa typescript
import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', fullyParallel: true, // paralelismo maximo forbidOnly: !!process.env.CI, // falla si test.only en CI retries: process.env.CI ? 2 : 0, // reintentos en CI para tests flaky workers: process.env.CI ? 1 : undefined, reporter: [ ['html', { open: 'never' }], ['junit', { outputFile: 'test-results/junit.xml' }], ['list'], ], use: { baseURL: process.env.BASE_URL || 'http://localhost:3000', trace: 'on-first-retry', // trazas para debug en CI screenshot: 'only-on-failure', // capturas solo en fallos video: 'retain-on-failure', // video en fallos locale: 'es-ES', timezoneId: 'Europe/Madrid', }, projects: [ // Auth setup — se ejecuta primero y una sola vez { name: 'setup', testMatch: /.*\.setup\.ts/ }, { name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: 'tests/.auth/user.json' }, dependencies: ['setup'], }, { name: 'firefox', use: { ...devices['Desktop Firefox'], storageState: 'tests/.auth/user.json' }, dependencies: ['setup'], }, { name: 'webkit', use: { ...devices['Desktop Safari'], storageState: 'tests/.auth/user.json' }, dependencies: ['setup'], }, { name: 'mobile-chrome', use: { ...devices['Pixel 5'], storageState: 'tests/.auth/user.json' }, dependencies: ['setup'], }, ], webServer: { command: 'npm run dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, timeout: 120_000, }, });