🚀 Oferta especial: 60% OFF no CrazyStack - Últimas vagas!Garantir vaga →
Módulo 4 - Aula 4

E2E Testing

Domine testes end-to-end para tRPC: Playwright avançado, user flows complexos, visual testing, automação CI/CD e debugging para SaaS enterprise-grade.

90 min
Avançado
E2E Testing

🎯 Por que E2E testing é crucial para SaaS?

Confiança Total: Validam toda a jornada do usuário, desde interface até banco de dados.

Detecção de Regressões: Identificam problemas que testes unitários não conseguem capturar.

🎭 Setup Avançado do Playwright

playwright.config.ts
// 📁 playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  // 📁 Diretório de testes
  testDir: './tests/e2e',
  
  // ⏰ Timeout global
  timeout: 30 * 1000,
  expect: {
    timeout: 10 * 1000,
  },
  
  // 🎯 Configurações globais
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  
  // 📊 Reporter
  reporter: [
    ['html'],
    ['junit', { outputFile: 'test-results/junit.xml' }],
    ['json', { outputFile: 'test-results/results.json' }],
  ],
  
  // 📂 Output
  outputDir: 'test-results/',
  
  // 🔧 Configurações globais
  use: {
    baseURL: process.env.E2E_BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    
    // 🍪 Configurações de autenticação
    storageState: 'tests/auth.json',
  },

  // 📱 Configurações de projetos/browsers
  projects: [
    // 🔧 Setup project para autenticação
    {
      name: 'setup',
      testMatch: /.*\.setup\.ts/,
    },
    
    // 💻 Desktop browsers
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
      dependencies: ['setup'],
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
      dependencies: ['setup'],
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
      dependencies: ['setup'],
    },

    // 📱 Mobile devices
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
      dependencies: ['setup'],
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] },
      dependencies: ['setup'],
    },
  ],

  // 🚀 Web server para desenvolvimento
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120 * 1000,
  },
});

// 📁 tests/e2e/setup/auth.setup.ts
import { test as setup, expect } from '@playwright/test';

const authFile = 'tests/auth.json';

setup('authenticate', async ({ page }) => {
  // 🔐 Realizar login
  await page.goto('/login');
  
  await page.fill('[data-testid="email"]', 'admin@test.com');
  await page.fill('[data-testid="password"]', 'password123');
  await page.click('[data-testid="login-button"]');
  
  // ✅ Aguardar redirecionamento para dashboard
  await page.waitForURL('/dashboard');
  await expect(page.getByText('Bem-vindo')).toBeVisible();
  
  // 💾 Salvar estado de autenticação
  await page.context().storageState({ path: authFile });
});

// 📁 tests/e2e/fixtures/test-fixtures.ts
import { test as base, expect } from '@playwright/test';
import { PrismaClient } from '@prisma/client';

// 🎯 Fixtures customizadas para testes E2E
export const test = base.extend<{
  // 🗄️ Database utilities
  db: PrismaClient;
  // 👤 User management
  createUser: (userData: any) => Promise<any>;
  // 📧 Email verification
  getLatestEmail: () => Promise<any>;
  // 🔧 Admin actions
  adminPage: any;
}>({
  // 🗄️ Database fixture
  db: async ({}, use) => {
    const prisma = new PrismaClient({
      datasources: {
        db: { url: process.env.TEST_DATABASE_URL },
      },
    });
    
    await use(prisma);
    await prisma.$disconnect();
  },

  // 👤 User creation fixture
  createUser: async ({ db }, use) => {
    const createdUsers: string[] = [];
    
    const createUser = async (userData: {
      email: string;
      name: string;
      role?: string;
    }) => {
      const user = await db.user.create({
        data: {
          email: userData.email,
          name: userData.name,
          role: userData.role || 'USER',
          password: 'hashed-password',
        },
      });
      
      createdUsers.push(user.id);
      return user;
    };
    
    await use(createUser);
    
    // 🧹 Cleanup
    await db.user.deleteMany({
      where: { id: { in: createdUsers } },
    });
  },

  // 📧 Email utilities
  getLatestEmail: async ({}, use) => {
    const getLatestEmail = async () => {
      // Simular busca por último email no MailHog ou serviço de teste
      const response = await fetch('http://localhost:8025/api/v2/messages');
      const emails = await response.json();
      return emails.items[0];
    };
    
    await use(getLatestEmail);
  },

  // 🔧 Admin page fixture
  adminPage: async ({ page, createUser }, use) => {
    // Criar usuário admin
    const adminUser = await createUser({
      email: 'admin@e2e-test.com',
      name: 'E2E Admin',
      role: 'ADMIN',
    });
    
    // Fazer login como admin
    await page.goto('/login');
    await page.fill('[data-testid="email"]', adminUser.email);
    await page.fill('[data-testid="password"]', 'password123');
    await page.click('[data-testid="login-button"]');
    await page.waitForURL('/dashboard');
    
    await use(page);
  },
});

