gohorsejobs/docs/FRONTEND_TESTING_STRATEGY.md
Tiago Yamamoto 4a096ff903 docs: add comprehensive frontend testing strategy
- Add test coverage strategy for Next.js 15 App Router
- Document Server vs Client Components testing approach
- Include MSW mock strategy for API testing
- Add edge cases often forgotten (auth, forms, pagination, uploads)
- Provide templates for unit, integration, and E2E tests
- Include Zustand store testing patterns
2026-02-23 21:13:04 -06:00

28 KiB

🧪 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

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

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

// ✅ 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(<LoginForm onSubmit={mockLogin} />);
        
        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)

// ❌ Limitações do mock manual
global.fetch = jest.fn().mockResolvedValue({
    ok: true,
    json: async () => mockData
});

Estratégia Recomendada (MSW)

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

// ❌ Comumente esquecido
describe('Auth Edge Cases', () => {
    it('should handle expired token gracefully', async () => {
        // Token expirado durante sessão
        mockApi.returnError(401, 'Token expired');
        render(<ProtectedPage />);
        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(<LoginForm />);
        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(<ProtectedPage />);
        
        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(<LoginForm />);
        await user.click(submitBtn);
        
        await waitFor(() => {
            expect(screen.getByText(/connection timeout/i)).toBeInTheDocument();
        });
    });
});

5.2 Formulários

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(<JobApplicationForm />);
        
        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(<MultiStepForm />);
        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

describe('List Edge Cases', () => {
    it('should handle empty results gracefully', async () => {
        mockApi.returnEmptyList();
        render(<JobsList />);
        
        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(<JobsList />);
        
        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(<JobsList />);
        
        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(<JobsList />);
        
        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(<JobsList />);
        
        // Simula deleção externa
        mockApi.returnItems(9);
        await user.click(refreshBtn);
        
        expect(screen.getByText(/item removed/i)).toBeInTheDocument();
    });
});

5.4 Upload de Arquivos

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<script>.pdf', {
            type: 'application/pdf'
        });
        
        await user.upload(fileInput, specialFile);
        
        // Nome deve ser sanitizado
        expect(screen.queryByText(/<script>/)).not.toBeInTheDocument();
    });
    
    it('should handle network interruption during upload', async () => {
        mockUpload.mockImplementation(() => 
            new Promise((_, reject) => 
                setTimeout(() => reject(new Error('Network error')), 100)
            )
        );
        
        const file = new File(['content'], 'test.pdf', { type: 'application/pdf' });
        await user.upload(fileInput, file);
        
        await waitFor(() => {
            expect(screen.getByText(/upload failed.*retry/i)).toBeInTheDocument();
        });
    });
});

5.5 Real-time / WebSockets

describe('Real-time Edge Cases', () => {
    it('should handle WebSocket disconnection', async () => {
        render(<NotificationsProvider />);
        
        // Simula desconexão
        act(() => {
            wsClient.simulateDisconnect();
        });
        
        await waitFor(() => {
            expect(screen.getByText(/reconnecting/i)).toBeInTheDocument();
        });
    });
    
    it('should handle out-of-order messages', async () => {
        // Mensagens chegando fora de ordem
        render(<ChatWindow />);
        
        act(() => {
            wsClient.receiveMessage({ id: '3', text: 'Third' });
            wsClient.receiveMessage({ id: '1', text: 'First' });
            wsClient.receiveMessage({ id: '2', text: 'Second' });
        });
        
        const messages = screen.getAllByRole('listitem');
        expect(messages[0]).toHaveTextContent('First');
        expect(messages[1]).toHaveTextContent('Second');
        expect(messages[2]).toHaveTextContent('Third');
    });
    
    it('should handle duplicate messages', async () => {
        render(<ChatWindow />);
        
        act(() => {
            wsClient.receiveMessage({ id: '1', text: 'Hello' });
            wsClient.receiveMessage({ id: '1', text: 'Hello' }); // Duplicado
        });
        
        const messages = screen.getAllByRole('listitem');
        expect(messages).toHaveLength(1);
    });
});

📁 6. Estrutura de Testes

