Skip to content

Estratégia de Testes — OLP

Documento mestre — Substitui TESTING.md, TESTING_STRATEGY.md, E2E_TESTING.md e TESTING_REAL_VS_MOCK_ANALYSIS.md.

Última atualização: 2026-04-04

Erros E2E: Toda falha de teste DEVE ser catalogada em TEST_ERROR_PLAYBOOK.md com causa raiz, classificação e padrão derivado. Protocolo: rodar contra produção primeiro; se passa em prod, teste está correto — corrigir staging/infra.


1. Princípio Central

text
Produção (Lovable sandbox)     Staging (CI/GitHub Actions)
────────────────────────────   ────────────────────────────
  Desenvolvimento + QA            Validação automatizada
  Usuários controlados            Mesmos usuários, senha diferente
  Rodar testes manualmente        Pipeline automático
  ↓                               ↓
  Se passa aqui → commit →  CI roda contra staging

Regra de ouro: Se um teste passa em produção e falha em staging, o problema é infraestrutura do staging (seed, secrets, config) — nunca altere o código de produção para fazer staging funcionar.


2. Pirâmide de Testes

text
          ┌──────────┐
          │   E2E    │  Playwright — fluxos completos no browser
          │  (poucos) │  Contra staging.olp.digital no CI
          ├──────────┤
          │ Segurança│  IDOR cross-escola, privilege escalation
          │  (CI)    │  Vitest HTTP com JWTs reais via e2e-login
          ├──────────┤
          │ Contrato │  HTTP puro — CORS, auth guards, response shape
          │  HTTP    │  Vitest no CI (staging), Deno no dev (produção)
          ├──────────┤
          │ Integração│  Vitest — hooks React Query, cache, wiring
          │  (médio)  │  Mocks controlados, QueryClient real
          ├──────────┤
          │  Unitário │  Vitest/Deno — funções puras, helpers
          │  (muitos) │  Zero I/O, zero rede
          └──────────┘

3. Árvore de Decisão de Camada

Regra canônica: Antes de escrever qualquer teste novo, percorra esta árvore para determinar a camada correta.

text
A lógica precisa de browser real?
├── Cookie HttpOnly, CORS worker       → E2E (Playwright)
├── Layout CSS (grid, flex, viewport)  → E2E (Playwright)
├── Drag-and-drop, FileReader          → E2E (Playwright)
├── Navegação SPA / routing real       → E2E (Playwright)
└── NÃO
    ├── Função pura (sem React)?       → Unitário (Vitest)
    ├── Componente com props?          → Componente (Testing Library)
    ├── Hook React Query?              → Integração (mock invokeAction)
    └── Contrato HTTP?                 → Deno (dev) / Vitest (CI)

3.1 Nomenclatura e Localização por Camada

CamadaPadrão de nomeLocalização
Unitário*.test.tssrc/**/__tests__/
Componente*.test.tsxsrc/components/**/__tests__/
Integração hooks*.integration.test.tssrc/hooks/__tests__/
Contrato Denoindex.test.tssupabase/functions/<nome>/
Contrato CI*.contract.test.tstests/contracts/
Segurançaidor-*.test.tstests/security/
E2E*.spec.tse2e/

3.2 Regras Obrigatórias Derivadas

  1. Regra de negócio UI → Vitest componente, nunca Playwright
  2. Função de visibilidade → extrair para helper puro testável (helpers.ts)
  3. data-testid obrigatório apenas em elementos interagidos por E2E ativo
  4. Proibido stub vazio sem corpo — se não tem assertions, é documentação de intenção e deve ter comentário explicando o que será testado
  5. Fixture factory (src/test/fixtures/) para dados reutilizáveis entre testes

3.3 Templates Mínimos por Camada

Unitário (função pura):

typescript
import { describe, it, expect } from 'vitest';
import { minhaFuncao } from '../helpers';

describe('minhaFuncao', () => {
  it('retorna X quando input Y', () => {
    expect(minhaFuncao('Y')).toBe('X');
  });
});

Componente (Testing Library):

typescript
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { MeuComponente } from '../MeuComponente';
import { buildDadosTeste } from '@/test/fixtures/portal-data';

describe('MeuComponente', () => {
  it('exibe campo quando config habilitada', () => {
    const dados = buildDadosTeste({ campoVisivel: true });
    render(<MeuComponente dados={dados} />);
    expect(screen.getByText('Campo')).toBeInTheDocument();
  });
});