export { expect } from '@playwright/test';

🚶 User Flows Complexos

tests/e2e/user-flows/onboarding.spec.ts
// 📁 tests/e2e/user-flows/onboarding.spec.ts
import { test, expect } from '../fixtures/test-fixtures';

test.describe('Complete User Onboarding Flow', () => {
  test('🚀 deve completar onboarding de novo usuário', async ({ 
    page, 
    createUser, 
    getLatestEmail 
  }) => {
    // 🎯 Arrange - Criar usuário sem onboarding
    const user = await createUser({
      email: 'newuser@onboarding.test',
      name: 'New User',
    });

    // 🎬 Step 1: Login inicial
    await page.goto('/login');
    await page.fill('[data-testid="email"]', user.email);
    await page.fill('[data-testid="password"]', 'password123');
    await page.click('[data-testid="login-button"]');

    // ✅ Deve ser redirecionado para onboarding
    await expect(page).toHaveURL('/onboarding');
    await expect(page.getByText('Bem-vindo! Vamos configurar sua conta')).toBeVisible();

    // 🎬 Step 2: Configurar perfil
    await page.fill('[data-testid="company-name"]', 'Test Company');
    await page.selectOption('[data-testid="company-size"]', '11-50');
    await page.selectOption('[data-testid="industry"]', 'technology');
    await page.click('[data-testid="next-step"]');

    // ✅ Verificar progresso
    const progressBar = page.locator('[data-testid="progress-bar"]');
    await expect(progressBar).toHaveAttribute('aria-valuenow', '33');

    // 🎬 Step 3: Escolher plano
    await page.click('[data-testid="plan-pro"]');
    await expect(page.locator('[data-testid="plan-pro"]')).toHaveClass(/selected/);
    
    // Preencher dados de pagamento (mock)
    await page.fill('[data-testid="card-number"]', '4242424242424242');
    await page.fill('[data-testid="card-expiry"]', '12/25');
    await page.fill('[data-testid="card-cvc"]', '123');
    await page.click('[data-testid="next-step"]');

    // ✅ Verificar processamento de pagamento
    await expect(page.getByText('Processando pagamento...')).toBeVisible();
    await expect(page.getByText('Pagamento confirmado!')).toBeVisible({ timeout: 10000 });

    // 🎬 Step 4: Convidar membros da equipe
    await page.fill('[data-testid="member-email-0"]', 'member1@company.test');
    await page.selectOption('[data-testid="member-role-0"]', 'EDITOR');
    
    await page.click('[data-testid="add-member"]');
    await page.fill('[data-testid="member-email-1"]', 'member2@company.test');
    await page.selectOption('[data-testid="member-role-1"]', 'VIEWER');
    
    await page.click('[data-testid="send-invites"]');

    // ✅ Verificar convites enviados
    await expect(page.getByText('Convites enviados com sucesso!')).toBeVisible();
    
    // Verificar email de convite
    const latestEmail = await getLatestEmail();
    expect(latestEmail.subject).toContain('Convite para');

    // 🎬 Step 5: Finalizar onboarding
    await page.click('[data-testid="complete-onboarding"]');

    // ✅ Deve ser redirecionado para dashboard
    await expect(page).toHaveURL('/dashboard');
    await expect(page.getByText('Parabéns! Sua conta está configurada')).toBeVisible();

    // Verificar que elementos de onboarding não aparecem mais
    await expect(page.locator('[data-testid="onboarding-banner"]')).not.toBeVisible();
  });

  test('📱 deve funcionar corretamente em mobile', async ({ 
    page, 
    createUser,
    browser 
  }) => {
    // 🔧 Criar contexto mobile
    const mobileContext = await browser.newContext({
      ...devices['iPhone 12'],
    });
    const mobilePage = await mobileContext.newPage();

    const user = await createUser({
      email: 'mobile@onboarding.test',
      name: 'Mobile User',
    });

    // 🎬 Executar onboarding em mobile
    await mobilePage.goto('/login');
    await mobilePage.fill('[data-testid="email"]', user.email);
    await mobilePage.fill('[data-testid="password"]', 'password123');
    await mobilePage.click('[data-testid="login-button"]');

    // ✅ Verificar layout mobile
    await expect(mobilePage).toHaveURL('/onboarding');
    
    // Verificar que steps são empilhados verticalmente
    const stepContainer = mobilePage.locator('[data-testid="steps-container"]');
    await expect(stepContainer).toHaveClass(/mobile-layout/);

    // Verificar navegação mobile
    await mobilePage.fill('[data-testid="company-name"]', 'Mobile Company');
    await mobilePage.click('[data-testid="next-step"]');
    
    // Em mobile, deve mostrar apenas um step por vez
    await expect(mobilePage.locator('[data-testid="step-1"]')).not.toBeVisible();
    await expect(mobilePage.locator('[data-testid="step-2"]')).toBeVisible();

    await mobileContext.close();
  });
});

