# 🧪 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