Integração hooks (React Query):

typescript
import { describe, it, expect, vi } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

describe('useMeuHook', () => {
  it('invalida cache após mutation', async () => {
    const qc = new QueryClient();
    const spy = vi.spyOn(qc, 'invalidateQueries');
    // ... renderHook + act + waitFor
    expect(spy).toHaveBeenCalledWith({ queryKey: ['minha-chave'] });
  });
});

E2E (Playwright):

typescript
import { coordenadorTest as test, expect } from '../playwright-fixture';

test('sidebar exibe mural', async ({ authedPage }) => {
  await authedPage.goto('/coordenacao/mural');
  await expect(authedPage.getByText('Mural')).toBeVisible();
});

4. Definições Canônicas

4.1 Unitário (Vitest ou Deno.test)

ItemRegra
O que testaFunções puras: validações, formatadores, cálculos, helpers
SemRede, banco, browser, mocks de fetch/supabase
Ondesrc/**/__tests__/*.test.ts ou supabase/functions/_shared/**/*.test.ts
CritérioSe precisa de mock de fetch ou supabase → NÃO é unitário

4.2 Contrato Deno (supabase--test_edge_functions)

ItemRegra
O que testaEdge Functions como black-box HTTP — envelope, não lógica
ValidaCORS headers, auth guard (401 strict), response shape {success, message}, Content-Type
Ondesupabase/functions/<nome>/index.test.ts
AmbienteProdução (via ferramenta Lovable supabase--test_edge_functions)
CritérioNUNCA precisa de sessão real — testa o protocolo HTTP

Regra de status HTTP:

SituaçãoStatus esperadoSe retornar 500
Sem cookie / token ausente401Bug no catch block da função
Token inválido / revogado401Bug no catch block da função
Acesso negado / papel insuficiente403Bug no catch block da função
Erro real de infraestrutura500OK — mas nunca para auth

PROIBIDO: Aceitar [401, 500] em testes de auth. Se retorna 500 quando deveria retornar 401, é um defeito real que deve ser corrigido na função, não mascarado no teste.

Template mínimo:

IMPORTANTE: O sistema usa CORS com origin reflection (não *) porque cookies HttpOnly exigem credentials: 'include'. Nos testes, envie Origin: https://olp.digital e valide que o header reflete essa origem.

typescript
import "https://deno.land/std@0.224.0/dotenv/load.ts";
import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts";

const SUPABASE_URL = Deno.env.get("VITE_SUPABASE_URL")!;
const ANON_KEY = Deno.env.get("VITE_SUPABASE_PUBLISHABLE_KEY")!;
const FUNCTION_URL = `${SUPABASE_URL}/functions/v1/<nome-funcao>`;

Deno.test("OPTIONS retorna CORS headers", async () => {
  const res = await fetch(FUNCTION_URL, {
    method: "OPTIONS",
    headers: { "apikey": ANON_KEY, "Origin": "https://olp.digital" },
  });
  assertEquals(res.status, 204);
  assertEquals(res.headers.get("access-control-allow-origin"), "https://olp.digital");
  assertEquals(res.headers.get("access-control-allow-credentials"), "true");
  await res.text();
});

Deno.test("POST sem cookie retorna 401", async () => {
  const res = await fetch(FUNCTION_URL, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "apikey": ANON_KEY,
      "Origin": "https://olp.digital",
    },
    body: JSON.stringify({ action: "list", params: {} }),
  });
  // 401 strict — se retornar 500, é bug no catch block da função
  assertEquals(res.status, 401, `Esperava 401, recebeu ${res.status}`);
  const body = await res.json();
  assertEquals(body.success, false);
});

Deno.test("Response é JSON com Content-Type correto", async () => {
  const res = await fetch(FUNCTION_URL, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "apikey": ANON_KEY,
      "Origin": "https://olp.digital",
    },
    body: JSON.stringify({ action: "list", params: {} }),
  });
  const ct = res.headers.get("content-type") || "";
  assertEquals(ct.includes("application/json"), true);
  await res.json();
});

Deno.test("Response não vaza stack traces", async () => {
  const res = await fetch(FUNCTION_URL, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "apikey": ANON_KEY,
      "Origin": "https://olp.digital",
    },
    body: JSON.stringify({ action: "list", params: {} }),
  });
  const text = await res.text();
  assertEquals(text.includes("node_modules"), false, "Response contém path interno");
  assertEquals(text.includes("at Object."), false, "Response contém stack trace");
});