// 📁 tests/e2e/user-flows/purchase-flow.spec.ts
import { test, expect } from '../fixtures/test-fixtures';

test.describe('Complete Purchase Flow', () => {
  test('💳 deve completar compra de upgrade de plano', async ({ 
    page, 
    createUser, 
    db 
  }) => {
    // 🎯 Arrange
    const user = await createUser({
      email: 'upgrade@purchase.test',
      name: 'Upgrade User',
    });

    // Criar organização com plano FREE
    const org = await db.organization.create({
      data: {
        name: 'Upgrade Organization',
        plan: 'FREE',
        ownerId: user.id,
      },
    });

    // 🎬 Step 1: Navegar para billing
    await page.goto('/dashboard');
    await page.click('[data-testid="sidebar-billing"]');
    await expect(page).toHaveURL('/billing');

    // ✅ Verificar plano atual
    await expect(page.getByText('Plano Atual: FREE')).toBeVisible();
    
    // 🎬 Step 2: Iniciar upgrade
    await page.click('[data-testid="upgrade-to-pro"]');
    
    // Verificar modal de upgrade
    const upgradeModal = page.locator('[data-testid="upgrade-modal"]');
    await expect(upgradeModal).toBeVisible();
    
    // Verificar cálculo de preço
    await expect(upgradeModal.getByText('$29/mês')).toBeVisible();
    await expect(upgradeModal.getByText('Cobrado anualmente: $290')).toBeVisible();

    // 🎬 Step 3: Confirmar upgrade
    await page.click('[data-testid="confirm-upgrade"]');
    
    // Preencher formulário de pagamento
    const paymentForm = page.locator('[data-testid="payment-form"]');
    await expect(paymentForm).toBeVisible();
    
    await page.fill('[data-testid="cardholder-name"]', 'Test User');
    await page.fill('[data-testid="card-number"]', '4242424242424242');
    await page.fill('[data-testid="card-expiry"]', '12/25');
    await page.fill('[data-testid="card-cvc"]', '123');
    
    // Aceitar termos
    await page.check('[data-testid="accept-terms"]');
    
    // 🎬 Step 4: Processar pagamento
    await page.click('[data-testid="process-payment"]');
    
    // ✅ Verificar loading state
    await expect(page.getByText('Processando pagamento...')).toBeVisible();
    await expect(page.locator('[data-testid="payment-spinner"]')).toBeVisible();
    
    // ✅ Verificar sucesso
    await expect(page.getByText('Upgrade realizado com sucesso!')).toBeVisible({ timeout: 15000 });
    await expect(page.getByText('Bem-vindo ao plano PRO!')).toBeVisible();
    
    // 🎬 Step 5: Verificar mudanças na interface
    await page.click('[data-testid="close-success-modal"]');
    
    // Plano deve ter mudado
    await expect(page.getByText('Plano Atual: PRO')).toBeVisible();
    
    // Novas funcionalidades devem estar disponíveis
    await expect(page.locator('[data-testid="pro-features"]')).toBeVisible();
    await expect(page.getByText('Analytics Avançados')).toBeVisible();
    await expect(page.getByText('API Access')).toBeVisible();
    
    // 🎬 Step 6: Verificar no banco de dados
    const updatedOrg = await db.organization.findUnique({
      where: { id: org.id },
      include: { subscription: true },
    });
    
    expect(updatedOrg?.plan).toBe('PRO');
    expect(updatedOrg?.subscription?.status).toBe('active');
  });

  test('❌ deve lidar com falha de pagamento', async ({ 
    page, 
    createUser 
  }) => {
    const user = await createUser({
      email: 'payment-fail@test.com',
      name: 'Payment Fail User',
    });

    await page.goto('/billing');
    await page.click('[data-testid="upgrade-to-pro"]');
    await page.click('[data-testid="confirm-upgrade"]');
    
    // Usar cartão que falha
    await page.fill('[data-testid="card-number"]', '4000000000000002');
    await page.fill('[data-testid="card-expiry"]', '12/25');
    await page.fill('[data-testid="card-cvc"]', '123');
    await page.check('[data-testid="accept-terms"]');
    
    await page.click('[data-testid="process-payment"]');
    
    // ✅ Verificar erro
    await expect(page.getByText('Cartão foi recusado')).toBeVisible();
    await expect(page.locator('[data-testid="payment-error"]')).toBeVisible();
    
    // Plano deve permanecer FREE
    await page.click('[data-testid="close-error-modal"]');
    await expect(page.getByText('Plano Atual: FREE')).toBeVisible();
  });
});

