E2E testing com Playwright ou Cypress

1. Introdução aos Testes E2E no Ecossistema React

Testes End-to-End (E2E) simulam a experiência real do usuário em uma aplicação React, validando fluxos completos desde a interface até o backend. Diferentemente de testes unitários (que verificam funções isoladas) ou de integração (que testam módulos combinados), os testes E2E garantem que o sistema como um todo funcione corretamente no navegador.

No ecossistema React, onde componentes interagem com hooks, estado global e APIs externas, os testes E2E são essenciais para capturar regressões visuais e de comportamento. Duas ferramentas dominam esse cenário:

  • Playwright (Microsoft): Focado em automação cross-browser, com suporte nativo a Chromium, Firefox e WebKit. Ideal para projetos que exigem testes em múltiplos navegadores e paralelização avançada.
  • Cypress (Cypress.io): Conhecido por sua experiência de desenvolvimento rica, com time-travel debugging e recarregamento automático. Excelente para equipes que priorizam produtividade no desenvolvimento de testes.

Ambas são escritas em JavaScript/TypeScript e integram-se perfeitamente com Node.js e React.

2. Configuração Inicial do Ambiente de Testes

Playwright

// Instalação
npm init playwright@latest
// ou
yarn create playwright

// playwright.config.js
const { defineConfig } = require('@playwright/test');
export default defineConfig({
  testDir: './e2e',
  timeout: 30000,
  use: {
    baseURL: 'http://localhost:3000',
    headless: true,
    viewport: { width: 1280, height: 720 },
  },
  projects: [
    { name: 'chromium', use: { browserName: 'chromium' } },
    { name: 'firefox', use: { browserName: 'firefox' } },
  ],
});

Cypress

// Instalação
npm install cypress --save-dev
npx cypress open

// cypress.config.js
const { defineConfig } = require('cypress');
export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    supportFile: 'cypress/support/e2e.js',
    specPattern: 'cypress/e2e/**/*.cy.js',
  },
});

Estrutura de diretórios recomendada:

projeto-react/
├── e2e/              # Playwright
│   ├── tests/
│   └── fixtures/
├── cypress/          # Cypress
│   ├── e2e/
│   ├── fixtures/
│   └── support/
└── playwright.config.js / cypress.config.js

3. Escrevendo o Primeiro Teste E2E

Vamos testar um fluxo de login em uma aplicação React com formulário controlado.

Playwright

// e2e/tests/login.spec.js
const { test, expect } = require('@playwright/test');

test('deve fazer login com sucesso', async ({ page }) => {
  await page.goto('/login');

  // Localizadores modernos (recomendados)
  await page.getByLabel('Email').fill('usuario@exemplo.com');
  await page.getByLabel('Senha').fill('senha123');
  await page.getByRole('button', { name: 'Entrar' }).click();

  // Aguarda redirecionamento
  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByText('Bem-vindo, Usuário!')).toBeVisible();
});

Cypress

// cypress/e2e/login.cy.js
describe('Fluxo de Login', () => {
  it('deve fazer login com sucesso', () => {
    cy.visit('/login');

    cy.get('input[name="email"]').type('usuario@exemplo.com');
    cy.get('input[name="senha"]').type('senha123');
    cy.get('button[type="submit"]').click();

    cy.url().should('include', '/dashboard');
    cy.contains('Bem-vindo, Usuário!').should('be.visible');
  });
});

Localizadores modernos: Prefira getByRole, getByText e getByTestId (Playwright) ou cy.contains e cy.get com atributos data-cy (Cypress) em vez de seletores CSS frágeis.

4. Interações Avançadas e Asserções

Aguardando elementos dinâmicos

// Playwright - aguardar elemento assíncrono
await page.waitForSelector('[data-testid="loading-spinner"]', { state: 'hidden' });
await expect(page.getByTestId('user-list')).toBeVisible({ timeout: 10000 });

// Cypress - asserções com retry automático
cy.get('[data-cy="modal"]', { timeout: 10000 }).should('be.visible');
cy.get('[data-cy="save-button"]').should('not.be.disabled');

Testando formulários com React Suspense

// Playwright
test('submeter formulário com lazy loading', async ({ page }) => {
  await page.goto('/profile');
  await page.getByLabel('Nome').fill('Maria');
  await page.getByRole('button', { name: 'Salvar' }).click();

  // Aguarda componente lazy carregar
  await expect(page.getByText('Perfil atualizado!')).toBeVisible();
});

// Cypress
it('deve exibir modal após submit', () => {
  cy.visit('/profile');
  cy.get('[data-cy="name-input"]').type('Maria');
  cy.get('[data-cy="submit-btn"]').click();
  cy.get('[data-cy="success-modal"]', { timeout: 8000 }).should('be.visible');
});

Asserções poderosas:

// Playwright
await expect(page.locator('h1')).toHaveText('Dashboard');
await expect(page.locator('.user-card')).toHaveCount(5);
await expect(page).toHaveScreenshot('dashboard.png');

