Files
vinihcrosaandClaude Sonnet 4.6 8539f1a46a test(web): configure Playwright E2E + Vitest unit testing with 80% coverage rule
- Add Playwright (Chromium) for E2E tests with webServer auto-start
- Add Vitest + @testing-library/react + coverage-v8 with 80% threshold
- Add smoke E2E spec and jest-dom setup file
- Update turbo.json with test:e2e task
- Enforce 80% coverage rule in AGENTS.md (global testing policy)
- Add full Testes section to docs/conventions/frontend.md
- Add tasks.md for scenario-creator feature (14 tasks, T01–T14)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 03:22:21 -03:00

8.0 KiB

Frontend Conventions — apps/web

Stack

  • Framework: React 18 + TypeScript
  • Build: Vite 6
  • Estado global: Zustand
  • Tipos compartilhados: @ymir/types
  • Workers: Web Workers nativos (ES modules)

Estrutura de Pastas

src/
├── features/              # Código organizado por domínio
│   ├── simulation/
│   │   ├── components/    # Componentes específicos da feature
│   │   ├── hooks/         # Hooks específicos da feature
│   │   └── store.ts       # Zustand store da feature
│   └── vessels/
│       ├── components/
│       ├── hooks/
│       └── store.ts
├── components/            # Componentes genéricos reutilizáveis (sem domínio)
├── hooks/                 # Hooks genéricos compartilhados
├── stores/                # Stores globais (cross-feature)
├── workers/               # Web Workers
├── lib/                   # Utilitários puros (sem React)
└── main.tsx

Features são verticais autossuficientes: um componente de simulation/ não importa de vessels/ diretamente — dados compartilhados passam por store global.


Componentes

Componentes são burros: recebem dados via props e disparam ações via callbacks ou stores. Sem fetching, sem lógica de negócio, sem efeitos colaterais dentro de componentes.

// ✅ correto
function VesselCard({ vessel, onSelect }: VesselCardProps) {
  return <div onClick={() => onSelect(vessel.id)}>{vessel.name}</div>
}

// ❌ errado — componente buscando dados
function VesselCard({ id }: { id: number }) {
  const [vessel, setVessel] = useState(null)
  useEffect(() => { fetch(`/vessels/${id}`).then(...) }, [id])
  ...
}

Lógica de busca de dados fica em custom hooks:

// hooks/use-vessel.ts
export function useVessel(id: number) {
  const [vessel, setVessel] = useState<VesselDTO | null>(null)
  useEffect(() => {
    fetch(`/api/vessels/${id}`)
      .then(r => r.json())
      .then(setVessel)
  }, [id])
  return vessel
}

Estado

Regras:

Tipo de estado Onde fica
Estado de UI puro (modal aberto, hover, input temporário) useState local
Estado de feature (simulação rodando, lista de vessels) Zustand store da feature
Estado cross-feature Zustand store global em stores/
Cache de dados remotos Custom hook com useState (ou SWR futuramente)

Sem prop drilling além de 2 níveis — extrair para store. Sem Context API para estado de aplicação — usar Zustand.

Estrutura de store:

// features/simulation/store.ts
import { create } from 'zustand'

interface SimulationStore {
  // estado
  status: 'idle' | 'running' | 'error'
  // ações
  start: () => void
  stop: () => void
}

export const useSimulationStore = create<SimulationStore>((set, get) => ({
  status: 'idle',
  start: () => set({ status: 'running' }),
  stop:  () => set({ status: 'idle' }),
}))

Custom Hooks

  • Prefixo use obrigatório
  • Co-localizados com a feature quando específicos
  • Em hooks/ quando reutilizáveis entre features
  • Encapsulam lógica — componentes não fazem useEffect com fetch direto
// ✅ hook com responsabilidade clara
export function useVesselList(): { vessels: VesselDTO[]; loading: boolean } {
  ...
}

// ✅ hook de store — apenas re-exporta slice relevante
export function useSimulationStatus() {
  return useSimulationStore(s => s.status)
}

Naming

Coisa Padrão Exemplo
Arquivos de componente PascalCase.tsx TelemetryPanel.tsx
Arquivos de hook use-kebab-case.ts use-vessel-list.ts
Arquivos de store store.ts (na feature) features/simulation/store.ts
Arquivos utilitários kebab-case.ts format-number.ts
Componentes PascalCase VesselCard
Hooks use[Ação/Entidade] useVesselList, useSimulationStatus
Stores use[Domain]Store useSimulationStore
Props types [Componente]Props VesselCardProps