👁️ Visual Testing

tests/e2e/visual/visual-regression.spec.ts
// 📁 tests/e2e/visual/visual-regression.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Visual Regression Tests', () => {
  test('📸 deve capturar screenshots de páginas principais', async ({ page }) => {
    // 🏠 Home page
    await page.goto('/');
    await expect(page).toHaveScreenshot('homepage.png', {
      fullPage: true,
      animations: 'disabled',
    });

    // 📊 Dashboard
    await page.goto('/dashboard');
    await page.waitForLoadState('networkidle');
    await expect(page).toHaveScreenshot('dashboard.png', {
      mask: [
        page.locator('[data-testid="user-avatar"]'), // Mask dynamic content
        page.locator('[data-testid="last-login"]'),
      ],
    });

    // ⚙️ Settings
    await page.goto('/settings');
    await expect(page).toHaveScreenshot('settings.png');
  });

  test('🎨 deve validar componentes específicos', async ({ page }) => {
    await page.goto('/dashboard');

    // 📊 Sidebar navigation
    const sidebar = page.locator('[data-testid="sidebar"]');
    await expect(sidebar).toHaveScreenshot('sidebar-component.png');

    // 📈 Analytics card
    const analyticsCard = page.locator('[data-testid="analytics-card"]');
    await expect(analyticsCard).toHaveScreenshot('analytics-card.png');

    // 🔔 Notifications panel
    await page.click('[data-testid="notifications-trigger"]');
    const notificationsPanel = page.locator('[data-testid="notifications-panel"]');
    await expect(notificationsPanel).toHaveScreenshot('notifications-panel.png');
  });

  test('📱 deve validar responsividade', async ({ page }) => {
    // Desktop
    await page.setViewportSize({ width: 1920, height: 1080 });
    await page.goto('/dashboard');
    await expect(page).toHaveScreenshot('dashboard-desktop.png');

    // Tablet
    await page.setViewportSize({ width: 768, height: 1024 });
    await expect(page).toHaveScreenshot('dashboard-tablet.png');

    // Mobile
    await page.setViewportSize({ width: 375, height: 667 });
    await expect(page).toHaveScreenshot('dashboard-mobile.png');
  });

  test('🌗 deve validar dark/light theme', async ({ page }) => {
    // Light theme
    await page.goto('/dashboard');
    await page.locator('[data-testid="theme-toggle"]').click();
    await page.waitForSelector('[data-theme="light"]');
    await expect(page).toHaveScreenshot('dashboard-light-theme.png');

    // Dark theme
    await page.locator('[data-testid="theme-toggle"]').click();
    await page.waitForSelector('[data-theme="dark"]');
    await expect(page).toHaveScreenshot('dashboard-dark-theme.png');
  });
});

