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
- Playwright Official Documentation — https://playwright.dev/
- Playwright Component Testing Guide — https://playwright.dev/docs/test-components
- React Testing Best Practices with Playwright — https://playwright.dev/docs/frameworks
- W3C WebDriver Protocol — https://www.w3.org/TR/webdriver/
- Testing Library — Accessibility-first queries — https://testing-library.com/