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>
This commit is contained in:
1 parent
078af243c8
commit
8539f1a46a
10 files changed
+1605
-3
No files matched your search
@@ -139,12 +139,60 @@ Use the bounded-context include prefix. Examples:
|
||||
|
||||
## Testing
|
||||
|
||||
### Regra global — toda feature deve ser testada
|
||||
|
||||
**Cobertura mínima obrigatória: 80%** em linhas, funções, branches e statements.
|
||||
Nenhuma feature é considerada completa sem testes que satisfaçam este limiar.
|
||||
|
||||
### C++ (libs/physics, libs/simulation, libs/vessel, …)
|
||||
|
||||
- Every force module has a unit test before it is integrated into `RHS`
|
||||
- Unit tests use analytical solutions or known limits (e.g., zero current → zero current force)
|
||||
- Integration tests run a full simulation and assert on conserved quantities or known maneuver behavior
|
||||
- Test files mirror source structure: `tests/physics/forces/TestCurrentForces.cpp`
|
||||
- Test executables are defined in `tests/CMakeLists.txt` and linked via `Ymir::*` targets
|
||||
- Coverage target: ≥80% line coverage measured via `-DYMIR_ENABLE_COVERAGE=ON`
|
||||
- Coverage measured via `-DYMIR_ENABLE_COVERAGE=ON` (gcov/llvm-cov)
|
||||
|
||||
### Frontend (apps/web)
|
||||
|
||||
Dois níveis de teste, ambos obrigatórios para features de UI:
|
||||
|
||||
| Nível | Ferramenta | Localização | O que testa |
|
||||
|-------|-----------|-------------|-------------|
|
||||
| Unit / Component | **Vitest** + Testing Library | `src/**/*.test.{ts,tsx}` | Lógica pura, hooks, stores, componentes isolados |
|
||||
| E2E / Flow | **Playwright** | `e2e/**/*.spec.ts` | Fluxos completos de usuário no browser |
|
||||
|
||||
**Comandos:**
|
||||
|
||||
```bash
|
||||
# Unit tests + cobertura (enforces 80% threshold — falha abaixo)
|
||||
pnpm --filter @ymir/web test
|
||||
|
||||
# E2E (requer servidor rodando ou usa webServer do playwright.config.ts)
|
||||
pnpm --filter @ymir/web test:e2e
|
||||
|
||||
# Modo watch (desenvolvimento)
|
||||
pnpm --filter @ymir/web test:watch
|
||||
```
|
||||
|
||||
**Regras de cobertura:**
|
||||
|
||||
- Thresholds configurados em `apps/web/vitest.config.ts` — `lines: 80, functions: 80, branches: 80`
|
||||
- `pnpm test` falha se qualquer threshold não for atingido (bloqueante em CI)
|
||||
- Excluções válidas: `src/main.tsx`, arquivos `*.d.ts`, barris `index.ts` sem lógica
|
||||
- Mocks de módulos externos (Leaflet, WASM) são aceitos e não contam contra cobertura
|
||||
|
||||
**Playwright:**
|
||||
|
||||
- Configuração em `apps/web/playwright.config.ts`
|
||||
- Browser: Chromium (headless em CI, headed em dev)
|
||||
- `webServer` inicia `vite dev` automaticamente antes dos testes
|
||||
- Toda feature nova deve ter ao menos um teste E2E cobrindo o happy path
|
||||
|
||||
### Backend (apps/api)
|
||||
|
||||
- Testes unitários de serviços e repositórios: a definir quando volume justificar framework
|
||||
- Enquanto não configurado: testar manualmente via `curl` e documentar no `SUMMARY.md` da task
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('app loads without errors', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await expect(page).not.toHaveURL(/error/)
|
||||
// Basic smoke: page title or root element renders
|
||||
await expect(page.locator('body')).toBeVisible()
|
||||
})
|
||||
+13
-2
@@ -7,7 +7,11 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"clean": "rm -rf dist"
|
||||
"clean": "rm -rf dist coverage",
|
||||
"test": "vitest run --coverage",
|
||||
"test:watch": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ymir/types": "workspace:*",
|
||||
@@ -16,10 +20,17 @@
|
||||
"zustand": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"@vitest/coverage-v8": "^4.1.9",
|
||||
"jsdom": "^29.1.1",
|
||||
"typescript": "^5.8.0",
|
||||
"vite": "^6.3.0"
|
||||
"vite": "^6.3.0",
|
||||
"vitest": "^4.1.9"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
|
||||
use: {
|
||||
baseURL: 'http://localhost:5173',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
|
||||
// Start the Vite dev server before running tests
|
||||
webServer: {
|
||||
command: 'pnpm dev',
|
||||
url: 'http://localhost:5173',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 30_000,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom'
|
||||
@@ -0,0 +1,37 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['e2e/**', 'node_modules/**'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'lcov', 'html'],
|
||||
reportsDirectory: './coverage',
|
||||
include: ['src/**/*.{ts,tsx}'],
|
||||
exclude: [
|
||||
'src/test/**',
|
||||
'src/main.tsx',
|
||||
'src/**/*.d.ts',
|
||||
'src/**/index.ts',
|
||||
],
|
||||
thresholds: {
|
||||
lines: 80,
|
||||
functions: 80,
|
||||
branches: 80,
|
||||
statements: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@ymir/types': resolve(__dirname, '../../packages/types/src/index.ts'),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -189,3 +189,79 @@ features/simulation/components/
|
||||
- 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)
|
||||
|
||||
```typescript
|
||||
// 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)
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
@@ -0,0 +1,485 @@
|
||||
# Scenario Creator — Tasks
|
||||
|
||||
**Spec**: `docs/specs/features/scenario-creator/spec.md`
|
||||
**Design**: `docs/specs/features/scenario-creator/design.md`
|
||||
**Status**: Pending
|
||||
|
||||
---
|
||||
|
||||
## Dependências externas a instalar antes de começar
|
||||
|
||||
```bash
|
||||
pnpm --filter @ymir/web add leaflet react-leaflet @types/leaflet
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mapa de dependências
|
||||
|
||||
```
|
||||
T01 (geo.ts)
|
||||
└─► T04 (AreaMapView)
|
||||
└─► T07 (ScenarioCreatorPage)
|
||||
|
||||
T02 (useScenarioStore)
|
||||
└─► T03 (useAreas / useVessels hooks)
|
||||
└─► T04 (AreaMapView)
|
||||
└─► T05 (ScenarioForm)
|
||||
└─► T06 (VesselList)
|
||||
|
||||
T05, T06, T08 (SimulationControls) ──► T09 (Sidebar)
|
||||
└─► T07 (ScenarioCreatorPage)
|
||||
|
||||
T10 (extend simulationStore) ──► T07
|
||||
T11 (extend simulation.worker) ──► T10
|
||||
|
||||
T07 ──► T12 (App.tsx swap)
|
||||
T07 ──► T13 (unit tests)
|
||||
T12 ──► T14 (E2E tests)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
---
|
||||
|
||||
### T01 — `geo.ts`: conversão lat/lng ↔ metros
|
||||
|
||||
**Requisitos cobertos**: SC-P1-01, SC-P1-02
|
||||
|
||||
**O que**: Criar utilitário de projeção equiretangular local.
|
||||
|
||||
**Onde**:
|
||||
- `apps/web/src/lib/geo.ts` (criar)
|
||||
- `apps/web/src/lib/geo.test.ts` (criar)
|
||||
|
||||
**Depende de**: nada
|
||||
|
||||
**Reutiliza**: nada
|
||||
|
||||
**Interfaces**:
|
||||
|
||||
```typescript
|
||||
export function latlngToMeters(
|
||||
lat: number, lng: number,
|
||||
origin: { latitude: number; longitude: number }
|
||||
): { x: number; y: number }
|
||||
|
||||
export function metersToLatLng(
|
||||
x: number, y: number,
|
||||
origin: { latitude: number; longitude: number }
|
||||
): [number, number] // [lat, lng]
|
||||
|
||||
export function isInsidePolygon(
|
||||
x: number, y: number,
|
||||
polygonMeters: Array<{ x: number; y: number }>
|
||||
): boolean
|
||||
```
|
||||
|
||||
**Done when**:
|
||||
- [ ] `latlngToMeters` e `metersToLatLng` são inversas (round-trip error < 1m em 100km)
|
||||
- [ ] `isInsidePolygon` usa ray-casting algorithm
|
||||
- [ ] Cobertura ≥ 80% no módulo
|
||||
|
||||
**Gate**: `pnpm --filter @ymir/web test -- src/lib/geo.test.ts`
|
||||
|
||||
---
|
||||
|
||||
### T02 — `useScenarioStore`: estado do rascunho de cenário
|
||||
|
||||
**Requisitos cobertos**: SC-P1-02, SC-P1-03, SC-P2-01
|
||||
|
||||
**O que**: Criar Zustand store para o cenário em construção.
|
||||
|
||||
**Onde**:
|
||||
- `apps/web/src/features/scenario-creator/store.ts` (criar)
|
||||
- `apps/web/src/features/scenario-creator/store.test.ts` (criar)
|
||||
|
||||
**Depende de**: `@ymir/types` (AreaDTO, CreateScenarioDTO)
|
||||
|
||||
**Reutiliza**: padrão de `stores/simulationStore.ts`
|
||||
|
||||
**Interface do store**:
|
||||
|
||||
```typescript
|
||||
interface ScenarioDraftVessel {
|
||||
vesselId: number
|
||||
name: string
|
||||
x: number // metros, relativo à origem da área
|
||||
y: number
|
||||
headingDeg: number
|
||||
}
|
||||
|
||||
interface ScenarioStore {
|
||||
name: string
|
||||
areaId: number | null
|
||||
area: AreaDTO | null
|
||||
vessels: ScenarioDraftVessel[]
|
||||
|
||||
setName: (name: string) => void
|
||||
setArea: (area: AreaDTO) => void
|
||||
addVessel: (v: Pick<ScenarioDraftVessel, 'vesselId' | 'name'>) => void
|
||||
removeVessel: (vesselId: number) => void
|
||||
updateVesselPosition: (vesselId: number, x: number, y: number) => void
|
||||
updateVesselHeading: (vesselId: number, headingDeg: number) => void
|
||||
reset: () => void
|
||||
toCreateScenarioDTO: () => CreateScenarioDTO
|
||||
}
|
||||
```
|
||||
|
||||
**Done when**:
|
||||
- [ ] Todas as ações mudam estado corretamente
|
||||
- [ ] `toCreateScenarioDTO()` serializa `initialConditions` com `psi = headingDeg * π/180`
|
||||
- [ ] Nome padrão gerado como `"Cenário ${new Date().toLocaleDateString('pt-BR')}"`
|
||||
- [ ] Cobertura ≥ 80%
|
||||
|
||||
**Gate**: `pnpm --filter @ymir/web test -- src/features/scenario-creator/store.test.ts`
|
||||
|
||||
---
|
||||
|
||||
### T03 — Hooks de dados: `useAreas` e `useVessels`
|
||||
|
||||
**Requisitos cobertos**: SC-P1-01, SC-P1-02
|
||||
|
||||
**O que**: Dois hooks que buscam dados da API.
|
||||
|
||||
**Onde**:
|
||||
- `apps/web/src/features/scenario-creator/hooks/use-areas.ts` (criar)
|
||||
- `apps/web/src/features/scenario-creator/hooks/use-vessels.ts` (criar)
|
||||
- `apps/web/src/features/scenario-creator/hooks/use-areas.test.ts` (criar)
|
||||
- `apps/web/src/features/scenario-creator/hooks/use-vessels.test.ts` (criar)
|
||||
|
||||
**Depende de**: `@ymir/types`
|
||||
|
||||
**Interfaces**:
|
||||
|
||||
```typescript
|
||||
// use-areas.ts
|
||||
export function useAreas(): {
|
||||
areas: AreaDTO[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
retry: () => void
|
||||
}
|
||||
|
||||
// use-vessels.ts
|
||||
export function useVessels(): {
|
||||
vessels: VesselDTO[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
```
|
||||
|
||||
**Done when**:
|
||||
- [ ] Fetch via `VITE_API_URL` env var (fallback `http://localhost:3000`)
|
||||
- [ ] Estado `loading` correto durante fetch
|
||||
- [ ] `error` populado em caso de falha de rede
|
||||
- [ ] `retry()` reexecuta o fetch
|
||||
- [ ] Testes com `fetch` mockado cobrem: success, loading, error
|
||||
- [ ] Cobertura ≥ 80%
|
||||
|
||||
**Gate**: `pnpm --filter @ymir/web test -- src/features/scenario-creator/hooks/`
|
||||
|
||||
---
|
||||
|
||||
### T04 — `AreaMapView` + `VesselMarker`: mapa com marcadores
|
||||
|
||||
**Requisitos cobertos**: SC-P1-01, SC-P1-02, SC-P1-03, SC-P2-02
|
||||
|
||||
**O que**: Componente Leaflet com polígono da área e marcadores draggáveis.
|
||||
|
||||
**Onde**:
|
||||
- `apps/web/src/features/scenario-creator/components/AreaMapView.tsx` (criar)
|
||||
- `apps/web/src/features/scenario-creator/components/VesselMarker.tsx` (criar)
|
||||
- `apps/web/src/features/scenario-creator/components/AreaMapView.test.tsx` (criar)
|
||||
|
||||
**Depende de**: T01 (geo.ts), T02 (useScenarioStore), `react-leaflet`, `leaflet`
|
||||
|
||||
**Instalar antes**:
|
||||
```bash
|
||||
pnpm --filter @ymir/web add leaflet react-leaflet @types/leaflet
|
||||
```
|
||||
|
||||
**CSS do Leaflet**: importar `leaflet/dist/leaflet.css` em `main.tsx`.
|
||||
|
||||
**Done when**:
|
||||
- [ ] Mapa renderiza tile OSM centrado na origem da área selecionada
|
||||
- [ ] Polígono da área desenhado em destaque (cor distinta, sem preenchimento sólido)
|
||||
- [ ] Bounds do mapa ajustados ao polígono na seleção da área
|
||||
- [ ] Cada vessel em `useScenarioStore.vessels` tem marcador no mapa
|
||||
- [ ] Drag do marcador chama `updateVesselPosition` com coords em metros
|
||||
- [ ] Durante simulação ativa (`status === 'running'`): marcadores seguem `SimulationStateDTO`; drag desabilitado
|
||||
- [ ] Marcadores com heading rotacionado visualmente
|
||||
- [ ] Cobertura ≥ 80% (mock `react-leaflet` em testes unitários)
|
||||
|
||||
**Nota**: Leaflet precisa de `height` explícito no container CSS — usar `height: 100%` com parent `100vh`.
|
||||
|
||||
**Gate**: `pnpm --filter @ymir/web test -- src/features/scenario-creator/components/AreaMapView.test.tsx`
|
||||
|
||||
---
|
||||
|
||||
### T05 — `ScenarioForm`: nome do cenário + seletor de área
|
||||
|
||||
**Requisitos cobertos**: SC-P1-01, SC-P2-01
|
||||
|
||||
**O que**: Formulário com campo de nome e dropdown de áreas.
|
||||
|
||||
**Onde**:
|
||||
- `apps/web/src/features/scenario-creator/components/ScenarioForm.tsx` (criar)
|
||||
- `apps/web/src/features/scenario-creator/components/ScenarioForm.test.tsx` (criar)
|
||||
|
||||
**Depende de**: T02 (useScenarioStore), T03 (useAreas)
|
||||
|
||||
**Done when**:
|
||||
- [ ] Input de nome controlado por `useScenarioStore.setName`
|
||||
- [ ] Dropdown lista áreas de `useAreas()` — cada option tem `value={area.id}`
|
||||
- [ ] Seleção de área chama `useScenarioStore.setArea(selectedArea)`
|
||||
- [ ] Estado `loading` exibe "Carregando áreas..."
|
||||
- [ ] Estado `error` exibe mensagem + botão retry
|
||||
- [ ] Cobertura ≥ 80%
|
||||
|
||||
**Gate**: `pnpm --filter @ymir/web test -- src/features/scenario-creator/components/ScenarioForm.test.tsx`
|
||||
|
||||
---
|
||||
|
||||
### T06 — `VesselList`: lista de vessels + seletor
|
||||
|
||||
**Requisitos cobertos**: SC-P1-02, SC-P1-03
|
||||
|
||||
**O que**: Lista vessels adicionados ao cenário; permite adicionar, remover e editar heading.
|
||||
|
||||
**Onde**:
|
||||
- `apps/web/src/features/scenario-creator/components/VesselList.tsx` (criar)
|
||||
- `apps/web/src/features/scenario-creator/components/VesselList.test.tsx` (criar)
|
||||
|
||||
**Depende de**: T02 (useScenarioStore), T03 (useVessels)
|
||||
|
||||
**Done when**:
|
||||
- [ ] Botão "Adicionar vessel" desabilitado quando `area === null`
|
||||
- [ ] Clique abre seletor com vessels de `useVessels()`
|
||||
- [ ] Seleção chama `addVessel({ vesselId, name })`
|
||||
- [ ] Cada item mostra: nome, x/y arredondados, input de heading (0–360)
|
||||
- [ ] Input de heading chama `updateVesselHeading` e normaliza para [0, 360)
|
||||
- [ ] Botão remover chama `removeVessel(vesselId)`
|
||||
- [ ] Cobertura ≥ 80%
|
||||
|
||||
**Gate**: `pnpm --filter @ymir/web test -- src/features/scenario-creator/components/VesselList.test.tsx`
|
||||
|
||||
---
|
||||
|
||||
### T07 — `SimulationControls`: iniciar / parar + status
|
||||
|
||||
**Requisitos cobertos**: SC-P1-04
|
||||
|
||||
**O que**: Botões de controle da simulação e indicador de status.
|
||||
|
||||
**Onde**:
|
||||
- `apps/web/src/features/scenario-creator/components/SimulationControls.tsx` (criar)
|
||||
- `apps/web/src/features/scenario-creator/components/SimulationControls.test.tsx` (criar)
|
||||
|
||||
**Depende de**: T02 (useScenarioStore), `stores/simulationStore.ts`
|
||||
|
||||
**Done when**:
|
||||
- [ ] "Iniciar" desabilitado quando `areaId === null` ou `vessels.length === 0`
|
||||
- [ ] "Iniciar" chama `POST /scenarios` com `toCreateScenarioDTO()`, depois `simulationStore.start()`
|
||||
- [ ] "Parar" chama `simulationStore.stop()`
|
||||
- [ ] Indicador de status reflete `simulationStore.status` (idle/loading/running/error)
|
||||
- [ ] Exibe tempo de simulação `t` em segundos quando `running`
|
||||
- [ ] Cobertura ≥ 80%
|
||||
|
||||
**Gate**: `pnpm --filter @ymir/web test -- src/features/scenario-creator/components/SimulationControls.test.tsx`
|
||||
|
||||
---
|
||||
|
||||
### T08 — `Sidebar`: compõe ScenarioForm + VesselList + SimulationControls
|
||||
|
||||
**Requisitos cobertos**: todos (composição)
|
||||
|
||||
**O que**: Painel lateral que agrupa os três subcomponentes.
|
||||
|
||||
**Onde**:
|
||||
- `apps/web/src/features/scenario-creator/components/Sidebar.tsx` (criar)
|
||||
|
||||
**Depende de**: T05, T06, T07
|
||||
|
||||
**Done when**:
|
||||
- [ ] Renderiza `<ScenarioForm />`, `<VesselList />`, `<SimulationControls />` na ordem correta
|
||||
- [ ] Largura fixa (~320px), altura 100vh, scroll interno se necessário
|
||||
- [ ] Sem lógica de negócio — apenas composição e layout
|
||||
|
||||
**Gate**: inspeção visual (sem gate de teste unitário — lógica está nos filhos)
|
||||
|
||||
---
|
||||
|
||||
### T09 — `ScenarioCreatorPage`: layout raiz
|
||||
|
||||
**Requisitos cobertos**: todos
|
||||
|
||||
**O que**: Container full-screen que posiciona Sidebar + AreaMapView lado a lado.
|
||||
|
||||
**Onde**:
|
||||
- `apps/web/src/features/scenario-creator/ScenarioCreatorPage.tsx` (criar)
|
||||
|
||||
**Depende de**: T04, T08
|
||||
|
||||
**Done when**:
|
||||
- [ ] Layout `display: flex; height: 100vh` — Sidebar fixa + mapa ocupa o restante
|
||||
- [ ] Sem estado próprio — lê stores
|
||||
|
||||
**Gate**: inspeção visual
|
||||
|
||||
---
|
||||
|
||||
### T10 — Extender `useSimulationStore` com `loadScenario`
|
||||
|
||||
**Requisitos cobertos**: SC-P1-04
|
||||
|
||||
**O que**: Adicionar `loadScenario(vessels)` ao store existente para substituir o `addVessel(1)` hardcoded.
|
||||
|
||||
**Onde**:
|
||||
- `apps/web/src/stores/simulationStore.ts` (modificar)
|
||||
|
||||
**Depende de**: nada (T07 consome isso)
|
||||
|
||||
**Done when**:
|
||||
- [ ] `loadScenario(vessels: ScenarioDraftVessel[])` armazena vessels no store
|
||||
- [ ] `start()` usa vessels do store (não mais `addVessel(1)` hardcoded no worker init)
|
||||
- [ ] `reset()` limpa vessels
|
||||
|
||||
**Gate**: `pnpm --filter @ymir/web test -- src/stores/simulationStore.test.ts` (criar junto)
|
||||
|
||||
---
|
||||
|
||||
### T11 — Extender `simulation.worker.ts` com suporte a múltiplos vessels
|
||||
|
||||
**Requisitos cobertos**: SC-P1-04
|
||||
|
||||
**O que**: Worker recebe lista de vessels com condições iniciais reais.
|
||||
|
||||
**Onde**:
|
||||
- `apps/web/src/workers/simulation.worker.ts` (modificar)
|
||||
|
||||
**Depende de**: T10
|
||||
|
||||
**Done when**:
|
||||
- [ ] Worker aceita mensagem `{ type: 'loadScenario', vessels: ScenarioDraftVessel[] }`
|
||||
- [ ] Para cada vessel: chama `simulation.addVessel(id)` e (quando disponível) `simulation.setInitialConditions(id, x, y, psi)`
|
||||
- [ ] Remove chamada hardcoded `simulation.addVessel(1)` do `initWasm()`
|
||||
|
||||
**Gate**: `pnpm --filter @ymir/web test:e2e` smoke test passa
|
||||
|
||||
---
|
||||
|
||||
### T12 — Atualizar `App.tsx`
|
||||
|
||||
**Requisitos cobertos**: todos (entry point)
|
||||
|
||||
**O que**: Substituir o `<TelemetryPanel />` atual por `<ScenarioCreatorPage />`.
|
||||
|
||||
**Onde**:
|
||||
- `apps/web/src/App.tsx` (modificar)
|
||||
- `apps/web/src/main.tsx` — adicionar import de `leaflet/dist/leaflet.css`
|
||||
|
||||
**Depende de**: T09
|
||||
|
||||
**Done when**:
|
||||
- [ ] `App.tsx` renderiza apenas `<ScenarioCreatorPage />`
|
||||
- [ ] `main.tsx` importa `leaflet/dist/leaflet.css` antes de `App`
|
||||
- [ ] `tsc --noEmit` sem erros
|
||||
|
||||
**Gate**: `pnpm --filter @ymir/web build`
|
||||
|
||||
---
|
||||
|
||||
### T13 — Testes unitários das utilidades e stores
|
||||
|
||||
**Requisitos cobertos**: Regra de 80% de cobertura
|
||||
|
||||
**O que**: Testes unitários para todos os módulos lógicos não cobertos em T01–T11.
|
||||
|
||||
**Onde**:
|
||||
- `apps/web/src/lib/geo.test.ts`
|
||||
- `apps/web/src/features/scenario-creator/store.test.ts`
|
||||
- `apps/web/src/stores/simulationStore.test.ts`
|
||||
- `apps/web/src/features/scenario-creator/hooks/use-areas.test.ts`
|
||||
- `apps/web/src/features/scenario-creator/hooks/use-vessels.test.ts`
|
||||
|
||||
**Depende de**: T01, T02, T03, T10
|
||||
|
||||
**Done when**:
|
||||
- [ ] `pnpm --filter @ymir/web test` passa com cobertura ≥ 80% em todos os thresholds
|
||||
- [ ] Nenhum teste usa implementação interna — apenas API pública dos módulos
|
||||
|
||||
**Gate**: `pnpm --filter @ymir/web test` — exit 0
|
||||
|
||||
---
|
||||
|
||||
### T14 — Testes E2E com Playwright
|
||||
|
||||
**Requisitos cobertos**: SC-P1-01 a SC-P1-04 (happy paths)
|
||||
|
||||
**O que**: Spec E2E cobrindo o fluxo completo do instrutor.
|
||||
|
||||
**Onde**:
|
||||
- `apps/web/e2e/scenario-creator.spec.ts` (criar)
|
||||
|
||||
**Depende de**: T12 (app funcionando), API rodando com seed
|
||||
|
||||
**Pré-condição**: `pnpm --filter @ymir/api dev` rodando com `vessel1.json` e `area.json`.
|
||||
|
||||
**Cenários obrigatórios**:
|
||||
|
||||
1. **Seleção de área**: selecionar "baia_de_guanabara" → mapa visível → polígono presente
|
||||
2. **Adicionar vessel**: clicar "Adicionar vessel" → selecionar VLCC → marcador aparece no mapa
|
||||
3. **Iniciar simulação**: clicar "Iniciar" → status muda para "running"
|
||||
4. **Parar simulação**: clicar "Parar" → status muda para estado parado
|
||||
|
||||
**Done when**:
|
||||
- [ ] Todos os 4 cenários passam em Chromium headless
|
||||
- [ ] `pnpm --filter @ymir/web test:e2e` — exit 0
|
||||
|
||||
**Gate**: `pnpm --filter @ymir/web test:e2e`
|
||||
|
||||
---
|
||||
|
||||
## Ordem de execução recomendada
|
||||
|
||||
```
|
||||
[P] T01 + T02 # paralelos — sem dependência entre si
|
||||
↓
|
||||
[P] T03 + T10 # paralelos
|
||||
↓
|
||||
[P] T04 + T05 + T06 + T07 + T11 # paralelos após T01-T03-T10
|
||||
↓
|
||||
T08 # depende de T05, T06, T07
|
||||
↓
|
||||
T09 # depende de T04, T08
|
||||
↓
|
||||
T12 # depende de T09
|
||||
↓
|
||||
[P] T13 + T14 # paralelos — testes finais
|
||||
```
|
||||
|
||||
**Estimativa**: 14 tasks, ~8 sessões de implementação com subagentes paralelos.
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
| Task | Descrição | Status |
|
||||
|------|-----------|--------|
|
||||
| T01 | geo.ts | Pending |
|
||||
| T02 | useScenarioStore | Pending |
|
||||
| T03 | useAreas / useVessels | Pending |
|
||||
| T04 | AreaMapView + VesselMarker | Pending |
|
||||
| T05 | ScenarioForm | Pending |
|
||||
| T06 | VesselList | Pending |
|
||||
| T07 | SimulationControls | Pending |
|
||||
| T08 | Sidebar | Pending |
|
||||
| T09 | ScenarioCreatorPage | Pending |
|
||||
| T10 | extend simulationStore | Pending |
|
||||
| T11 | extend simulation.worker | Pending |
|
||||
| T12 | App.tsx swap | Pending |
|
||||
| T13 | Unit tests | Pending |
|
||||
| T14 | E2E Playwright | Pending |
|
||||
Generated
+900
File diff suppressed because it is too large.
Load diff
@@ -21,6 +21,11 @@
|
||||
"outputs": ["coverage/**"],
|
||||
"cache": true
|
||||
},
|
||||
"test:e2e": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["playwright-report/**", "test-results/**"],
|
||||
"cache": false
|
||||
},
|
||||
"lint": {
|
||||
"outputs": [],
|
||||
"cache": true
|
||||
|
||||
Reference in new issue
Block a user