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



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:

TS
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.

100%
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

BASH
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

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

TEXT 📖 Somente leitura
├── 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

TS
// 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')
})
BASH
npx playwright test --project=chromium --headed
💻 Saída:

TEXT 📖 Somente leitura
Running 1 test using 1 worker
  ✓ e2e/example.spec.ts:3:1 › página inicial tem o título correto (2.3s)

Saída:

TEXT 📖 Somente leitura
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".

100%
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

TS
// 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

TS
// 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:

TEXT 📖 Somente leitura
Define tipo(s) TypeScript: Page, Locator.
TS
// 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:

TEXT 📖 Somente leitura
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

TS
// 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

TS
// 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

TS
// 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().

100%
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

TS
// 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

BASH
# 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:

TEXT 📖 Somente leitura
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)
TS
// 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:

TEXT 📖 Somente leitura
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

TS
// 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:

TEXT 📖 Somente leitura
Código TypeScript executado com sucesso.
TS
// 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:

TEXT 📖 Somente leitura
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.

YAML
# .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

TS
// 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()
  })
})
💻 Saída:

TEXT 📖 Somente leitura
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 webServer do Playwright também é mais adequada para cenários de inicialização automática npm run dev do Next.js.

P: Como testar uma página que requer login? R: Recomendamos usar o recurso storageState do Playwright: Após fazer login no auth.setup.ts, salve os cookies e localStorage em storageState.json e depois referencie esse arquivo na configuração use de 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 .gitattributes para marcar os arquivos de screenshot como binary.

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-se page.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: 1 para evitar contenção de recursos; (2) Faça cache dos binários do navegador (~/.cache/ms-playwright); (3) Use --project=chromium para 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 mask do 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


📝 Exercícios

  1. Questão Básica (⭐): Crie uma Page Object LoginPage contendo três métodos — goto(), login(email, password) e expectLoggedIn() — e escreva um caso de teste que use este POM.

  2. 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.

  3. 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.toHaveScreenshot e maxDiffPixels; (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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%