frontend/
├── __tests__/                    # Testes globais
│   └── setup/
│       └── test-utils.tsx        # Custom render com providers
├── e2e/                          # Playwright E2E
│   ├── auth.spec.ts              # Fluxos de autenticação
│   ├── jobs.spec.ts              # Fluxos de vagas
│   ├── payment.spec.ts           # Fluxos de pagamento
│   └── fixtures/                 # Dados de teste
│       └── test-data.ts
├── src/
│   ├── app/
│   │   ├── login/
│   │   │   └── page.test.tsx     # Testes da página
│   │   └── dashboard/
│   │       └── candidates/
│   │           └── page.test.tsx
│   ├── components/
│   │   └── ui/
│   │       └── __tests__/
│   │           └── job-card.test.tsx
│   ├── lib/
│   │   ├── __tests__/
│   │   │   ├── api.test.ts       # API client
│   │   │   ├── auth.test.ts      # Auth utils
│   │   │   └── validation.test.ts # Zod schemas
│   │   └── store/
│   │       └── __tests__/
│   │           └── notifications-store.test.ts
│   └── hooks/
│       └── __tests__/
│           └── use-auth.test.ts
└── msw/
    └── handlers.ts               # API mocks globais

🧪 7. Templates de Teste

Template: Teste de Página (Client Component)

// src/app/dashboard/settings/page.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SettingsPage from './page';

// Mocks
jest.mock('next/navigation', () => ({
    useRouter: () => ({ push: jest.fn(), refresh: jest.fn() }),
}));

jest.mock('@/lib/api', () => ({
    settingsApi: {
        get: jest.fn().mockResolvedValue({ logoUrl: '', primaryColor: '#000' }),
        save: jest.fn().mockResolvedValue({}),
    },
    credentialsApi: {
        list: jest.fn().mockResolvedValue({ services: [] }),
    },
}));

describe('SettingsPage', () => {
    beforeEach(() => {
        jest.clearAllMocks();
    });

    describe('Loading State', () => {
        it('should show loading spinner initially', () => {
            render(<SettingsPage />);
            expect(screen.getByRole('status')).toBeInTheDocument();
        });
    });

    describe('Theme Settings', () => {
        it('should save theme changes', async () => {
            const user = userEvent.setup();
            render(<SettingsPage />);
            
            await waitFor(() => {
                expect(screen.getByLabelText(/company name/i)).toBeInTheDocument();
            });
            
            await user.type(screen.getByLabelText(/company name/i), 'Test Company');
            await user.click(screen.getByRole('button', { name: /save/i }));
            
            await waitFor(() => {
                expect(screen.getByText(/saved successfully/i)).toBeInTheDocument();
            });
        });
    });

    describe('Error Handling', () => {
        it('should display error message on API failure', async () => {
            const { settingsApi } = require('@/lib/api');
            settingsApi.save.mockRejectedValue(new Error('API Error'));
            
            const user = userEvent.setup();
            render(<SettingsPage />);
            
            await waitFor(() => {
                expect(screen.getByLabelText(/company name/i)).toBeInTheDocument();
            });
            
            await user.click(screen.getByRole('button', { name: /save/i }));
            
            await waitFor(() => {
                expect(screen.getByText(/failed to save/i)).toBeInTheDocument();
            });
        });
    });
});

Template: Teste de Store (Zustand)

// src/lib/store/__tests__/notifications-store.test.ts
import { act } from '@testing-library/react';
import { useNotificationsStore } from '../notifications-store';

// Mock da API
jest.mock('@/lib/api', () => ({
    notificationsApi: {
        list: jest.fn(),
        markAsRead: jest.fn(),
        markAllAsRead: jest.fn(),
    },
}));

describe('useNotificationsStore', () => {
    beforeEach(() => {
        // Reset store state
        useNotificationsStore.setState({
            notifications: [],
            unreadCount: 0,
            loading: false,
        });
        jest.clearAllMocks();
    });

    describe('fetchNotifications', () => {
        it('should fetch and set notifications', async () => {
            const { notificationsApi } = require('@/lib/api');
            notificationsApi.list.mockResolvedValue([
                { id: '1', message: 'Test', read: false },
                { id: '2', message: 'Test 2', read: true },
            ]);

            await act(async () => {
                await useNotificationsStore.getState().fetchNotifications();
            });

            const state = useNotificationsStore.getState();
            expect(state.notifications).toHaveLength(2);
            expect(state.unreadCount).toBe(1);
            expect(state.loading).toBe(false);
        });

        it('should handle API errors gracefully', async () => {
            const { notificationsApi } = require('@/lib/api');
            notificationsApi.list.mockRejectedValue(new Error('Network error'));

            await act(async () => {
                await useNotificationsStore.getState().fetchNotifications();
            });

            const state = useNotificationsStore.getState();
            expect(state.notifications).toEqual([]);
            expect(state.loading).toBe(false);
        });
    });

    describe('markAsRead (Optimistic Update)', () => {
        it('should optimistically mark notification as read', async () => {
            // Setup initial state
            useNotificationsStore.setState({
                notifications: [
                    { id: '1', message: 'Test', read: false },
                ],
                unreadCount: 1,
            });

            const { notificationsApi } = require('@/lib/api');
            notificationsApi.markAsRead.mockResolvedValue({});

            await act(async () => {
                await useNotificationsStore.getState().markAsRead('1');
            });

            const state = useNotificationsStore.getState();
            expect(state.notifications[0].read).toBe(true);
            expect(state.unreadCount).toBe(0);
        });

        it('should revert optimistic update on API failure', async () => {
            useNotificationsStore.setState({
                notifications: [
                    { id: '1', message: 'Test', read: false },
                ],
                unreadCount: 1,
            });

            const { notificationsApi } = require('@/lib/api');
            notificationsApi.markAsRead.mockRejectedValue(new Error('Failed'));

            await act(async () => {
                await useNotificationsStore.getState().markAsRead('1');
            });

            // TODO: Implementar rollback no store
            // const state = useNotificationsStore.getState();
            // expect(state.notifications[0].read).toBe(false);
        });
    });
});

