# 🧪 Estratégia de Cobertura de Testes - Frontend Next.js
> **Última Atualização:** 2026-02-24
> **Projeto:** GoHorse Jobs Frontend
> **Versão Next.js:** 15.5.7 (App Router)
---
## 📊 Stack Identificada
| Componente | Tecnologia | Observações |
|------------|------------|-------------|
| **Framework** | Next.js 15.5.7 | App Router |
| **Linguagem** | TypeScript 5 | Strict mode |
| **Estado Global** | Zustand 4.5.7 | notifications-store |
| **Formulários** | React Hook Form + Zod | Validação client-side |
| **Data Fetching** | Fetch nativo | Server + Client Components |
| **Estilização** | Tailwind CSS 4 + shadcn/ui | Componentes Radix |
| **Testes Unitários** | Jest 30 + React Testing Library 16 | Configurado |
| **Testes E2E** | Playwright 1.57 | Configurado |
---
## 🎯 1. Priorização de Testes
### 🔴 Crítico (100% de cobertura almejada)
| Área | Por quê | Tipo de Teste |
|------|--------|---------------|
| **Autenticação** | Login, registro, logout, recuperação de senha | E2E + Unit |
| **Formulários de Pagamento** | Integração Stripe, dados sensíveis | E2E + Integration |
| **Cálculos de Preço** | Planos, assinaturas, descontos | Unit (pure functions) |
| **Validação de Formulários** | Zod schemas, mensagens de erro | Unit |
| **Stores Zustand** | Estado global, optimistic updates | Unit |
### 🟡 Importante (80% de cobertura almejada)
| Área | Por quê | Tipo de Teste |
|------|--------|---------------|
| **Listagem de Vagas** | Filtros, paginação, busca | Integration |
| **Dashboard** | Gráficos, estatísticas, KPIs | Integration |
| **Perfil de Usuário** | Edição, upload de foto | E2E |
| **Notificações** | Real-time, marcação como lida | Unit + Integration |
| **Tickets de Suporte** | Criação, categoria, prioridade | Integration |
### 🟢 Desejável (60% de cobertura almejada)
| Área | Por quê | Tipo de Teste |
|------|--------|---------------|
| **UI Components** | shadcn/ui (testado pela lib) | Snapshot |
| **Páginas Estáticas** | About, Terms, Privacy | E2E smoke |
| **Animações** | Framer Motion | Visual regression |
---
## 🛠️ 2. Ferramentas por Camada
### Pirâmide de Testes
```
┌─────────┐
│ E2E │ Playwright
│ 10% │ Fluxos críticos completos
├─────────┤
│Integration│ Jest + RTL
│ 20% │ Componentes + API mocked
├─────────┤
│ Unit │ Jest
│ 70% │ Funções puras, hooks, stores
└─────────┘
```
### Ferramentas Recomendadas
| Camada | Ferramenta | Uso |
|--------|------------|-----|
| **Unit Tests** | Jest + @testing-library/react | Hooks, utils, stores, validações |
| **Integration Tests** | Jest + MSW (Mock Service Worker) | Componentes com API mocked |
| **E2E Tests** | Playwright | Fluxos completos de usuário |
| **Visual Regression** | Playwright + Percy/Chromatic | UI snapshots (opcional) |
| **A11y Tests** | jest-axe + @axe-core/playwright | Acessibilidade |
### Setup Recomendado
```bash
# Já instalado
npm install -D jest @testing-library/react @testing-library/jest-dom @playwright/test
# Adicionar para melhor cobertura
npm install -D msw jest-axe @testing-library/user-event
```
---
## 🧩 3. Server Components vs Client Components
### Server Components (Next.js 15)
**Características:**
- Renderizados no servidor
- Sem estado local
- Sem interatividade direta
- Ideal para data fetching inicial
**Estratégia de Teste:**
```typescript
// ❌ NÃO testar Server Components diretamente com RTL
// Eles não têm estado client-side
// ✅ Testar via E2E (Playwright)
test('Job listing page loads jobs from server', async ({ page }) => {
await page.goto('/jobs');
await expect(page.getByRole('heading', { name: /jobs/i })).toBeVisible();
await expect(page.getByTestId('job-card')).toHaveCount(10);
});
// ✅ Testar funções de data fetching separadamente
// lib/__tests__/api.test.ts (já existe)
describe('jobsApi', () => {
it('should fetch jobs with correct parameters', async () => {
// Testa a lógica de construção de URL e fetch
});
});
```
### Client Components
**Características:**
- Renderizados no browser
- Podem ter estado local
- Interativos (eventos)
- Hooks (useState, useEffect)
**Estratégia de Teste:**
```typescript
// ✅ Testar com React Testing Library
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
describe('LoginForm', () => {
it('should submit form with valid credentials', async () => {
const user = userEvent.setup();
const mockLogin = jest.fn().mockResolvedValue({ success: true });
render();
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(mockLogin).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
});
```
### Matriz de Decisão
| Componente | Server/Client | Teste Recomendado |
|------------|---------------|-------------------|
| Página de listagem | Server | E2E + API test |
| Formulário de login | Client | RTL + Unit |
| Card de vaga | Client | RTL snapshot |
| Dashboard com gráficos | Client | RTL + MSW |
| Filtros de busca | Client | RTL + integration |
| Layout/Header | Server | E2E smoke |
---
## 🎭 4. Mocks de API
### Estratégia Atual (Jest mock)
```typescript
// ❌ Limitações do mock manual
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: async () => mockData
});
```
### Estratégia Recomendada (MSW)
```typescript
// msw/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('*/api/v1/jobs', () => {
return HttpResponse.json({
data: [
{ id: '1', title: 'Software Engineer', company: 'Tech Corp' }
],
pagination: { total: 1, page: 1, limit: 10 }
});
}),
http.post('*/api/v1/auth/login', async ({ request }) => {
const body = await request.json();
if (body.email === 'error@test.com') {
return new HttpResponse(null, { status: 401 });
}
return HttpResponse.json({
token: 'mock-jwt-token',
user: { id: '1', email: body.email }
});
}),
];
// jest.setup.js
import { setupServer } from 'msw/node';
import { handlers } from './msw/handlers';
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
```
### Vantagens do MSW
| Aspecto | Jest Mock | MSW |
|---------|-----------|-----|
| Interceptação | Manual por teste | Automática global |
| Realismo | Baixo | Alto (rede real) |
| Manutenção | Alta | Baixa |
| Debug | Difícil | Network tab |
| Reutilização | Por teste | Global |
---
## ⚠️ 5. Edge Cases Frequentemente Esquecidos
### 5.1 Autenticação
```typescript
// ❌ Comumente esquecido
describe('Auth Edge Cases', () => {
it('should handle expired token gracefully', async () => {
// Token expirado durante sessão
mockApi.returnError(401, 'Token expired');
render();
await waitFor(() => {
expect(screen.getByText(/session expired/i)).toBeInTheDocument();
});
});
it('should handle concurrent login attempts', async () => {
// Usuário clica login múltiplas vezes
const { rerender } = render();
const submitBtn = screen.getByRole('button', { name: /login/i });
fireEvent.click(submitBtn);
fireEvent.click(submitBtn);
fireEvent.click(submitBtn);
// Deve fazer apenas 1 requisição
expect(mockLogin).toHaveBeenCalledTimes(1);
});
it('should handle password with special characters', async () => {
// Senha com caracteres especiais: !@#$%^&*(){}[]
await user.type(passwordInput, 'P@ss!w0rd#$%');
await user.click(submitBtn);
expect(mockLogin).toHaveBeenCalledWith(
expect.objectContaining({ password: 'P@ss!w0rd#$%' })
);
});
it('should handle empty roles array in JWT', async () => {
// JWT válido mas sem roles
mockLogin.mockResolvedValue({ token: 'valid-token', roles: [] });
render();
await waitFor(() => {
expect(screen.getByText(/unauthorized/i)).toBeInTheDocument();
});
});
it('should handle network timeout during login', async () => {
mockLogin.mockImplementation(() =>
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 5000)
)
);
render();
await user.click(submitBtn);
await waitFor(() => {
expect(screen.getByText(/connection timeout/i)).toBeInTheDocument();
});
});
});
```
### 5.2 Formulários
```typescript
describe('Form Edge Cases', () => {
it('should handle paste event in confirm password', async () => {
// Usuário cola senha no campo de confirmação
const password = 'MyP@ssw0rd';
await user.type(passwordInput, password);
// Simula paste no confirm field
await user.click(confirmPasswordInput);
await user.paste(password);
expect(confirmPasswordInput).toHaveValue(password);
});
it('should handle very long input values', async () => {
// Nome com 500 caracteres
const longName = 'A'.repeat(500);
await user.type(nameInput, longName);
await waitFor(() => {
expect(screen.getByText(/maximum.*characters/i)).toBeInTheDocument();
});
});
it('should handle unicode characters in fields', async () => {
// Caracteres unicode: emojis, acentos, caracteres asiáticos
await user.type(nameInput, 'José 日本語 👨💻');
await user.click(submitBtn);
expect(mockSubmit).toHaveBeenCalledWith(
expect.objectContaining({ name: 'José 日本語 👨💻' })
);
});
it('should handle rapid consecutive form submissions', async () => {
// Submit spam - deve bloquear após primeiro
render();
for (let i = 0; i < 10; i++) {
await user.click(submitBtn);
}
expect(mockSubmit).toHaveBeenCalledTimes(1);
});
it('should preserve form state on accidental navigation', async () => {
// Usuário preenche formulário, clica em link, volta
render();
await user.type(input1, 'value1');
// Simula navegação acidental
window.history.pushState({}, '', '/other-page');
window.history.back();
// Formulário deve preservar ou avisar
});
});
```
### 5.3 Paginação e Listas
```typescript
describe('List Edge Cases', () => {
it('should handle empty results gracefully', async () => {
mockApi.returnEmptyList();
render();
await waitFor(() => {
expect(screen.getByText(/no jobs found/i)).toBeInTheDocument();
});
});
it('should handle single item pagination', async () => {
// Apenas 1 item - não deve mostrar paginação
mockApi.returnSingleItem();
render();
await waitFor(() => {
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
});
});
it('should handle exactly N items per page', async () => {
// Exatamente 10 itens - deve mostrar paginação
mockApi.returnItems(10);
render();
await waitFor(() => {
expect(screen.getByRole('navigation')).toBeInTheDocument();
});
});
it('should handle invalid page number in URL', async () => {
// URL com page=999 quando só há 5 páginas
window.history.pushState({}, '', '/jobs?page=999');
render();
await waitFor(() => {
expect(window.location.search).toContain('page=5');
});
});
it('should handle deleted item while on page', async () => {
// Item deletado enquanto usuário está na página
mockApi.returnItems(10);
render();
// Simula deleção externa
mockApi.returnItems(9);
await user.click(refreshBtn);
expect(screen.getByText(/item removed/i)).toBeInTheDocument();
});
});
```
### 5.4 Upload de Arquivos
```typescript
describe('Upload Edge Cases', () => {
it('should reject files larger than limit', async () => {
const largeFile = new File(['x'.repeat(11 * 1024 * 1024)], 'large.pdf', {
type: 'application/pdf'
});
await user.upload(fileInput, largeFile);
await waitFor(() => {
expect(screen.getByText(/file too large/i)).toBeInTheDocument();
});
});
it('should handle zero-byte files', async () => {
const emptyFile = new File([], 'empty.txt', { type: 'text/plain' });
await user.upload(fileInput, emptyFile);
await waitFor(() => {
expect(screen.getByText(/file is empty/i)).toBeInTheDocument();
});
});
it('should handle special characters in filename', async () => {
const specialFile = new File(['content'], 'file