4.3 Contrato HTTP (Vitest — CI contra staging)

ItemRegra
O que testaMesmas validações do Deno, mas rodando no CI contra staging
ValidaAuth guards (401 strict), CORS, response shape, paridade staging/produção
Ondetests/contracts/*.contract.test.ts
AmbienteStaging (CI) via env vars VITE_SUPABASE_URL, VITE_SUPABASE_PUBLISHABLE_KEY
FallbackPROIBIDO — se env não configurado, o teste falha explicitamente

4.4 Segurança — IDOR Cross-Escola (Vitest — CI)

ItemRegra
O que testaIsolamento de dados entre escolas com JWTs reais
ValidaIDOR via RLS (espera array vazio ou dados próprios) e ownership checks (espera 403)
Ondetests/security/idor-cross-escola.test.ts
AmbienteStaging (CI) — requer E2E_TEST_PASSWORD
FallbackPROIBIDO — sem fallback para produção

Distinção por tipo de proteção:

ProteçãoExpectativa no teste
RLS puro (Postgres filtra)200 com array vazio ou só dados da própria escola
Query scoped por JWT200 com dados da própria escola (assert escola_id === minha_escola)
Ownership check explícito403 Forbidden

4.5 Integração de Hooks (Vitest)

ItemRegra
O que testaWiring React Query: chaves de cache, invalidações, transformações
ComQueryClient real, spies em invalidateQueries, mocks de invokeAction
Ondesrc/hooks/__tests__/*.integration.test.ts
CritérioValida que o hook chama a action certa e invalida o cache certo

4.6 E2E (Playwright)

ItemRegra
O que testaFluxos completos no browser com usuário real autenticado
ValidaNavegação, RBAC visual, formulários, modais, redirecionamentos
Ondee2e/<modulo>.spec.ts
AmbienteStaging no CI (BASE_URL=https://staging.olp.digital), preview no dev
Critérioworkers: 1, timeouts estendidos, waitUntil: 'domcontentloaded' (nunca networkidle)

Arquitetura do e2e-login (bypass de autenticação):

text
Playwright (sandbox)                    e2e-login EF (Deno)
─────────────────────                   ────────────────────
process.env.E2E_TEST_PASSWORD  ──────►  Deno.env.get('E2E_TEST_PASSWORD')
        ↓                                       ↓
  Envia como header                     Compara string simples (===)
  X-E2E-Key: <valor>                    Se bate → gera JWT direto
                                        (sem Argon2id, sem lookup de senha)

                                         cookie olp_auth

                                       ┌────────▼────────┐
                                       │ Testes E2E       │
                                       │ (autenticado)    │
                                       └─────────────────┘

Princípio: Login via Edge Function e2e-login usando shared secret. O secret E2E_TEST_PASSWORD é comparado por igualdade simples (sem Argon2id). JWT e cookie HttpOnly são gerados normalmente. O secret só existe em staging — produção retorna erro se acessado.

4.7 Fixtures de Autenticação E2E

Imports disponíveis:

typescript
// Base (não autenticado)
import { test, expect } from '../playwright-fixture';

// Autenticado por papel
import { 
  authenticatedTest,  // admin (multi-role)
  especialistaTest,
  coordenadorTest,
  diretorTest,
  escolaTest 
} from '../playwright-fixture';

Uso:

typescript
coordenadorTest('sidebar exibe mural', async ({ authedPage }) => {
  await authedPage.goto('/coordenacao/mural');
  await expect(authedPage.getByText('Mural')).toBeVisible();
});

Como funciona internamente:

  1. beforeEach do fixture faz POST /functions/v1/e2e-login com CPF + header X-E2E-Key
  2. Edge Function valida o shared secret (comparação simples, sem Argon2id)
  3. Extrai cookie olp_auth do header Set-Cookie
  4. Injeta cookie no contexto do browser via context.addCookies()
  5. authedPage é a page com cookie configurado — navegação autenticada

5. Usuários de Teste

Todos usam a senha do secret E2E_TEST_PASSWORD. CPFs são matematicamente válidos.

NOTA: Os IDs (UUIDs) dos usuários e escolas são gerados pela UI no staging — não são determinísticos. Os testes fazem lookup por codigo (CPF/CNPJ) via e2e-login e extraem escola_id do response em runtime.

5.1 Escolas

NomeCNPJMural SlugUso
Escola Municipal Monteiro Lobato77457094000157monteiro-lobatoEscola principal de testes
Colegio Particular Nova Eranova-eraIDOR cross-escola
Rede Educacional FuturoMulti-role (coord vinculado)

5.2 Escola A — Monteiro Lobato

PapelNomeCódigoTipo
AdminDev Admin42970698064cpf
EspecialistaDev Especialista63100416066cpf
CoordenadorDev Coordenador99407464075cpf
DiretorDev Diretor61626069026cpf
EscolaDev Escola77457094000157cnpj
Multi-roleDev Testes Multi Role40750810017cpf

5.3 Escola B — Nova Era (IDOR)

PapelNomeCódigoTipo
CoordenadorDev Coordenador da Nova Era47691226080cpf

5.4 Regra do Usuário Escola

O usuário com papel escola não é uma pessoa — é o acesso institucional da escola. Seu codigo (CNPJ ou INEP) é idêntico ao da escola vinculada. Não se deve atribuir pessoalidade a este perfil.

5.5 Multi-Role

O usuário "Dev Testes Multi Role" (CPF 40750810017) possui 9+ papéis:

  • Com tela: administrador, especialista, coordenador (×3 escolas), diretor, escola
  • Sem tela: pedagogico, professor

Os testes validam que papéis sem tela geram bloqueios (PERFIL_SEM_TELA).

5.6 Papéis no Banco

Nome no bancoID
administrador00000000-0000-0000-0000-000000000101
especialista00000000-0000-0000-0000-000000000102
coordenador00000000-0000-0000-0000-000000000103
diretor00000000-0000-0000-0000-000000000104
escola00000000-0000-0000-0000-000000000105
escola_trial00000000-0000-0000-0000-000000000106
pedagogico00000000-0000-0000-0000-000000000107
professor00000000-0000-0000-0000-000000000108

6. Workflow de Desenvolvimento

text
1. Escreve teste
2. Roda no sandbox (produção) → testes Deno via supabase--test_edge_functions
3. Passa? → commit
4. CI roda em staging:
   a. lint-and-build
   b. deploy-staging (supabase db push + functions deploy)
   c. contract-tests (Vitest HTTP contra staging) ← smoke test
   d. security-tests (IDOR cross-escola contra staging) ← bloqueador
   e. e2e (Playwright contra staging.olp.digital) ← só se tudo passa
5. Falha no CI? → Diagnosticar:
   - 404 "Usuário não encontrado" → seed faltando no staging
   - 500 em auth → BUG no catch block da função (corrigir a função, não o teste)
   - 401 inesperado → OLP_JWT_SECRET do staging não bate com o Supabase staging
   - Timeout → cold start, ajustar timeout ou retry

7. Pipeline CI

text
lint-and-build → deploy-staging → contract-tests → security-tests → e2e
                                   (sequencial)     (bloqueador)     (só se tudo passa)

O job contract-tests serve como smoke test pós-deploy: se as Edge Functions não respondem corretamente a CORS e auth guards, os testes E2E vão falhar de qualquer forma — melhor falhar rápido e barato.

O job security-tests valida IDOR cross-escola com JWTs reais — se houver vazamento de dados entre escolas, o pipeline trava antes do E2E.


8. Testes de Segurança

8.1 IDOR Cross-Escola

Localização: tests/security/idor-cross-escola.test.ts

Valida que um usuário autenticado da Escola A não consegue acessar dados da Escola B:

  • Login como Coordenador Monteiro Lobato → JWT com escola_id da Monteiro Lobato
  • Login como Coordenador Nova Era → JWT com escola_id da Nova Era
  • Com JWT Monteiro Lobato, tentar operar sobre dados da Nova Era → espera resultado específico por tipo de proteção

Os escola_id são extraídos dinamicamente do response do e2e-login — sem UUIDs hardcoded.

Expectativas por endpoint:

EndpointProteçãoExpectativa
escola-dados getQuery scoped por JWT200 com id === minha_escola_id
tarefas-escola listRLS200 com array vazio ou escola_id === minha_escola_id
inscricoes-olimpiada listRLS + query200 com array vazio
gestao-resultados listOwnership check403 ou 200 com array vazio

8.2 Paridade Staging/Produção

Os testes de contrato HTTP (tests/contracts/auth-guards.contract.test.ts) rodam contra staging no CI. Se passam em produção (via Deno) mas falham em staging → problema de infra staging (secret, seed, worker config).

Validações de paridade:

  • POST sem cookie → 401 (não 500)
  • POST com token inválido → 401
  • CORS headers presentes em todas as respostas

9. Isolamento de Secrets por Ambiente

REGRA CRÍTICA DE SEGURANÇA: Cada ambiente (produção, staging) DEVE ter seu próprio OLP_JWT_SECRET, diferente entre si.

text
CORRETO:
  OLP_JWT_SECRET do staging ≠ OLP_JWT_SECRET da produção
  OLP_JWT_SECRET do staging == JWT secret do projeto Supabase de staging
  OLP_JWT_SECRET da produção == JWT secret do projeto Supabase de produção

ERRADO:
  OLP_JWT_SECRET do staging == OLP_JWT_SECRET da produção  ← CROSS-ASSIGNMENT, INSEGURO

Se um JWT de produção funcionar em staging (ou vice-versa), significa que os secrets são iguais — isso é uma vulnerabilidade de segurança.


10. Checklist Pré-CI (Staging)

  • [ ] Usuários de teste criados no staging (ver docs/staging/seed_test_users.sql para referência)
  • [ ] Secret E2E_TEST_PASSWORD configurado no Supabase staging
  • [ ] Secret OLP_JWT_SECRET próprio do staging (diferente de produção), sincronizado com o JWT secret do projeto Supabase de staging
  • [ ] Worker do Cloudflare configurado para staging.olp.digital com rewrite de cookies
  • [ ] staging.olp.digital acessível publicamente
  • [ ] Edge Functions deployadas (via CI ou manual supabase functions deploy)

10.1 GitHub Secrets Necessários para CI

Os secrets devem ser criados como Repository secrets (Settings → Secrets and variables → Actions → Repository secrets).

SecretValorObrigatório
SUPABASE_ACCESS_TOKENToken pessoal do Supabase CLISim
STAGING_PROJECT_REFRef do projeto Supabase de stagingSim
STAGING_SUPABASE_URLURL do projeto Supabase de stagingSim
STAGING_SUPABASE_ANON_KEYAnon key do projeto Supabase de stagingSim
E2E_TEST_PASSWORDMesmo valor do secret no Supabase stagingSim
NTFY_TOPIC_URLURL completa do tópico ntfy para notificações CIOpcional

⚠️ secrets.* em if:: GitHub Actions não aceita secrets.X diretamente em expressões if: de steps. O padrão correto é injetar o secret em env: do job e checar a variável de ambiente no shell.


11. Troubleshooting

ErroCausa ProvávelFix
404 "Usuário não encontrado"Usuário de teste não existe no banco stagingExecutar seed_test_users.sql
500 em auth (sem cookie)Catch block da função não mapeia erro de auth para 401Corrigir a função — adicionar Token→401, Acesso negado→403 no catch
401 inesperado (com cookie válido)OLP_JWT_SECRET do staging não bate com o Supabase stagingVerificar que o secret do staging bate com o JWT secret do projeto Supabase de staging
CORS bloqueadoWorker não configurado para stagingVerificar regras do Worker
Timeout no PlaywrightCold start de Edge FunctionAumentar timeout ou adicionar warmup
"SecurityError" em localStorageCI headless bloqueia localStorage cross-originProteger com try/catch (já implementado)

12. Regras de Segurança para Testes

12.1 Sem Fallback para Produção

Testes remotos (contrato HTTP, IDOR) nunca devem ter fallback silencioso para URL de produção:

typescript
// ❌ ERRADO — se env não vier, roda contra produção sem aviso
const URL = process.env.VITE_SUPABASE_URL || 'https://prod.supabase.co';

// ✅ CORRETO — falha explícita se env não configurado
const URL = process.env.VITE_SUPABASE_URL;
if (!URL) throw new Error('VITE_SUPABASE_URL obrigatório');

12.2 Matriz de Status HTTP

text
401 = não autenticado (token ausente, inválido, revogado)
403 = sem permissão (IDOR, papel insuficiente, acesso negado)
500 = defeito interno (bug real, crash, erro de infra)

O padrão correto no catch block das Edge Functions:

typescript
} catch (error) {
  const msg = (error as Error).message;
  if (msg.includes('Token')) {
    return new Response(JSON.stringify({ success: false, message: '...' }), { status: 401 });
  }
  if (msg.includes('Acesso negado') || msg.includes('Não autorizado')) {
    return new Response(JSON.stringify({ success: false, message: '...' }), { status: 403 });
  }
  return new Response(JSON.stringify({ success: false, message: 'Erro interno.' }), { status: 500 });
}

13. Estrutura de Diretórios

text
tests/
├── contracts/                         # Contrato HTTP (Vitest, CI contra staging)
│   └── auth-guards.contract.test.ts
├── security/                          # Testes de segurança (Vitest, CI contra staging)
│   └── idor-cross-escola.test.ts
e2e/
├── fixtures/
│   ├── auth.ts                        # Fixture de autenticação
│   ├── constants.ts                   # Usuários, URLs, timeouts
│   └── helpers.ts                     # Helpers de navegação
├── *.spec.ts                          # Specs E2E
supabase/functions/<nome>/
└── index.test.ts                      # Contrato Deno (dev, produção)
docs/development/
└── TESTING_MASTER.md                  # Este documento
docs/staging/
└── seed_test_users.sql                # Seed idempotente para staging

14. Cobertura de Contratos Deno por Prioridade

PrioridadeFunçõesJustificativa
P0admin-faturas, escola-pagamentos, admin-assinaturasFinanceiro
P0admin-escolas, escola-dados, escola-dashboardDados core
P1gestao-turmas, gestao-responsaveis, diretor-dashboardOperacional
P1admin-dashboard, admin-logs, admin-sms-logsMonitoramento
P2especialista-* (6 funções)Conteúdo
P2coordenador-videos, notificacoesComplementar
Skiphealthcheck-cron, maintenance-cron, benchmark-argon2, e2e-login, auth-diagnosticsInfraestrutura/utilitário

15. Referências


16. Limitações Conhecidas e Inventário E2E

16.1 Limitações de Ambiente

LimitaçãoMotivoWorkaround
Portal aluno/responsávelAuth via matrícula+DN ou OTP WhatsApp, não via senhaTestes mantidos como test.skip
Definir senhaRequer usuário sem senha_hash — destrutivoTestes como test.skip, rodar manualmente
Seed de dadosTestes de CRUD precisam de dados pré-existentesCriar factories ou usar dados existentes do dev
Rate limitverify-password tem lockout progressivoUsar senhas corretas; em massa, espaçar requests
Cold startEdge Functions podem ter latência no primeiro requestTimeout generoso (TIMEOUTS.login = 30s)

16.2 Onde os Testes E2E Rodam

AmbienteFunciona?Motivo
Sandbox Lovable✅ SimAuth Lovable é transparente
GitHub Actions + staging✅ SimSem auth gate, banco isolado
GitHub Actions + preview Lovable❌ NãoAuth gate Lovable bloqueia
GitHub Actions + produção⚠️ PossívelPúblico, mas risco de side effects

16.3 Inventário de Cobertura E2E

Pós-migração Fases 1-5 (2026-04-04): 85 testes migrados para Vitest, 10 removidos (duplicados). Ver TEST_MIGRATION_REPORT.md para detalhes.

Ativos (16 testes):

SpecTestesAuth Fixture
login-senha.spec.ts6Nenhum (testa login)
agenda-coordenador.spec.ts4coordenadorTest
comunicacao-coordenador.spec.ts5coordenadorTest
loading-gate.spec.ts1authenticatedTest + test

Skipados — aguardando staging (89 testes):

SpecSkipJustificativa principal
portal-aluno-dashboard.spec.ts28Cookie olp_mural, login matrícula+DN, CSS layout
portal-responsavel.spec.ts24OTP WhatsApp, cookie olp_mural, lockout timing
mural-coordenador.spec.ts19Sidebar/routing real, CRUD + upload, clipboard
olimpiada-detalhes.spec.ts15Skeleton, drag-and-drop, multi-tab routing
loading-gate.spec.ts2Multi-role rendering
login-senha.spec.ts1Rate limit / lockout

Removido:

SpecMotivo
definir-senha.spec.tsRemovido na Fase 1 — 100% coberto por modal-definir-senha.test.tsx