// Cypress
cy.get('h1').should('have.text', 'Dashboard');
cy.get('.user-card').should('have.length', 5);
cy.get('input').should('have.value', 'usuario@exemplo.com');

5. Mocking de APIs e Dados Externos

Playwright - interceptação de rede

test('exibir estado de carregamento e erro', async ({ page }) => {
  // Mock de resposta lenta
  await page.route('**/api/users', async route => {
    await new Promise(resolve => setTimeout(resolve, 2000));
    await route.fulfill({ status: 200, body: JSON.stringify({ users: [] }) });
  });

  await page.goto('/users');
  await expect(page.getByTestId('loading')).toBeVisible();
  await expect(page.getByTestId('loading')).toBeHidden();
});

// Mock de erro
await page.route('**/api/login', async route => {
  await route.fulfill({ status: 401, body: 'Unauthorized' });
});

Cypress - interceptação de requisições

it('testar fluxo com dados mockados', () => {
  cy.intercept('GET', '/api/products', {
    fixture: 'products.json'
  }).as('getProducts');

  cy.intercept('POST', '/api/checkout', {
    statusCode: 200,
    body: { orderId: '123' }
  }).as('checkout');

  cy.visit('/products');
  cy.wait('@getProducts');
  cy.get('[data-cy="buy-button"]').first().click();
  cy.wait('@checkout');
  cy.contains('Pedido confirmado!').should('be.visible');
});

Mocking GraphQL:

// Playwright
await page.route('**/graphql', async route => {
  const request = route.request();
  if (request.postDataJSON().operationName === 'GetUser') {
    await route.fulfill({
      data: { user: { id: 1, name: 'João' } }
    });
  }
});

6. Gerenciamento de Estado e Dados de Teste

Fixtures com Faker.js

// fixtures/userFactory.js
const { faker } = require('@faker-js/faker');

export function createUser() {
  return {
    name: faker.person.fullName(),
    email: faker.internet.email(),
    password: faker.internet.password(),
  };
}

// Playwright test
test('registrar novo usuário', async ({ page }) => {
  const user = createUser();
  await page.goto('/register');
  await page.getByLabel('Nome').fill(user.name);
  await page.getByLabel('Email').fill(user.email);
  await page.getByLabel('Senha').fill(user.password);
  await page.getByRole('button', { name: 'Criar conta' }).click();
  await expect(page.getByText('Conta criada!')).toBeVisible();
});

Limpeza de dados entre testes

// Playwright - global setup
// global-setup.js
const { request } = require('@playwright/test');
export default async () => {
  const api = await request.newContext();
  await api.post('/api/reset-database');
};

// playwright.config.js
globalSetup: './global-setup.js',

// Cypress - hook before
beforeEach(() => {
  cy.request('POST', '/api/reset-test-data');
  cy.setCookie('session_id', 'test-session-123');
});

7. Execução em Pipeline CI/CD

GitHub Actions com Playwright

# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run build
      - run: npx playwright test --shard=${{ matrix.shard }}/3
      - uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-report
          path: playwright-report/

GitLab CI com Cypress

# .gitlab-ci.yml
e2e-tests:
  image: cypress/browsers:latest
  script:
    - npm ci
    - npm run build
    - npx cypress run --parallel --record --key $CYPRESS_RECORD_KEY
  artifacts:
    when: always
    paths:
      - cypress/videos/
      - cypress/screenshots/

Dicas para CI:
- Use --workers 4 no Playwright para paralelização
- Ative gravação de vídeo apenas em falhas: video: 'on-first-retry'
- Gere relatórios HTML: npx playwright show-report

8. Boas Práticas e Comparação Final

Page Object Model

// pages/LoginPage.js (Playwright)
export class LoginPage {
  constructor(page) {
    this.page = page;
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Senha');
    this.submitButton = page.getByRole('button', { name: 'Entrar' });
  }

  async login(email, password) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }
}

// Test
test('login com Page Object', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await page.goto('/login');
  await loginPage.login('user@test.com', '123456');
  await expect(page).toHaveURL('/dashboard');
});

Quando escolher Playwright vs Cypress

Critério Playwright Cypress
Navegadores Chromium, Firefox, WebKit, Edge Chromium, Firefox, Edge
Paralelismo nativo Sim (workers) Sim (dashboard pago)
Testes mobile Emulação de dispositivos Limitado
Debugging Trace Viewer, VS Code Time-travel, GUI interativa
Comunidade Crescendo rápido Maturidade consolidada
Curva de aprendizado Moderada Baixa

Recomendação: Use Playwright se precisar de testes cross-browser robustos, paralelização gratuita ou testes mobile. Escolha Cypress se valorizar uma experiência de desenvolvimento interativa, time-travel debugging e integração fácil com React.

Manutenção de testes E2E

  • Evite fragilidade: Prefira getByRole a seletores CSS aninhados
  • Reduza duplicação: Crie custom commands e Page Objects
  • Documente fluxos críticos: Mantenha um README com cenários cobertos
  • Execute testes regularmente: Integre no CI e monitore relatórios
  • Use dados isolados: Nunca dependa de dados de produção

Referências