diff --git a/docs/FRONTEND_TESTING_STRATEGY.md b/docs/FRONTEND_TESTING_STRATEGY.md
new file mode 100644
index 0000000..75e778c
--- /dev/null
+++ b/docs/FRONTEND_TESTING_STRATEGY.md
@@ -0,0 +1,923 @@
+# 🧪 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