// 📁 tests/e2e/visual/component-states.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Component State Testing', () => {
  test('🔄 deve capturar estados de loading', async ({ page }) => {
    await page.goto('/dashboard');

    // Simular loading state interceptando requests
    await page.route('**/api/trpc/analytics.getOverview*', async route => {
      // Delay de 5 segundos para capturar loading
      await new Promise(resolve => setTimeout(resolve, 5000));
      await route.continue();
    });

    // Refresh para disparar loading
    await page.reload();
    
    // Capturar loading state
    const loadingCard = page.locator('[data-testid="analytics-card"]');
    await expect(loadingCard).toHaveScreenshot('analytics-loading-state.png');
  });

  test('❌ deve capturar estados de erro', async ({ page }) => {
    // Mock error response
    await page.route('**/api/trpc/analytics.getOverview*', async route => {
      await route.fulfill({
        status: 500,
        contentType: 'application/json',
        body: JSON.stringify({
          error: { message: 'Internal server error' }
        })
      });
    });

    await page.goto('/dashboard');
    
    // Capturar error state
    const errorCard = page.locator('[data-testid="analytics-card"]');
    await expect(errorCard).toHaveScreenshot('analytics-error-state.png');
  });

  test('📊 deve capturar diferentes dados', async ({ page }) => {
    // Mock com dados específicos
    await page.route('**/api/trpc/analytics.getOverview*', async route => {
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify({
          result: {
            data: {
              users: 1000,
              revenue: 50000,
              growth: 25.5,
              apiCalls: 500000,
            }
          }
        })
      });
    });

    await page.goto('/dashboard');
    await page.waitForLoadState('networkidle');
    
    // Capturar com dados específicos
    await expect(page).toHaveScreenshot('dashboard-high-metrics.png');
  });
});

// 📁 tests/e2e/visual/accessibility.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('Accessibility Visual Tests', () => {
  test('♿ deve validar contraste e acessibilidade', async ({ page }) => {
    await page.goto('/dashboard');

    // Executar auditoria de acessibilidade
    const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
    expect(accessibilityScanResults.violations).toEqual([]);

    // Capturar com foco visível
    await page.keyboard.press('Tab'); // Foco no primeiro elemento
    await expect(page).toHaveScreenshot('dashboard-focus-visible.png');

    // Testar navegação por teclado
    for (let i = 0; i < 5; i++) {
      await page.keyboard.press('Tab');
      await page.waitForTimeout(200);
    }
    await expect(page).toHaveScreenshot('dashboard-keyboard-navigation.png');
  });

  test('🔍 deve validar zoom 200%', async ({ page }) => {
    await page.goto('/dashboard');
    
    // Aplicar zoom 200%
    await page.evaluate(() => {
      document.body.style.zoom = '2';
    });
    
    await expect(page).toHaveScreenshot('dashboard-zoom-200.png', {
      fullPage: true,
    });
  });

  test('🎨 deve validar high contrast mode', async ({ page }) => {
    await page.emulateMedia({ colorScheme: 'dark', forcedColors: 'active' });
    await page.goto('/dashboard');
    
    await expect(page).toHaveScreenshot('dashboard-high-contrast.png');
  });
});

⚡ Performance Testing