Template: Teste E2E (Playwright)

// e2e/candidate-application.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Candidate Job Application Flow', () => {
    test.beforeEach(async ({ page }) => {
        // Login como candidato
        await page.goto('/login');
        await page.fill('[name="email"]', 'candidate@example.com');
        await page.fill('[name="password"]', 'password123');
        await page.click('button[type="submit"]');
        await page.waitForURL(/\/dashboard/);
    });

    test('should apply to a job successfully', async ({ page }) => {
        // Navegar para listagem de vagas
        await page.goto('/jobs');
        
        // Aguardar vagas carregarem
        await expect(page.getByTestId('job-card').first()).toBeVisible();
        
        // Clicar em uma vaga
        await page.getByTestId('job-card').first().click();
        
        // Verificar detalhes da vaga
        await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
        
        // Clicar em candidatar-se
        await page.click('button:has-text("Apply")');
        
        // Preencher formulário de candidatura
        await page.fill('[name="coverLetter"]', 'I am very interested...');
        await page.click('button:has-text("Submit Application")');
        
        // Verificar sucesso
        await expect(page.getByText(/application submitted/i)).toBeVisible();
    });

    test('should show validation errors for incomplete application', async ({ page }) => {
        await page.goto('/jobs/1/apply');
        
        // Tentar submeter sem preencher
        await page.click('button[type="submit"]');
        
        // Verificar mensagens de erro
        await expect(page.getByText(/cover letter is required/i)).toBeVisible();
    });

    test('should prevent duplicate applications', async ({ page }) => {
        // Candidatar a uma vaga que já se candidatou
        await page.goto('/jobs/already-applied-job/apply');
        
        await expect(page.getByText(/already applied/i)).toBeVisible();
        
        // Botão de apply deve estar desabilitado
        await expect(page.getByRole('button', { name: /apply/i })).toBeDisabled();
    });
});

📈 8. Métricas e Cobertura

Comandos de Cobertura

# Unit tests com coverage
npm run test -- --coverage --coverageReporters=text --coverageReporters=lcov

# E2E tests com trace
npx playwright test --trace on

# Coverage por diretório
npm run test -- --coverage --testPathPattern="src/lib"

Metas por Módulo

Módulo Cobertura Atual Meta
lib/auth.ts ? 100%
lib/api.ts 85% 95%
lib/validation.ts ? 100%
lib/store/* ? 90%
components/ui/* ? 70%
app/login/* 70% 90%
app/dashboard/* ? 80%

CI Integration

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm run test -- --coverage
      - uses: codecov/codecov-action@v4
        with:
          files: ./coverage/lcov.info

  e2e-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run build
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/

9. Checklist de Implementação

Fase 1: Infraestrutura (Semana 1)

  • Instalar MSW para mocks de API
  • Configurar custom render com providers
  • Setup test fixtures e factories
  • Configurar CI com coverage

Fase 2: Unit Tests Críticos (Semana 2)

  • Testes de autenticação (auth.ts)
  • Testes de validação (Zod schemas)
  • Testes de stores (Zustand)
  • Testes de hooks customizados

Fase 3: Integration Tests (Semana 3)

  • Formulários de login/registro
  • Formulários de candidatura
  • Listagem de vagas com filtros
  • Dashboard com dados mocked

Fase 4: E2E Tests (Semana 4)

  • Fluxo completo de registro
  • Fluxo completo de candidatura
  • Fluxo de pagamento (Stripe)
  • Fluxo de recuperação de senha

Fase 5: Edge Cases (Semana 5)

  • Implementar edge cases documentados
  • Testes de acessibilidade (jest-axe)
  • Testes de performance (Lighthouse CI)

📚 10. Referências


Nota: Este documento deve ser atualizado conforme a cobertura de testes evolui. Execute npm run test -- --coverage regularmente para acompanhar métricas.