Como Usar Playwright com React: E2E, Visual Regression e Component Testing em Produção Já leu

Entendendo Playwright e sua Integração com React Playwright é uma framework de automação moderna que permite controlar navegadores (Chrome, Firefox, Safari) de forma programática. Diferente de ferramentas legadas, ela foi construída desde o início com foco em confiabilidade, velocidade e suporte a múltiplos navegadores. Quando aplicada em projetos React, o Playwright oferece três tipos de testes complementares: testes end-to-end (E2E), regressão visual e testes de componentes — cada um resolvendo um problema específico. A razão pela qual Playwright se destaca para React é sua capacidade de interagir com o DOM da mesma forma que um usuário real faria, sem depender de detalhes de implementação. Isso significa que seus testes continuam válidos mesmo quando você refatora o código interno do componente, desde que o comportamento visível permaneça o mesmo. Além disso, o Playwright oferece isolamento de contexto (múltiplas abas e janelas em paralelo) e espera inteligente de elementos, reduzindo flakiness — testes que falham aleatoriamente. Instalação e Configuração Inicial Para começar,

Entendendo Playwright e sua Integração com React

Playwright é uma framework de automação moderna que permite controlar navegadores (Chrome, Firefox, Safari) de forma programática. Diferente de ferramentas legadas, ela foi construída desde o início com foco em confiabilidade, velocidade e suporte a múltiplos navegadores. Quando aplicada em projetos React, o Playwright oferece três tipos de testes complementares: testes end-to-end (E2E), regressão visual e testes de componentes — cada um resolvendo um problema específico.

A razão pela qual Playwright se destaca para React é sua capacidade de interagir com o DOM da mesma forma que um usuário real faria, sem depender de detalhes de implementação. Isso significa que seus testes continuam válidos mesmo quando você refatora o código interno do componente, desde que o comportamento visível permaneça o mesmo. Além disso, o Playwright oferece isolamento de contexto (múltiplas abas e janelas em paralelo) e espera inteligente de elementos, reduzindo flakiness — testes que falham aleatoriamente.

Instalação e Configuração Inicial

Para começar, você precisa instalar o Playwright no seu projeto React. Abra o terminal na raiz do projeto e execute:

npm install -D @playwright/test
npx playwright install

O comando playwright install baixa os navegadores necessários. Isso é crítico — sem essa etapa, os testes não funcionarão.

Agora crie um arquivo de configuração playwright.config.ts na raiz do seu projeto:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
});

Este arquivo configura três coisas essenciais: a URL base da aplicação, o servidor de desenvolvimento (que será iniciado automaticamente), e os navegadores em que os testes rodará. A opção baseURL economiza digitação — você não precisa escrever http://localhost:3000 em cada teste.

Testes End-to-End (E2E) com Playwright

Testes E2E simulam fluxos reais de usuários. Você clica em botões, preenche formulários, navega entre páginas e valida que o resultado corresponde às expectativas. Diferente de testes unitários, E2E não se importa com detalhes internos — apenas com o que o usuário vê e interage.

Estruturando seus Primeiros Testes

Crie um diretório e2e na raiz do projeto. Lá, você colocará seus testes. Vamos começar com um exemplo simples: uma página de login.

Suponha que sua aplicação React tenha uma página /login com um formulário básico:

// src/pages/Login.tsx
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';

export function Login() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const navigate = useNavigate();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!email || !password) {
      setError('Email e senha são obrigatórios');
      return;
    }

    // Simula chamada à API
    if (email === 'user@example.com' && password === 'password123') {
      localStorage.setItem('token', 'fake-token');
      navigate('/dashboard');
    } else {
      setError('Credenciais inválidas');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        placeholder="Email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        data-testid="email-input"
      />
      <input
        type="password"
        placeholder="Senha"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        data-testid="password-input"
      />
      <button type="submit" data-testid="submit-button">
        Entrar
      </button>
      {error && <div data-testid="error-message">{error}</div>}
    </form>
  );
}

Agora, crie o teste E2E em e2e/login.spec.ts:

import { test, expect } from '@playwright/test';

test.describe('Login Flow', () => {
  test('deve exibir erro quando credenciais inválidas', async ({ page }) => {
    // Navega até a página de login
    await page.goto('/login');

    // Preenche os campos com dados inválidos
    await page.fill('[data-testid="email-input"]', 'wrong@example.com');
    await page.fill('[data-testid="password-input"]', 'wrongpassword');

    // Clica no botão de submit
    await page.click('[data-testid="submit-button"]');

    // Valida que a mensagem de erro apareceu
    const errorMessage = page.locator('[data-testid="error-message"]');
    await expect(errorMessage).toContainText('Credenciais inválidas');
  });

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

    // Preenche com credenciais válidas
    await page.fill('[data-testid="email-input"]', 'user@example.com');
    await page.fill('[data-testid="password-input"]', 'password123');
    await page.click('[data-testid="submit-button"]');

    // Valida redirecionamento
    await expect(page).toHaveURL('/dashboard');

    // Valida que o token foi armazenado
    const token = await page.evaluate(() => localStorage.getItem('token'));
    expect(token).toBe('fake-token');
  });

  test('deve desabilitar submit se campos vazios', async ({ page }) => {
    await page.goto('/login');

    // Não preenche nada, apenas clica
    await page.click('[data-testid="submit-button"]');

    // Valida a mensagem de erro
    const errorMessage = page.locator('[data-testid="error-message"]');
    await expect(errorMessage).toContainText('obrigatórios');
  });
});

Note que usamos data-testid para identificar elementos. Isso é uma boa prática — você não depende de classes CSS que mudam frequentemente. O Playwright aguarda automaticamente que o elemento esteja visível antes de interagir, reduzindo falsos negativos.

Usando Locadores Eficientemente

Playwright oferece várias formas de localizar elementos. As mais robustas são:

// data-testid é a mais confiável
page.locator('[data-testid="button"]')

// CSS selectors funcionam bem
page.locator('button.primary')

// Role-based (acessibilidade) é muito bom
page.getByRole('button', { name: 'Submit' })

// Label
page.getByLabel('Email')

// Placeholder
page.getByPlaceholder('Digite seu email')

O getByRole é especialmente poderoso porque força você a criar componentes acessíveis. Se um botão não tem role correto, o teste falha — e isso é uma vitória para acessibilidade:

// Bom: uso de role semanticamente correto
await page.getByRole('button', { name: /entrar/i }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com');

// Evite quando possível: muito específico de implementação
await page.locator('div.login-form > input:nth-child(1)')

Testes de Regressão Visual

Regressão visual detecta mudanças não intencionais no design. Alguém muda uma cor CSS, e o layout quebra sutilmente — testes visuais pegam isso. Playwright captura screenshots em momentos críticos e compara com screenshots de referência.

Como Funciona Visual Regression

O Playwright tira uma screenshot e a compara pixel-por-pixel com uma imagem armazenada. Na primeira execução, cria a imagem de referência. Nas execuções seguintes, detecta diferenças. Se houver mudanças, gera um relatório visual mostrando o antes, depois e a diferença.

import { test, expect } from '@playwright/test';

test.describe('Visual Regression', () => {
  test('página de login deve estar visualmente correta', async ({ page }) => {
    await page.goto('/login');

    // Aguarda a página carregar completamente
    await page.waitForLoadState('networkidle');

    // Tira screenshot da página inteira
    await expect(page).toHaveScreenshot('login-page.png');
  });

  test('componente de formulário deve renderizar corretamente', async ({ page }) => {
    await page.goto('/login');

    // Tira screenshot apenas de um elemento específico
    const form = page.locator('form');
    await expect(form).toHaveScreenshot('login-form.png');
  });

  test('estado de hover deve estar correto', async ({ page }) => {
    await page.goto('/login');

    const button = page.locator('[data-testid="submit-button"]');

    // Hover no botão
    await button.hover();

    // Captura screenshot com o estado de hover
    await expect(button).toHaveScreenshot('button-hover.png');
  });
});

Execute os testes para gerar as imagens de referência:

npx playwright test --update-snapshots

Nas execuções posteriores, qualquer diferença será detectada:

npx playwright test

Se houver diferenças, o Playwright gera um relatório HTML mostrando lado-a-lado:

npx playwright show-report

Gerenciando Imagens de Referência

As imagens são armazenadas em e2e/{test-name}.spec.ts-snapshots/. Commit essas imagens no Git — são parte do seu teste. Quando a mudança é intencional (novo design), regenere com --update-snapshots.

Um problema comum: testes visuais são sensíveis a detalhes do ambiente (fonte renderizada, anti-aliasing). Para reduzir falsos positivos, configure tolerância:

await expect(page).toHaveScreenshot('login-page.png', {
  maxDiffPixels: 100,  // Permite até 100 pixels diferentes
  threshold: 0.2,       // Permite até 20% de diferença
});

Testes de Componentes com Playwright

Component testing é diferente de E2E. Você testa um componente React isoladamente, sem o servidor web completo. É como um teste unitário visual — rápido, focado e determinístico.

Configurando Component Testing

Primeiro, instale o adapter do Playwright para React:

npm install -D @playwright/experimental-ct-react

Crie um arquivo playwright-ct.config.ts:

import { defineConfig, devices } from '@playwright/experimental-ct-react';

export default defineConfig({
  testDir: './src/components',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  webServer: undefined,
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
  ],
});