tests/e2e/performance/performance.spec.ts
// 📁 tests/e2e/performance/performance.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Performance Tests', () => {
  test('🚀 deve validar Core Web Vitals', async ({ page }) => {
    // Navegar para página principal
    await page.goto('/dashboard');

    // Aguardar carregamento completo
    await page.waitForLoadState('networkidle');

    // Medir Core Web Vitals
    const webVitals = await page.evaluate(() => {
      return new Promise((resolve) => {
        const vitals = {};
        
        // LCP (Largest Contentful Paint)
        new PerformanceObserver((entryList) => {
          const entries = entryList.getEntries();
          const lastEntry = entries[entries.length - 1];
          vitals.lcp = lastEntry.startTime;
        }).observe({ entryTypes: ['largest-contentful-paint'] });

        // CLS (Cumulative Layout Shift)
        new PerformanceObserver((entryList) => {
          let cls = 0;
          for (const entry of entryList.getEntries()) {
            if (!entry.hadRecentInput) {
              cls += entry.value;
            }
          }
          vitals.cls = cls;
        }).observe({ entryTypes: ['layout-shift'] });

        // FID (First Input Delay) - simular através de timing
        vitals.fid = performance.now();
        
        setTimeout(() => resolve(vitals), 3000);
      });
    });

    // ✅ Validar métricas
    expect(webVitals.lcp).toBeLessThan(2500); // LCP < 2.5s
    expect(webVitals.cls).toBeLessThan(0.1);  // CLS < 0.1
    
    console.log('Web Vitals:', webVitals);
  });

  test('📊 deve medir tempo de carregamento de páginas', async ({ page }) => {
    const pages = [
      { url: '/dashboard', name: 'Dashboard' },
      { url: '/analytics', name: 'Analytics' },
      { url: '/settings', name: 'Settings' },
      { url: '/billing', name: 'Billing' },
    ];

    for (const pageInfo of pages) {
      const startTime = Date.now();
      
      await page.goto(pageInfo.url);
      await page.waitForLoadState('networkidle');
      
      const loadTime = Date.now() - startTime;
      
      // ✅ Página deve carregar em menos de 3 segundos
      expect(loadTime).toBeLessThan(3000);
      
      console.log(`${pageInfo.name}: ${loadTime}ms`);
    }
  });

  test('🔄 deve medir performance de interações', async ({ page }) => {
    await page.goto('/dashboard');

    // Medir tempo de abertura de modal
    const modalOpenStart = Date.now();
    await page.click('[data-testid="create-project-button"]');
    await page.waitForSelector('[data-testid="create-project-modal"]');
    const modalOpenTime = Date.now() - modalOpenStart;

    expect(modalOpenTime).toBeLessThan(500); // Modal deve abrir em < 500ms

    // Medir tempo de formulário
    const formStart = Date.now();
    await page.fill('[data-testid="project-name"]', 'Test Project');
    await page.selectOption('[data-testid="project-type"]', 'web');
    await page.click('[data-testid="create-project-submit"]');
    await page.waitForSelector('[data-testid="project-created-toast"]');
    const formTime = Date.now() - formStart;

    expect(formTime).toBeLessThan(2000); // Criação deve levar < 2s

    console.log(`Modal open: ${modalOpenTime}ms, Form submit: ${formTime}ms`);
  });

  test('📈 deve validar performance com dados grandes', async ({ page }) => {
    // Mock com dataset grande
    await page.route('**/api/trpc/analytics.getDetailedReport*', async route => {
      const largeDataset = Array.from({ length: 10000 }, (_, i) => ({
        id: i,
        name: `Item ${i}`,
        value: Math.random() * 1000,
        date: new Date(Date.now() - i * 86400000).toISOString(),
      }));

      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify({
          result: { data: largeDataset }
        })
      });
    });

    const startTime = Date.now();
    await page.goto('/analytics/detailed');
    
    // Aguardar tabela renderizar
    await page.waitForSelector('[data-testid="data-table"]');
    await page.waitForFunction(() => {
      const table = document.querySelector('[data-testid="data-table"]');
      return table && table.children.length > 0;
    });

    const renderTime = Date.now() - startTime;
    
    // ✅ Mesmo com muitos dados, deve renderizar em < 5s
    expect(renderTime).toBeLessThan(5000);
    
    // Testar scroll performance
    const scrollStart = Date.now();
    await page.mouse.wheel(0, 5000); // Scroll grande
    await page.waitForTimeout(100);
    const scrollTime = Date.now() - scrollStart;
    
    expect(scrollTime).toBeLessThan(100); // Scroll suave

    console.log(`Large dataset render: ${renderTime}ms, Scroll: ${scrollTime}ms`);
  });
});

// 📁 tests/e2e/performance/memory-leaks.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Memory Leak Tests', () => {
  test('🧠 deve verificar vazamentos de memória', async ({ page }) => {
    await page.goto('/dashboard');

    // Medir memória inicial
    const initialMemory = await page.evaluate(() => {
      return (performance as any).memory?.usedJSHeapSize || 0;
    });

    // Executar ações que podem causar vazamentos
    for (let i = 0; i < 10; i++) {
      // Abrir e fechar modais
      await page.click('[data-testid="create-project-button"]');
      await page.waitForSelector('[data-testid="create-project-modal"]');
      await page.press('Escape');
      await page.waitForSelector('[data-testid="create-project-modal"]', { state: 'hidden' });

      // Navegar entre páginas
      await page.click('[data-testid="nav-analytics"]');
      await page.waitForLoadState('networkidle');
      await page.click('[data-testid="nav-dashboard"]');
      await page.waitForLoadState('networkidle');
    }

    // Forçar garbage collection (se disponível)
    await page.evaluate(() => {
      if ((window as any).gc) {
        (window as any).gc();
      }
    });

    // Medir memória final
    const finalMemory = await page.evaluate(() => {
      return (performance as any).memory?.usedJSHeapSize || 0;
    });

    const memoryIncrease = finalMemory - initialMemory;
    const memoryIncreaseMB = memoryIncrease / (1024 * 1024);

    // ✅ Aumento de memória deve ser menor que 50MB
    expect(memoryIncreaseMB).toBeLessThan(50);

    console.log(`Memory increase: ${memoryIncreaseMB.toFixed(2)}MB`);
  });
});

