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.
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.
// 📁 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';
// 📁 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();
});
});
// 📁 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');
});
});
// 📁 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`);
});
});
// 📁 .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
});
}
}
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.