Testando Componentes Isoladamente

Digamos que você tenha um componente Button reutilizável:

// src/components/Button.tsx
import React from 'react';
import './Button.css';

interface ButtonProps {
  onClick?: () => void;
  disabled?: boolean;
  children: React.ReactNode;
  variant?: 'primary' | 'secondary';
}

export function Button({
  onClick,
  disabled = false,
  children,
  variant = 'primary',
}: ButtonProps) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`button button--${variant}`}
      data-testid="button"
    >
      {children}
    </button>
  );
}

Crie o teste de componente em src/components/Button.spec.tsx:

import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';

test.describe('Button Component', () => {
  test('deve renderizar com o texto correto', async ({ mount }) => {
    const component = await mount(<Button>Clique aqui</Button>);

    const button = component.locator('[data-testid="button"]');
    await expect(button).toContainText('Clique aqui');
  });

  test('deve chamar onClick quando clicado', async ({ mount }) => {
    let clicked = false;

    const component = await mount(
      <Button onClick={() => { clicked = true; }}>
        Clique
      </Button>
    );

    const button = component.locator('[data-testid="button"]');
    await button.click();

    expect(clicked).toBe(true);
  });

  test('deve estar desabilitado quando prop disabled é true', async ({ mount }) => {
    const component = await mount(
      <Button disabled onClick={() => {}}>
        Desabilitado
      </Button>
    );

    const button = component.locator('[data-testid="button"]');
    await expect(button).toBeDisabled();
  });

  test('deve aplicar classe de variante corretamente', async ({ mount }) => {
    const component = await mount(
      <Button variant="secondary">Secondary</Button>
    );

    const button = component.locator('[data-testid="button"]');
    await expect(button).toHaveClass(/button--secondary/);
  });

  test('visual: botão primário deve estar correto', async ({ mount }) => {
    const component = await mount(<Button variant="primary">Primary</Button>);

    await expect(component).toHaveScreenshot('button-primary.png');
  });

  test('visual: botão desabilitado deve estar correto', async ({ mount }) => {
    const component = await mount(
      <Button disabled variant="primary">Disabled</Button>
    );

    await expect(component).toHaveScreenshot('button-disabled.png');
  });
});

Note a diferença: em component testing, você usa mount() em vez de page.goto(). O componente é renderizado em isolamento, sem toda a aplicação. Isso torna os testes mais rápidos.

Testando Componentes com Context e Hooks

Se seu componente depende de Context ou hooks customizados, você pode envolver o componente em providers:

