<h2>Entendendo Playwright e sua Integração com React</h2>
<p>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.</p>
<p>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.</p>
<h3>Instalação e Configuração Inicial</h3>
<p>Para começar, você precisa instalar o Playwright no seu projeto React. Abra o terminal na raiz do projeto e execute:</p>
<pre><code class="language-bash">npm install -D @playwright/test
npx playwright install</code></pre>
<p>O comando <code>playwright install</code> baixa os navegadores necessários. Isso é crítico — sem essa etapa, os testes não funcionarão.</p>
<p>Agora crie um arquivo de configuração <code>playwright.config.ts</code> na raiz do seu projeto:</p>
<pre><code class="language-typescript">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'] },
},
],
});</code></pre>
<p>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 <code>baseURL</code> economiza digitação — você não precisa escrever <code>http://localhost:3000</code> em cada teste.</p>
<h2>Testes End-to-End (E2E) com Playwright</h2>
<p>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.</p>
<h3>Estruturando seus Primeiros Testes</h3>
<p>Crie um diretório <code>e2e</code> na raiz do projeto. Lá, você colocará seus testes. Vamos começar com um exemplo simples: uma página de login.</p>
<p>Suponha que sua aplicação React tenha uma página <code>/login</code> com um formulário básico:</p>
<pre><code class="language-typescript">// 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>
);
}</code></pre>
<p>Agora, crie o teste E2E em <code>e2e/login.spec.ts</code>:</p>
<pre><code class="language-typescript">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');
});
});</code></pre>
<p>Note que usamos <code>data-testid</code> 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.</p>
<h3>Usando Locadores Eficientemente</h3>
<p>Playwright oferece várias formas de localizar elementos. As mais robustas são:</p>
<pre><code class="language-typescript">// 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')</code></pre>
<p>O <code>getByRole</code> é 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:</p>
<pre><code class="language-typescript">// 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)')</code></pre>
<h2>Testes de Regressão Visual</h2>
<p>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.</p>
<h3>Como Funciona Visual Regression</h3>
<p>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.</p>
<pre><code class="language-typescript">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');
});
});</code></pre>
<p>Execute os testes para gerar as imagens de referência:</p>
<pre><code class="language-bash">npx playwright test --update-snapshots</code></pre>
<p>Nas execuções posteriores, qualquer diferença será detectada:</p>
<pre><code class="language-bash">npx playwright test</code></pre>
<p>Se houver diferenças, o Playwright gera um relatório HTML mostrando lado-a-lado:</p>
<pre><code class="language-bash">npx playwright show-report</code></pre>
<h3>Gerenciando Imagens de Referência</h3>
<p>As imagens são armazenadas em <code>e2e/{test-name}.spec.ts-snapshots/</code>. Commit essas imagens no Git — são parte do seu teste. Quando a mudança é intencional (novo design), regenere com <code>--update-snapshots</code>.</p>
<p>Um problema comum: testes visuais são sensíveis a detalhes do ambiente (fonte renderizada, anti-aliasing). Para reduzir falsos positivos, configure tolerância:</p>
<pre><code class="language-typescript">await expect(page).toHaveScreenshot('login-page.png', {
maxDiffPixels: 100, // Permite até 100 pixels diferentes
threshold: 0.2, // Permite até 20% de diferença
});</code></pre>
<h2>Testes de Componentes com Playwright</h2>
<p>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.</p>
<h3>Configurando Component Testing</h3>
<p>Primeiro, instale o adapter do Playwright para React:</p>
<pre><code class="language-bash">npm install -D @playwright/experimental-ct-react</code></pre>
<p>Crie um arquivo <code>playwright-ct.config.ts</code>:</p>
<pre><code class="language-typescript">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'] },
},
],
});</code></pre>
<h3>Testando Componentes Isoladamente</h3>
<p>Digamos que você tenha um componente Button reutilizável:</p>
<pre><code class="language-typescript">// 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>
);
}</code></pre>
<p>Crie o teste de componente em <code>src/components/Button.spec.tsx</code>:</p>
<pre><code class="language-typescript">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');
});
});</code></pre>
<p>Note a diferença: em component testing, você usa <code>mount()</code> em vez de <code>page.goto()</code>. O componente é renderizado em isolamento, sem toda a aplicação. Isso torna os testes mais rápidos.</p>
<h3>Testando Componentes com Context e Hooks</h3>
<p>Se seu componente depende de Context ou hooks customizados, você pode envolver o componente em providers:</p>
<pre><code class="language-typescript">// 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();
});</code></pre>
<h2>Boas Práticas e Padrões Avançados</h2>
<h3>Executando Testes em CI/CD</h3>
<p>Em pipelines (GitHub Actions, GitLab CI, etc.), configure o Playwright para rodar em modo headless com retry:</p>
<pre><code class="language-yaml"># .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/</code></pre>
<h3>Padrão Page Object</h3>
<p>Em E2E, use Page Objects para abstrair seletores e ações. Isso reduz duplicação e facilita manutenção:</p>
<pre><code class="language-typescript">// 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');
});</code></pre>
<h3>Testando Requisições HTTP</h3>
<p>O Playwright intercepta requisições HTTP, permitindo validar chamadas à API:</p>
<pre><code class="language-typescript">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',
});
});</code></pre>
<p>Também é possível mockar respostas:</p>
<pre><code class="language-typescript">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');
});</code></pre>
<h2>Conclusão</h2>
<p>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.</p>
<p>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.</p>
<h2>Referências</h2>
<ol>
<li><strong>Playwright Official Documentation</strong> — https://playwright.dev/</li>
<li><strong>Playwright Component Testing Guide</strong> — https://playwright.dev/docs/test-components</li>
<li><strong>React Testing Best Practices with Playwright</strong> — https://playwright.dev/docs/frameworks</li>
<li><strong>W3C WebDriver Protocol</strong> — https://www.w3.org/TR/webdriver/</li>
<li><strong>Testing Library — Accessibility-first queries</strong> — https://testing-library.com/</li>
</ol>
<p><!-- FIM --></p>