🚀 CI/CD Integration

.github/workflows/e2e-tests.yml
// 📁 .github/workflows/e2e-tests.yml
name: E2E Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  e2e-tests:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:15-alpine
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: trpc_e2e_test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Setup test database
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/trpc_e2e_test
        run: |
          npx prisma migrate deploy
          npx prisma db seed

      - name: Build application
        run: npm run build
        env:
          NODE_ENV: production

      - name: Install Playwright Browsers
        run: npx playwright install --with-deps

      - name: Run E2E tests
        run: npm run test:e2e
        env:
          NODE_ENV: test
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/trpc_e2e_test
          E2E_BASE_URL: http://localhost:3000

      - name: Upload Playwright Report
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

      - name: Upload test results
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: test-results
          path: test-results/
          retention-days: 30

  visual-regression:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright
        run: npx playwright install --with-deps

      - name: Run visual regression tests
        run: npm run test:visual
        env:
          NODE_ENV: test

      - name: Upload visual diffs
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: visual-regression-diffs
          path: test-results/
          retention-days: 30

// 📁 package.json - Scripts de E2E
{
  "scripts": {
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui",
    "test:e2e:debug": "playwright test --debug",
    "test:e2e:headed": "playwright test --headed",
    "test:visual": "playwright test tests/e2e/visual/",
    "test:performance": "playwright test tests/e2e/performance/",
    "test:mobile": "playwright test --project='Mobile Chrome' --project='Mobile Safari'",
    "test:cross-browser": "playwright test --project=chromium --project=firefox --project=webkit"
  }
}

// 📁 tests/e2e/utils/test-helpers.ts
import { Page, expect } from '@playwright/test';

export class TestHelpers {
  constructor(private page: Page) {}

  // 🔄 Aguardar carregamento completo
  async waitForFullLoad() {
    await this.page.waitForLoadState('networkidle');
    await this.page.waitForFunction(() => {
      return document.readyState === 'complete';
    });
  }

  // 📊 Aguardar dados carregarem
  async waitForDataLoad(selector: string) {
    await this.page.waitForSelector(selector);
    await this.page.waitForFunction((sel) => {
      const element = document.querySelector(sel);
      return element && !element.hasAttribute('data-loading');
    }, selector);
  }

  // 🔔 Verificar toast de sucesso
  async expectSuccessToast(message?: string) {
    const toast = this.page.locator('[data-testid="success-toast"]');
    await expect(toast).toBeVisible();
    
    if (message) {
      await expect(toast).toContainText(message);
    }
    
    // Toast deve desaparecer automaticamente
    await expect(toast).not.toBeVisible({ timeout: 10000 });
  }

  // ❌ Verificar toast de erro
  async expectErrorToast(message?: string) {
    const toast = this.page.locator('[data-testid="error-toast"]');
    await expect(toast).toBeVisible();
    
    if (message) {
      await expect(toast).toContainText(message);
    }
  }

  // 📱 Simular condições de rede
  async simulateSlowNetwork() {
    await this.page.route('**/*', async (route) => {
      await new Promise(resolve => setTimeout(resolve, 1000));
      await route.continue();
    });
  }

  async simulateOffline() {
    await this.page.context().setOffline(true);
  }

  async restoreNetwork() {
    await this.page.context().setOffline(false);
    await this.page.unroute('**/*');
  }

  // 📸 Screenshot com timestamp
  async takeTimestampedScreenshot(name: string) {
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    await this.page.screenshot({ 
      path: `test-results/${name}-${timestamp}.png`,
      fullPage: true 
    });
  }
}

💡 Melhores Práticas para E2E Testing

Page Object Model:Organize seletores e ações em classes reutilizáveis.

Data Test IDs:Use data-testid ao invés de classes CSS para seletores estáveis.

Testes Independentes:Cada teste deve ser completamente independente e auto-contido.

Paralelização:Execute testes em paralelo para reduzir tempo total de execução.