Next.js: Testes E2E: Playwright
Última atualização: 2026-08-26
Testes E2E são uma "simulação de ponta a ponta" que imitam interações reais do usuário — eles mostram exatamente o quão bem sua aplicação funciona em um navegador.
1. O Que Você Vai Aprender
- Configurar o Playwright e fazer o servidor de desenvolvimento Next.js iniciar automaticamente (configuração
webServer) - Projetar código de teste de fácil manutenção usando o Page Object Model (POM)
- Desenvolver testes de caminho crítico: navegação de páginas, envio de formulários e fluxo de autenticação
- Usar
toHaveScreenshot()para testes de regressão visual - Implementar testes isolados interceptando requisições de API via
page.route() - Integrar o Playwright em um pipeline de CI do GitHub Actions
2. Uma História Real de uma Engenheira de Testes
(1) Ponto Problemático: Cobertura de testes manuais incompleta e incidentes frequentes em produção
Diana é engenheira de testes na equipe do TaskFlow. A equipe mantém uma plataforma SaaS que atende mais de 10.000 usuários e, antes de cada release, três engenheiros de QA precisam testar manualmente mais de 200 casos de teste — um processo que leva dois dias inteiros.
No release da semana passada, o recurso "Editar Nome do Projeto" funcionou bem no Chrome, mas o campo de entrada não podia ser focado no Safari. A equipe de QA testou apenas no Chrome e, após o release, receberam mais de 50 reclamações.
O problema dos testes manuais é evidente:
| Problema | Impacto |
|---|---|
| Cobertura de navegadores incompleta | Testado apenas no Chrome; Safari e Firefox foram omitidos |
| Ciclo de desenvolvimento longo | Cada release exige 2 dias de testes manuais |
| Resultados inconsistentes | Testadores diferentes usam métodos diferentes |
| Incapacidade de reproduzir | Necessidade de retestar após a correção do bug |
(2) Solução com Playwright E2E
Diana introduziu o Playwright para substituir operações manuais por código:
test('usuário pode criar um projeto', async ({ page }) => {
await page.goto('/projects')
await page.getByRole('button', { name: 'Novo Projeto' }).click()
await page.getByPlaceholder('Nome do projeto').fill('App E-commerce')
await page.getByRole('button', { name: 'Criar' }).click()
await expect(page.getByText('App E-commerce')).toBeVisible()
})
(3) Resultados
| Dimensão | Teste Manual | Playwright E2E |
|---|---|---|
| Duração do Teste | 2 dias | 15 minutos |
| Cobertura de Navegadores | Apenas Chrome | Chrome + Firefox + Safari + Edge |
| Ciclo de Restauração | Restauração Completa a Cada Vez | Restauração Incremental + CI Automático |
| Taxa de Escape de Bugs | 15% | < 2% |
| Repetibilidade do Teste | Baixa (variação humana) | Alta (100% de consistência) |
3. Configurando o Ambiente Playwright
Playwright é um framework de teste automatizado multi-navegador mantido pela Microsoft, com suporte a Chromium, Firefox e WebKit.
graph TB
A[playwright.config.ts] --> B[Configuração webServer]
A --> C[Configurações do Navegador]
A --> D[Diretório de Testes]
B --> E[Inicialização Automática npm run dev]
B --> F[Escuta na porta 3000 Disponível]
C --> G[Chromium / Firefox / WebKit]
C --> H[Viewport 1280x720]
style A fill:#cce5ff
style B fill:#d4edda
| Arquivo de Configuração | Propósito | Opções Principais |
|---|---|---|
playwright.config.ts |
Configuração Principal do E2E | webServer Gerenciamento Automatizado do Ciclo de Vida |
playwright/index.html |
Portal de Testes de Componentes | Configuração ct.target |
.github/workflows/playwright.yml |
Integração CI | npx playwright install --with-deps |
(1) Instalação e Inicialização
npm init playwright@latest
# Selecione:
# ✓ TypeScript
# ✓ Testes E2E
# ✓ Adicionar workflow do GitHub Actions
# ✓ Instalar navegadores (Chromium, Firefox, WebKit)
(2) Configuração Principal do playwright.config.ts
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'],
['json', { outputFile: 'playwright-report/results.json' }]
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
}
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 30000
}
})
(3) Estrutura de Diretórios
├── e2e/
│ ├── auth.setup.ts # Pré-processamento de Autenticação
│ ├── models/
│ │ ├── LoginPage.ts # POM da Página de Login
│ │ ├── ProjectPage.ts # POM da Página de Projetos
│ │ └── TaskPage.ts # POM da Página de Tarefas
│ ├── navigation.spec.ts # Teste de Navegação
│ ├── auth.spec.ts # Teste de Autenticação
│ ├── projects.spec.ts # CRUD de Projetos
│ └── visual.spec.ts # Regressão Visual
├── playwright.config.ts
├── playwright-report/
└── test-results/
▶ Exemplo: Verificando se o Playwright está em execução
// e2e/example.spec.ts
import { test, expect } from '@playwright/test'
test('página inicial tem o título correto', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveTitle(/TaskFlow/)
await expect(page.locator('h1')).toContainText('Bem-vindo')
})
npx playwright test --project=chromium --headed
Running 1 test using 1 worker
✓ e2e/example.spec.ts:3:1 › página inicial tem o título correto (2.3s)
Saída:
Running 1 test using 1 worker
✓ e2e/example.spec.ts:3:1 › página inicial tem o título correto (2.3s)
1 passed (2.3s)
4. Padrão de Design Page Object Model (POM)
O POM encapsula a lógica de interação da página em classes separadas, para que o código de teste se concentre apenas em "o que fazer" em vez de "como fazer".
graph TB
A[Casos de Teste] --> B[Camada Page Object]
B --> C[LoginPage]
B --> D[ProjectsPage]
B --> E[TaskPage]
C --> F[API do Navegador]
D --> F
E --> F
style B fill:#cce5ff
style C fill:#d4edda
style D fill:#d4edda
style E fill:#d4edda
(1) Implementação do POM LoginPage
// e2e/models/LoginPage.ts
import { type Page, type Locator } from '@playwright/test'
export class LoginPage {
readonly page: Page
readonly emailInput: Locator
readonly passwordInput: Locator
readonly submitButton: Locator
readonly errorMessage: Locator
constructor(page: Page) {
this.page = page
this.emailInput = page.getByPlaceholder('Digite seu endereço de email')
this.passwordInput = page.getByPlaceholder('Digite sua senha')
this.submitButton = page.getByRole('button', { name: 'Entrar' })
this.errorMessage = page.getByTestId('login-error')
}
async goto() {
await this.page.goto('/login')
}
async login(email: string, password: string) {
await this.emailInput.fill(email)
await this.passwordInput.fill(password)
await this.submitButton.click()
}
async expectError(message: string) {
await expect(this.errorMessage).toContainText(message)
}
async expectLoggedIn() {
await expect(this.page).toHaveURL(/dashboard/)
}
}
(2) Implementação do POM ProjectsPage
// e2e/models/ProjectsPage.ts
import { type Page, type Locator } from '@playwright/test'
export class ProjectsPage {
readonly page: Page
readonly newProjectButton: Locator
readonly projectNameInput: Locator
readonly createButton: Locator
readonly projectList: Locator
readonly searchInput: Locator
constructor(page: Page) {
this.page = page
this.newProjectButton = page.getByRole('button', { name: 'Novo Projeto' })
this.projectNameInput = page.getByPlaceholder('Nome do projeto')
this.createButton = page.getByRole('button', { name: 'Criar' })
this.projectList = page.getByTestId('project-list')
this.searchInput = page.getByPlaceholder('Buscar Itens')
}
async goto() {
await this.page.goto('/projects')
}
async createProject(name: string) {
await this.newProjectButton.click()
await this.projectNameInput.fill(name)
await this.createButton.click()
}
async searchProject(name: string) {
await this.searchInput.fill(name)
}
async expectProjectVisible(name: string) {
await expect(this.projectList).toContainText(name)
}
async openProject(name: string) {
await this.page.getByRole('link', { name }).first().click()
}
}
▶ Exemplo: Testes Orientados a POM
Saída:
Define tipo(s) TypeScript: Page, Locator.
// e2e/projects.spec.ts
import { test, expect } from '@playwright/test'
import { LoginPage } from './models/LoginPage'
import { ProjectsPage } from './models/ProjectsPage'
test.describe('Gerenciamento de Projetos', () => {
let loginPage: LoginPage
let projectsPage: ProjectsPage
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page)
projectsPage = new ProjectsPage(page)
await loginPage.goto()
await loginPage.login('alice@exemplo.com', 'senha123')
await loginPage.expectLoggedIn()
})
test('criar um novo projeto', async () => {
await projectsPage.goto()
await projectsPage.createProject('Painel E-commerce')
await projectsPage.expectProjectVisible('Painel E-commerce')
})
test('buscar um projeto existente', async () => {
await projectsPage.goto()
await projectsPage.searchProject('E-commerce')
await projectsPage.expectProjectVisible('Painel E-commerce')
})
})
Saída:
Código TypeScript executado com sucesso.
5. Testes de Caminho Crítico
O caminho crítico é o fluxo de trabalho de negócios mais frequentemente usado pelos usuários e deve ser priorizado.
(1) Teste do Fluxo de Autenticação
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Fluxo de Autenticação', () => {
test('redireciona usuário não autenticado para o login', async ({ page }) => {
await page.goto('/dashboard')
await expect(page).toHaveURL(/login/)
})
test('exibe erros de validação em formulário vazio', async ({ page }) => {
await page.goto('/login')
await page.getByRole('button', { name: 'Entrar' }).click()
await expect(page.getByText(/Por favor, digite seu endereço de email/i)).toBeVisible()
await expect(page.getByText(/Por favor, digite sua senha/i)).toBeVisible()
})
test('login bem-sucedido redireciona para o dashboard', async ({ page }) => {
await page.goto('/login')
await page.getByPlaceholder('Digite seu endereço de email').fill('alice@exemplo.com')
await page.getByPlaceholder('Digite sua senha').fill('senha-correta')
await page.getByRole('button', { name: 'Entrar' }).click()
await expect(page).toHaveURL(/dashboard/)
await expect(page.getByText(/Bem-vinda de volta, Alice/i)).toBeVisible()
})
test('logout limpa a sessão', async ({ page }) => {
await page.goto('/login')
await page.getByPlaceholder('Digite seu endereço de email').fill('alice@exemplo.com')
await page.getByPlaceholder('Digite sua senha').fill('senha-correta')
await page.getByRole('button', { name: 'Entrar' }).click()
await page.getByRole('button', { name: /Sair/i }).click()
await expect(page).toHaveURL(/login/)
})
})
(2) Teste de Envio de Formulário
// e2e/task-creation.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Fluxo de Criação de Tarefa', () => {
test.beforeEach(async ({ page }) => {
// Faça login e vá para o projeto
await page.goto('/login')
await page.getByPlaceholder('Digite seu endereço de email').fill('alice@exemplo.com')
await page.getByPlaceholder('Digite sua senha').fill('senha123')
await page.getByRole('button', { name: 'Entrar' }).click()
await page.goto('/projects/p1')
})
test('cria uma tarefa com todos os campos', async ({ page }) => {
await page.getByRole('button', { name: 'Adicionar Tarefa' }).click()
await page.getByPlaceholder('Título da tarefa').fill('Implementar autenticação de usuário')
await page.getByLabel('Prioridade').selectOption('Alta')
await page.getByLabel('Responsável').selectOption('Bob')
await page.getByRole('button', { name: 'Salvar' }).click()
await expect(page.getByText('Implementar autenticação de usuário')).toBeVisible()
})
test('exibe erro para título vazio', async ({ page }) => {
await page.getByRole('button', { name: 'Adicionar Tarefa' }).click()
await page.getByRole('button', { name: 'Salvar' }).click()
await expect(page.getByText(/O título não pode ficar em branco./i)).toBeVisible()
})
})
(3) Teste de Navegação e Roteamento
// e2e/navigation.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Fluxo de Navegação', () => {
test('links da barra lateral navegam corretamente', async ({ page }) => {
await page.goto('/login')
await page.getByPlaceholder('Digite seu endereço de email').fill('alice@exemplo.com')
await page.getByPlaceholder('Digite sua senha').fill('senha123')
await page.getByRole('button', { name: 'Entrar' }).click()
await page.getByRole('link', { name: 'Projetos' }).click()
await expect(page).toHaveURL(/\/projects/)
await page.getByRole('link', { name: 'Dashboard' }).click()
await expect(page).toHaveURL(/\/dashboard/)
await page.getByRole('link', { name: 'Configurações' }).click()
await expect(page).toHaveURL(/\/settings/)
})
test('breadcrumb mostra a localização atual', async ({ page }) => {
await page.goto('/login')
await page.getByPlaceholder('Digite seu endereço de email').fill('alice@exemplo.com')
await page.getByPlaceholder('Digite sua senha').fill('senha123')
await page.getByRole('button', { name: 'Entrar' }).click()
await page.goto('/projects/p1/tasks/t1')
await expect(page.getByTestId('breadcrumb')).toContainText([
/Projetos/, /Projeto 1/, /Tarefa 1/
])
})
})
6. Testes de Regressão Visual
Testes de regressão visual capturam mudanças sutis no estilo da interface. O Playwright permite comparação em nível de pixel através de toHaveScreenshot().
graph LR
A[Execução do Teste] --> B[Tirar screenshot do estado atual]
B --> C{Comparação com a screenshot de referência}
C -->|Corresponde| D[Teste Aprovado]
C -->|Diferenças > Limiar| E[Teste Reprovado]
E --> F[Gerar Relatório de Variação]
style A fill:#cce5ff
style C fill:#fff3cd
style D fill:#d4edda
style E fill:#f8d7da
| Opção | Descrição | Valor Recomendado |
|---|---|---|
maxDiffPixels |
Número máximo de pixels diferentes | 100 |
maxDiffPixelRatio |
Proporção máxima de diferença | 0.01 |
threshold |
Limiar de Comparação de Pixels | 0.2 |
animations |
Desabilitar animações | 'disabled' |
stylePath |
Sobrescritas CSS Adicionais | Ocultar Elementos Aleatórios |
(1) Configurar Testes Visuais
// Trecho do playwright.config.ts
export default defineConfig({
use: {
screenshot: 'only-on-failure',
viewport: { width: 1280, height: 720 }
},
expect: {
toHaveScreenshot: {
maxDiffPixels: 100,
animations: 'disabled'
}
}
})
(2) Gerar uma screenshot de referência
# Gerar screenshot de linha de base na primeira execução
npx playwright test --update-snapshots
# Comparação das Execuções Subsequentes com a Linha de Base
npx playwright test
▶ Exemplo: Testes de Regressão Visual
Saída:
Running 5 tests using 1 worker
✓ navigation.spec.ts:3:1 › navega para a página sobre (1.2s)
✓ navigation.spec.ts:10:1 › exibe lista de produtos (0.8s)
✓ form.spec.ts:5:1 › envia formulário de contato (1.5s)
3 passed (3.5s)
Running 5 tests using 1 worker
✓ navigation.spec.ts:3:1 › navega para a página sobre (1.2s)
✓ navigation.spec.ts:10:1 › exibe lista de produtos (0.8s)
✓ form.spec.ts:5:1 › envia formulário de contato (1.5s)
3 passed (3.5s)
// e2e/visual.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Regressão Visual', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login')
await page.getByPlaceholder('Digite seu endereço de email').fill('alice@exemplo.com')
await page.getByPlaceholder('Digite sua senha').fill('senha123')
await page.getByRole('button', { name: 'Entrar' }).click()
})
test('página do dashboard corresponde ao snapshot', async ({ page }) => {
await page.goto('/dashboard')
await page.waitForLoadState('networkidle')
await expect(page).toHaveScreenshot('dashboard.png', {
fullPage: true
})
})
test('lista de projetos corresponde ao snapshot', async ({ page }) => {
await page.goto('/projects')
await page.waitForSelector('[data-testid="project-list"]')
await expect(page).toHaveScreenshot('projects.png')
})
test('detalhe da tarefa corresponde ao snapshot', async ({ page }) => {
await page.goto('/projects/p1/tasks/t1')
await page.waitForLoadState('networkidle')
await expect(page).toHaveScreenshot('task-detail.png', {
mask: [page.locator('[data-testid="timestamp"]')]
})
})
})
Saída:
Código TypeScript executado com sucesso.
7. Mock de API e Interceptação de Requisições
O page.route() do Playwright pode interceptar requisições de rede no nível do navegador, permitindo testar interações front-end/back-end sem um backend real.
(1) Fundamentos da Interceptação de Rotas
// Interceptação Global
await page.route('**/api/**', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, title: 'Tarefa Mockada' }])
})
})
// Interceptação de URL Específica
await page.route('https://api.exemplo.com/products*', async route => {
const response = await route.fetch() // Deixa Ir para a API Real
const body = await response.json()
body.push({ id: 999, name: 'Produto Injetado' })
await route.fulfill({ response, body: JSON.stringify(body) })
})
(2) Comparação de Padrões de Mock de API
| Padrão | Método | Cenários Aplicáveis |
|---|---|---|
| Mock Completo | route.fulfill() |
Testar o front-end quando o back-end não está pronto |
| Modificação por Proxy | route.fetch() + Modificação |
Injetar Dados de Teste/Erro |
| Passagem de Proxy | route.fetch() + route.fulfill({ response }) |
Ouvir requisições sem modificar os dados |
| Requisição Interrompida | route.abort() |
Testar Status Offline |
▶ Exemplo: Um Teste Completo com Mock de API
Saída:
Código TypeScript executado com sucesso.
// e2e/api-mock.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Cenários de Mock de API', () => {
test('exibe estado vazio quando não há dados', async ({ page }) => {
await page.route('**/api/projects', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([])
})
})
await page.goto('/projects')
await expect(page.getByText(/Nenhum projeto ainda/i)).toBeVisible()
})
test('trata erro 500 com elegância', async ({ page }) => {
await page.route('**/api/projects', async route => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Erro do servidor' })
})
})
await page.goto('/projects')
await expect(page.getByText(/Falha ao carregar/i)).toBeVisible()
})
test('exibe estado de carregamento e depois os dados', async ({ page }) => {
// Atrasa respostas para testar sob condições de carga
await page.route('**/api/projects', async route => {
await new Promise(resolve => setTimeout(resolve, 1000))
await route.fulfill({
status: 200,
body: JSON.stringify([
{ id: 'p1', name: 'Projeto Mock', taskCount: 5 }
])
})
})
await page.goto('/projects')
await expect(page.getByTestId('loading-skeleton')).toBeVisible()
await expect(page.getByText('Projeto Mock')).toBeVisible({ timeout: 5000 })
})
test('cenário de rede offline', async ({ page }) => {
await page.route('**/api/**', async route => {
await route.abort('internetdisconnected')
})
await page.goto('/projects')
await expect(page.getByText(/Falha na conexão de rede/i)).toBeVisible()
})
})
Saída:
Código TypeScript executado com sucesso.
8. Integração CI: GitHub Actions
O Playwright integra-se perfeitamente com GitHub Actions, executando automaticamente testes E2E a cada push.
# .github/workflows/playwright.yml
name: Testes Playwright
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chromium, firefox, webkit]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Instalar dependências
run: npm ci
- name: Cache dos navegadores Playwright
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }}
- name: Instalar navegadores Playwright
run: npx playwright install --with-deps ${{ matrix.browser }}
- name: Executar testes Playwright
run: npx playwright test --project=${{ matrix.browser }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report-${{ matrix.browser }}
path: playwright-report/
retention-days: 7
9. Exemplo Completo: Suíte E2E da Aplicação TaskFlow
// e2e/taskflow-e2e.spec.ts
// ============================================
// Teste E2E Geral: O Fluxo Completo do Usuário da Aplicação TaskFlow
// ============================================
import { test, expect, type Page } from '@playwright/test'
// --- Funções Utilitárias: Definir Status de Autenticação ---
async function setupAuth(page: Page) {
await page.goto('/login')
await page.getByPlaceholder('Digite seu endereço de email').fill('alice@taskflow.io')
await page.getByPlaceholder('Digite sua senha').fill('Senha123!')
await page.getByRole('button', { name: 'Entrar' }).click()
await expect(page).toHaveURL(/dashboard/)
}
test.describe('Suíte E2E TaskFlow', () => {
test.beforeEach(async ({ page }) => {
await setupAuth(page)
})
// --- 1. Carregamento do Dashboard ---
test('dashboard exibe métricas principais', async ({ page }) => {
await page.goto('/dashboard')
await expect(page.getByTestId('total-projects')).toBeVisible()
await expect(page.getByTestId('total-tasks')).toBeVisible()
await expect(page.getByTestId('team-members')).toBeVisible()
const projectCount = await page.getByTestId('total-projects').textContent()
expect(Number(projectCount)).toBeGreaterThan(0)
})
// --- 2. Fluxo CRUD de Projetos ---
test('ciclo de vida completo do projeto', async ({ page }) => {
// Criar
await page.goto('/projects')
await page.getByRole('button', { name: 'Novo Projeto' }).click()
await page.getByPlaceholder('Nome do projeto').fill('Projeto de Teste E2E')
await page.getByPlaceholder('Descrição').fill('Criado pelo Playwright')
await page.getByRole('button', { name: 'Criar' }).click()
await expect(page.getByText('Projeto de Teste E2E')).toBeVisible()
// Editar
await page.getByRole('button', { name: /Editar/i }).click()
await page.getByPlaceholder('Nome do projeto').fill('Projeto de Teste E2E v2')
await page.getByRole('button', { name: 'Salvar' }).click()
await expect(page.getByText('Projeto de Teste E2E v2')).toBeVisible()
// Excluir
await page.getByRole('button', { name: /Excluir/i }).click()
await page.getByRole('button', { name: /Confirmar/i }).click()
await expect(page.getByText('Projeto de Teste E2E v2')).not.toBeVisible()
})
// --- 3. Gerenciamento de Tarefas ---
test('arrastar e soltar muda o status da tarefa', async ({ page }) => {
await page.goto('/projects/p1')
// Criar tarefa
await page.getByRole('button', { name: 'Adicionar Tarefa' }).click()
await page.getByPlaceholder('Título da tarefa').fill('Configurar testes Playwright')
await page.getByText('A FAZER').click()
await page.getByRole('button', { name: 'Salvar' }).click()
// Verificar se a tarefa aparece na coluna A FAZER
await expect(page.getByTestId('column-todo'))
.toContainText('Configurar testes Playwright')
})
// --- 4. Função de Busca ---
test('busca filtra projetos corretamente', async ({ page }) => {
await page.goto('/projects')
await page.getByPlaceholder('Buscar Itens').fill('Marketing')
await page.waitForTimeout(300) // debounce
const items = page.getByTestId('project-item')
const count = await items.count()
for (let i = 0; i < count; i++) {
await expect(items.nth(i)).toContainText(/Marketing/i)
}
})
// --- 5. Layout Responsivo ---
test.describe('Design Responsivo', () => {
test('barra lateral colapsa em viewport mobile', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 })
await page.goto('/dashboard')
await expect(page.getByTestId('sidebar')).not.toBeVisible()
await page.getByRole('button', { name: /menu/i }).click()
await expect(page.getByTestId('sidebar')).toBeVisible()
})
})
// --- 6. Teste de Mock de API ---
test('trata erros de API com elegância', async ({ page }) => {
await page.route('**/api/dashboard/metrics', async route => {
await route.fulfill({
status: 503,
body: JSON.stringify({ error: 'Serviço Indisponível' })
})
})
await page.goto('/dashboard')
await expect(page.getByText(/O serviço está temporariamente indisponível/i)).toBeVisible()
})
})
Running 8 tests using 2 workers
✓ e2e/taskflow-e2e.spec.ts (8 tests) 34.2s
✓ chromium | 8 passed (34.2s)
✓ firefox | 8 passed (38.1s)
✓ webkit | 8 passed (41.5s)
❓ Perguntas Frequentes
P: Qual é a diferença entre Playwright e Cypress? R: O Playwright é mantido pela Microsoft (45k⭐), suporta múltiplos navegadores (Chromium/Firefox/WebKit) e usa o protocolo CDP para controlar diretamente o navegador, sendo mais rápido. O Cypress requer a injeção de JavaScript na página para executar e suporta apenas navegadores baseados em Chromium. A configuração
webServerdo Playwright também é mais adequada para cenários de inicialização automáticanpm run devdo Next.js.
P: Como testar uma página que requer login? R: Recomendamos usar o recurso
storageStatedo Playwright: Após fazer login noauth.setup.ts, salve os cookies e localStorage emstorageState.jsone depois referencie esse arquivo na configuraçãousede outros testes para evitar ter que fazer login repetidamente em cada teste.
P: Onde as screenshots de linha de base para testes de regressão visual devem ser mantidas? R: As screenshots de linha de base devem ser enviadas para o repositório Git (
e2e/__screenshots__/) como parte do código. O sistema CI fará a comparação automaticamente durante as execuções. Certifique-se de usar a tag.gitattributespara marcar os arquivos de screenshot comobinary.
P: Como escolher entre
page.route()e MSW para testes E2E? R:page.route()intercepta requisições no nível do navegador e não requer dependências adicionais, sendo adequado para mocking temporário. O MSW requer registro de Service Worker e é adequado para testes de componentes (Vitest). Para testes E2E, recomenda-sepage.route()por padrão para reduzir dependências externas.
P: O que posso fazer se os testes Playwright estiverem lentos no CI? R: (1) Use
workers: 1para evitar contenção de recursos; (2) Faça cache dos binários do navegador (~/.cache/ms-playwright); (3) Use--project=chromiumpara executar apenas navegadores críticos no CI; (4) Paralelize: Divida os testes em múltiplos jobs.
P: Como lidar com dados aleatórios em testes (como timestamps e IDs de banco de dados)? R: Use a opção
maskdo Playwright para ocultar elementos dinâmicos em testes visuais:toHaveScreenshot({ mask: [page.locator('[data-testid="timestamp"]')] }). Para asserções de texto, use padrões de expressão regular em vez de valores fixos.
📖 Resumo
- A configuração
webServerdo Playwright permite que o servidor de desenvolvimento Next.js inicie e pare automaticamente junto com os testes - O Page Object Model encapsula interações de página em classes separadas, tornando o código de teste mais conciso e de fácil manutenção
- Testes de caminho crítico cobrem fluxos de trabalho centrais do usuário, como autenticação, navegação e envio de formulários
toHaveScreenshot()permite comparações de regressão visual em nível de pixel para detectar mudanças não intencionais no estilo da interfacepage.route()intercepta requisições de API no nível do navegador; adequado para testes de isolamento e simulação de cenários de erro- A integração do Playwright com GitHub Actions requer cache do navegador, configuração de matriz paralela e upload de relatórios de falha
📝 Exercícios
-
Questão Básica (⭐): Crie uma Page Object
LoginPagecontendo três métodos —goto(),login(email, password)eexpectLoggedIn()— e escreva um caso de teste que use este POM. -
Exercício Avançado (⭐⭐): Escreva cinco testes end-to-end (E2E) para o fluxo de trabalho completo "Criar-Editar-Excluir" (CRUD) em sua aplicação Next.js, incluindo mocks
page.route()para erros de validação de formulário e cenários de erro de API. -
Desafio (⭐⭐⭐): Implemente uma suíte completa de testes de regressão visual: (1) Gere screenshots de linha de base de 5 páginas principais; (2) Configure os limiares
expect.toHaveScreenshotemaxDiffPixels; (3) Escreva um script CI para comparar automaticamente as screenshots em um pull request e adicionar um comentário com o relatório de diferenças.