TypeScript

  • import type obrigatório para importações tipo-only
  • Sem any — exceção: boundary com WASM (documentar com comentário)
  • Props sempre tipadas com interface ou type alias explícito — sem inline objects grandes
  • Sem ! non-null assertion onde pode ser evitado
  • Sem enum — usar as const ou union types
// ✅
import type { VesselDTO } from '@ymir/types'

interface VesselCardProps {
  vessel: VesselDTO
  onSelect: (id: number) => void
}

// ❌
function VesselCard(props: any) { ... }

Organização de Arquivos de Componente

Para componentes simples: um arquivo .tsx. Para componentes com lógica complexa: co-locar hook e tipos:

features/simulation/components/
├── TelemetryPanel.tsx       # componente
├── TelemetryPanel.hook.ts   # lógica extraída (quando > ~30 linhas)
└── TelemetryPanel.types.ts  # tipos locais (quando extensos)

Regras Gerais

  • Arquivo máximo: ~200 linhas — dividir se passar
  • Sem barrel index.ts dentro de features (importar direto do arquivo)
  • Barrel index.ts permitido em components/ e hooks/ globais
  • Sem console.log em código de produção
  • Ordenação de imports: externos → @ymir/* → features/ → components/ → relativos
  • Componentes funcionais apenas — sem class components

Testes

Obrigatoriedade

Toda feature de UI deve ter testes antes de ser considerada completa.
Cobertura mínima: 80% em linhas, funções, branches e statements.
O build CI falha se os thresholds não forem atingidos (pnpm test retorna exit 1).

Dois níveis obrigatórios

Nível Ferramenta Onde O que testar
Unit / Component Vitest + Testing Library src/**/*.test.{ts,tsx} Lógica pura, hooks, stores, componentes isolados
E2E / Fluxo Playwright e2e/**/*.spec.ts Happy path e fluxos críticos no browser real

Convenções de testes unitários (Vitest)

// src/features/scenario-creator/store.test.ts
import { renderHook, act } from '@testing-library/react'
import { useScenarioStore } from './store'

describe('useScenarioStore', () => {
  beforeEach(() => {
    useScenarioStore.getState().reset()
  })

  it('adds a vessel to the scenario', () => {
    const { result } = renderHook(() => useScenarioStore())
    act(() => {
      result.current.addVessel({ vesselId: 1, name: 'VLCC' })
    })
    expect(result.current.vessels).toHaveLength(1)
    expect(result.current.vessels[0].vesselId).toBe(1)
  })
})
  • Arquivo de teste co-localizado com o arquivo testado: Foo.tsx → Foo.test.tsx
  • describe agrupa por módulo/componente; it descreve comportamento observável
  • Mocks externos (Leaflet, fetch, WASM) em src/test/mocks/
  • Setup global em src/test/setup.ts (já configurado)
  • Rodar: pnpm --filter @ymir/web test

Convenções de testes E2E (Playwright)

// e2e/scenario-creator.spec.ts
import { test, expect } from '@playwright/test'

test('instrutor cria e inicia simulação', async ({ page }) => {
  await page.goto('/')
  await page.getByRole('combobox', { name: /área/i }).selectOption('baia_de_guanabara')
  await page.getByRole('button', { name: /adicionar vessel/i }).click()
  await page.getByRole('button', { name: /iniciar/i }).click()
  await expect(page.getByText(/rodando/i)).toBeVisible()
})
  • Usar getByRole / getByText / getByLabel — nunca seletores CSS frágeis
  • Um arquivo por feature: e2e/scenario-creator.spec.ts
  • Toda feature nova: ao menos um spec cobrindo o happy path
  • Rodar: pnpm --filter @ymir/web test:e2e
  • Config em apps/web/playwright.config.ts (Chromium, webServer automático)

Comandos

pnpm --filter @ymir/web test          # unit + coverage (CI gate)
pnpm --filter @ymir/web test:watch    # modo watch (desenvolvimento)
pnpm --filter @ymir/web test:e2e      # E2E headless
pnpm --filter @ymir/web test:e2e:ui   # E2E com UI do Playwright