Se você tem 95% de cobertura de unit tests mas nunca testou o fluxo completo de cadastro até pagamento, você tem uma falsa sensação de segurança. Os bugs que mais doem em produção vivem nas integrações entre componentes, nos caminhos de usuário que ninguém testou de ponta a ponta.

Por que Playwright e não Cypress

Ambos são bons. Playwright tem vantagens práticas: suporte nativo a múltiplas abas e iframes, execução paralela por teste, auto-waiting mais confiável, e suporte a Chromium, Firefox e WebKit no mesmo setup. Para times que já usam Vitest ou Jest, a API do Playwright se integra melhor ao ecossistema existente.

// playwright.config.ts
import { defineConfig } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  timeout: 30000,
  retries: process.env.CI ? 2 : 0,
  use: {
    baseURL: 'http://localhost:3000',
    screenshot: 'only-on-failure',
    trace: 'on-first-retry',
  },
  webServer: {
    command: 'pnpm dev',
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
})

Seu primeiro teste E2E

Um teste E2E que vale a pena: o fluxo de login completo. Não é o teste mais complexo, mas valida camadas inteiras: frontend, API, autenticação, sessão.

// e2e/login.spec.ts
import { test, expect } from '@playwright/test'

test('user can log in and see dashboard', async ({ page }) => {
  await page.goto('/login')

  await page.fill('[data-testid="email"]', 'user@example.com')
  await page.fill('[data-testid="password"]', 'password123')
  await page.click('[data-testid="submit"]')

  await expect(page).toHaveURL('/dashboard')
  await expect(page.locator('h1')).toContainText('Welcome')
})

Observe: data-testid como seletores, não classes CSS ou texto visível. Seletores estáveis sobrevivem a mudanças de UI.

Page Object Model: não repita seletores

Quando você tem 20 testes que usam o mesmo formulário de login, selectors duplicados viram manutenção pesada. Page Objects encapsulam a interação com cada página:

// e2e/pages/LoginPage.ts
import { Page, expect } from '@playwright/test'

export class LoginPage {
  constructor(private readonly page: Page) {}

  async goto() {
    await this.page.goto('/login')
  }

  async login(email: string, password: string) {
    await this.page.fill('[data-testid="email"]', email)
    await this.page.fill('[data-testid="password"]', password)
    await this.page.click('[data-testid="submit"]')
  }

  async expectError(message: string) {
    await expect(this.page.locator('[data-testid="error"]'))
      .toContainText(message)
  }
}

// e2e/login.spec.ts
test('failed login shows error', async ({ page }) => {
  const loginPage = new LoginPage(page)
  await loginPage.goto()
  await loginPage.login('wrong@example.com', 'bad')
  await loginPage.expectError('Credenciais inválidas')
})

Testando estados de erro

Onde E2E agrega mais valor é testando caminhos de erro que unit tests não cobrem: timeout de API, erro de rede, estado de loading, comportamento offline.

test('shows error when API is down', async ({ page }) => {
  // Intercepta a API e simula falha
  await page.route('**/api/orders', route => route.abort('connectionrefused'))

  await page.goto('/orders')

  await expect(page.locator('[data-testid="error-message"]'))
    .toContainText('Não foi possível carregar os pedidos')
  await expect(page.locator('[data-testid="retry-button"]'))
    .toBeVisible()
})

Dados de teste: seeding e cleanup

E2E tests precisam de dados consistentes. Seus dados de teste devem ser criados antes de cada teste e removidos depois:

// e2e/fixtures.ts
import { test as base } from '@playwright/test'

export const test = base.extend<{ testUser: User }>({
  testUser: async ({ request }, use) => {
    // Cria usuário de teste via API
    const res = await request.post('/api/test/users', {
      data: { email: `test-${Date.now()}@example.com`, password: 'test123' }
    })
    const user = await res.json()

    await use(user)

    // Cleanup
    await request.delete(`/api/test/users/${user.id}`)
  },
})

Execução em CI

Playwright em CI precisa de passos específicos: instalar browsers, rodar com --reporter=html, e上传 artifacts em caso de falha. No GitHub Actions:

- name: Install Playwright
  run: pnpm exec playwright install --with-deps

- name: Run E2E tests
  run: pnpm test:e2e

- name: Upload test report
  if: failure()
  uses: actions/upload-artifact@v4
  with:
    name: playwright-report
    path: playwright-report/

Onde parar

Não teste tudo com E2E. Testes E2E são lentos, frágeis, e caros de manter. Use-os para os fluxos críticos de usuário: login, checkout, criação de conta, fluxos de pagamento. Para o resto, unit tests e integration tests são suficientes.

A regra: se uma falha nesse fluxo gera perda de receita ou dados, teste E2E. Se é uma feature secundária, teste com unit ou integration.