feat(simulations): useSimulation hook + TaskSelector + SimulationForm + SimulationPage + route (Sprint 3 étape 14)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-20 00:08:34 +03:00
parent b31e8666a5
commit 997f39bd33
7 changed files with 621 additions and 0 deletions

View file

@ -0,0 +1,160 @@
/**
* Tests de la state machine useSimulation.
*
* Transitions couvertes :
* idle task-selected (selectTask success)
* task-selected correcting (submitText déclenché)
* correcting done (correctEe success)
* correcting task-selected (correctEe error)
* * idle (reset)
* guard submitText sans production (aucune mutation)
*/
import { renderHook, act, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useSimulation } from '../useSimulation'
import { createSimulation } from '@/entities/production/api'
import { correctEe } from '@/entities/report/api'
import type { Production } from '@/entities/production/types'
import type { Report } from '@/entities/report/types'
vi.mock('@/entities/production/api')
vi.mock('@/entities/report/api')
const mockCreateSimulation = vi.mocked(createSimulation)
const mockCorrectEe = vi.mocked(correctEe)
const mockProduction: Production = {
id: 'sim-1',
tache: 'EE_T1',
mode: 'entrainement',
created_at: '2026-04-19T00:00:00Z',
}
const mockReport: Report = {
simulation_id: 'sim-1',
score: 80,
nclc: 9,
feedback_court: 'Bon travail.',
criteres: [],
erreurs: [],
modele: '',
idees: [],
exercices: [],
}
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: { mutations: { retry: false } },
})
return function Wrapper({ children }: { children: React.ReactNode }) {
return React.createElement(QueryClientProvider, { client: queryClient }, children)
}
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('useSimulation — état initial', () => {
it('step = idle, production null, report null', () => {
const { result } = renderHook(() => useSimulation(), { wrapper: createWrapper() })
expect(result.current.step).toBe('idle')
expect(result.current.production).toBeNull()
expect(result.current.report).toBeNull()
})
})
describe('useSimulation — selectTask', () => {
it('step passe à task-selected et production est hydratée après succès', async () => {
mockCreateSimulation.mockResolvedValue(mockProduction)
const { result } = renderHook(() => useSimulation(), { wrapper: createWrapper() })
act(() => {
result.current.selectTask({ tache: 'EE_T1', mode: 'entrainement' })
})
await waitFor(() => expect(result.current.step).toBe('task-selected'))
expect(result.current.production).toEqual(mockProduction)
})
it('isCreating = true pendant la mutation createSimulation', async () => {
let resolveCreate!: (p: Production) => void
mockCreateSimulation.mockImplementation(() => new Promise(r => { resolveCreate = r }))
const { result } = renderHook(() => useSimulation(), { wrapper: createWrapper() })
act(() => {
result.current.selectTask({ tache: 'EE_T2', mode: 'entrainement' })
})
await waitFor(() => expect(result.current.isCreating).toBe(true))
act(() => resolveCreate(mockProduction))
await waitFor(() => expect(result.current.isCreating).toBe(false))
})
})
describe('useSimulation — submitText', () => {
it('step correcting pendant la correction, puis done après succès', async () => {
mockCreateSimulation.mockResolvedValue(mockProduction)
let resolveCorrect!: (r: Report) => void
mockCorrectEe.mockImplementation(() => new Promise(r => { resolveCorrect = r }))
const { result } = renderHook(() => useSimulation(), { wrapper: createWrapper() })
act(() => result.current.selectTask({ tache: 'EE_T1', mode: 'entrainement' }))
await waitFor(() => expect(result.current.step).toBe('task-selected'))
act(() => result.current.submitText('Mon texte de production.'))
await waitFor(() => expect(result.current.step).toBe('correcting'))
act(() => resolveCorrect(mockReport))
await waitFor(() => expect(result.current.step).toBe('done'))
expect(result.current.report).toEqual(mockReport)
})
it('step revient à task-selected si correctEe échoue', async () => {
mockCreateSimulation.mockResolvedValue(mockProduction)
mockCorrectEe.mockRejectedValue({ code: 'SIMULATION_NOT_FOUND', message: 'Not found' })
const { result } = renderHook(() => useSimulation(), { wrapper: createWrapper() })
act(() => result.current.selectTask({ tache: 'EE_T1', mode: 'entrainement' }))
await waitFor(() => expect(result.current.step).toBe('task-selected'))
act(() => result.current.submitText('Mon texte.')
)
await waitFor(() => expect(result.current.step).toBe('task-selected'))
expect(result.current.report).toBeNull()
})
it('submitText sans production ne déclenche aucune mutation', async () => {
const { result } = renderHook(() => useSimulation(), { wrapper: createWrapper() })
act(() => result.current.submitText('texte quelconque'))
expect(mockCorrectEe).not.toHaveBeenCalled()
expect(result.current.step).toBe('idle')
})
})
describe('useSimulation — reset', () => {
it('reset depuis task-selected remet step à idle et production à null', async () => {
mockCreateSimulation.mockResolvedValue(mockProduction)
const { result } = renderHook(() => useSimulation(), { wrapper: createWrapper() })
act(() => result.current.selectTask({ tache: 'EE_T1', mode: 'entrainement' }))
await waitFor(() => expect(result.current.step).toBe('task-selected'))
act(() => result.current.reset())
expect(result.current.step).toBe('idle')
expect(result.current.production).toBeNull()
})
})