// src/components/UserCard.spec.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { UserCard } from './UserCard';
import { UserProvider } from '../contexts/UserContext';

test('deve exibir informações do usuário do context', async ({ mount }) => {
  const component = await mount(
    <UserProvider initialUser={{ name: 'João', email: 'joao@example.com' }}>
      <UserCard />
    </UserProvider>
  );

  await expect(component.locator('text=João')).toBeVisible();
  await expect(component.locator('text=joao@example.com')).toBeVisible();
});

Boas Práticas e Padrões Avançados

Executando Testes em CI/CD

Em pipelines (GitHub Actions, GitLab CI, etc.), configure o Playwright para rodar em modo headless com retry:

# .github/workflows/test.yml
name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18'

      - run: npm ci
      - run: npx playwright install --with-deps

      - run: npm run test:e2e
      - run: npm run test:component

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

Padrão Page Object

Em E2E, use Page Objects para abstrair seletores e ações. Isso reduz duplicação e facilita manutenção:

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

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

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

  async fillEmail(email: string) {
    await this.page.fill('[data-testid="email-input"]', email);
  }

  async fillPassword(password: string) {
    await this.page.fill('[data-testid="password-input"]', password);
  }

  async clickSubmit() {
    await this.page.click('[data-testid="submit-button"]');
  }

  async getErrorMessage() {
    return this.page.locator('[data-testid="error-message"]').textContent();
  }
}

// e2e/login.spec.ts — muito mais limpo
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

test('login com credenciais inválidas', async ({ page }) => {
  const loginPage = new LoginPage(page);

  await loginPage.goto();
  await loginPage.fillEmail('wrong@example.com');
  await loginPage.fillPassword('wrong');
  await loginPage.clickSubmit();

  const errorMessage = await loginPage.getErrorMessage();
  expect(errorMessage).toContain('inválidas');
});

Testando Requisições HTTP

O Playwright intercepta requisições HTTP, permitindo validar chamadas à API:

test('deve enviar dados corretos para API ao fazer login', async ({ page }) => {
  await page.goto('/login');

  // Aguarda por uma requisição POST e valida seu payload
  const requestPromise = page.waitForRequest(
    request => request.url().includes('/api/login') && request.method() === 'POST'
  );

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

  const request = await requestPromise;
  const postData = request.postDataJSON();

  expect(postData).toEqual({
    email: 'user@example.com',
    password: 'password123',
  });
});

Também é possível mockar respostas:

test('deve exibir erro quando API falha', async ({ page }) => {
  await page.goto('/login');

  // Mocka a resposta da API
  await page.route('**/api/login', route => {
    route.abort('failed');
  });

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

  const errorMessage = page.locator('[data-testid="error-message"]');
  await expect(errorMessage).toContainText('Erro ao conectar');
});

Conclusão

Playwright é extraordinariamente poderoso para testar aplicações React em múltiplas dimensões. Primeiro, testes E2E garantem que fluxos completos funcionam do ponto de vista do usuário — você valida comportamentos reais, não implementações. Segundo, testes de regressão visual pegam mudanças acidentais no design, coisa que testes unitários jamais fariam. Terceiro, testes de componentes oferecem rapidez e isolamento, testando peças individualmente antes de juntá-las.

O segredo está em combinar estrategicamente: use E2E para jornadas críticas do usuário (login, checkout), visual regression para componentes visuais importantes, e component tests para lógica de componentes reutilizáveis. Essa tríade, bem executada, oferece cobertura confiável sem overhead excessivo. E não esqueça: data-testid, Page Objects e retry policies inteligentes transformam testes flaky em suites robustas.

Referências

  1. Playwright Official Documentation — https://playwright.dev/
  2. Playwright Component Testing Guide — https://playwright.dev/docs/test-components
  3. React Testing Best Practices with Playwright — https://playwright.dev/docs/frameworks
  4. W3C WebDriver Protocol — https://www.w3.org/TR/webdriver/
  5. Testing Library — Accessibility-first queries — https://testing-library.com/

Artigos relacionados