feat(simulations): choix du sujet — dropdown intégré + bouton aléatoire
This commit is contained in:
parent
477477b6a6
commit
7902eec042
5 changed files with 193 additions and 29 deletions
|
|
@ -2,7 +2,8 @@
|
|||
* Tests de la state machine useSimulation.
|
||||
*
|
||||
* Transitions couvertes :
|
||||
* idle → task-selected (selectTask success)
|
||||
* idle → choosing-subject (selectTask success, tâche avec catalogue)
|
||||
* choosing-subject → task-selected (selectRandom / selectSujet)
|
||||
* task-selected → correcting (submitText déclenché)
|
||||
* correcting → done (correctEe success)
|
||||
* correcting → task-selected (correctEe error)
|
||||
|
|
@ -69,7 +70,7 @@ describe('useSimulation — état initial', () => {
|
|||
})
|
||||
|
||||
describe('useSimulation — selectTask', () => {
|
||||
it('step passe à task-selected et production est hydratée après succès', async () => {
|
||||
it('step passe à choosing-subject et production est hydratée pour une tâche avec catalogue', async () => {
|
||||
mockCreateSimulation.mockResolvedValue(mockProduction)
|
||||
|
||||
const { result } = renderHook(() => useSimulation(), { wrapper: createWrapper() })
|
||||
|
|
@ -78,10 +79,24 @@ describe('useSimulation — selectTask', () => {
|
|||
result.current.selectTask({ tache: 'EE_T1', mode: 'entrainement' })
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.step).toBe('task-selected'))
|
||||
await waitFor(() => expect(result.current.step).toBe('choosing-subject'))
|
||||
expect(result.current.production).toEqual(mockProduction)
|
||||
})
|
||||
|
||||
it('step passe directement à task-selected pour EO_T1 (sans catalogue)', async () => {
|
||||
const eoProduction: Production = { ...mockProduction, tache: 'EO_T1' }
|
||||
mockCreateSimulation.mockResolvedValue(eoProduction)
|
||||
|
||||
const { result } = renderHook(() => useSimulation(), { wrapper: createWrapper() })
|
||||
|
||||
act(() => {
|
||||
result.current.selectTask({ tache: 'EO_T1', mode: 'entrainement' })
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.step).toBe('task-selected'))
|
||||
expect(result.current.production).toEqual(eoProduction)
|
||||
})
|
||||
|
||||
it('isCreating = true pendant la mutation createSimulation', async () => {
|
||||
let resolveCreate!: (p: Production) => void
|
||||
mockCreateSimulation.mockImplementation(() => new Promise(r => { resolveCreate = r }))
|
||||
|
|
@ -109,6 +124,8 @@ describe('useSimulation — submitText', () => {
|
|||
const { result } = renderHook(() => useSimulation(), { wrapper: createWrapper() })
|
||||
|
||||
act(() => result.current.selectTask({ tache: 'EE_T1', mode: 'entrainement' }))
|
||||
await waitFor(() => expect(result.current.step).toBe('choosing-subject'))
|
||||
act(() => result.current.selectRandom([]))
|
||||
await waitFor(() => expect(result.current.step).toBe('task-selected'))
|
||||
|
||||
act(() => result.current.submitText('Mon texte de production.'))
|
||||
|
|
@ -126,6 +143,8 @@ describe('useSimulation — submitText', () => {
|
|||
const { result } = renderHook(() => useSimulation(), { wrapper: createWrapper() })
|
||||
|
||||
act(() => result.current.selectTask({ tache: 'EE_T1', mode: 'entrainement' }))
|
||||
await waitFor(() => expect(result.current.step).toBe('choosing-subject'))
|
||||
act(() => result.current.selectRandom([]))
|
||||
await waitFor(() => expect(result.current.step).toBe('task-selected'))
|
||||
|
||||
act(() => result.current.submitText('Mon texte.')
|
||||
|
|
@ -151,6 +170,8 @@ describe('useSimulation — reset', () => {
|
|||
const { result } = renderHook(() => useSimulation(), { wrapper: createWrapper() })
|
||||
|
||||
act(() => result.current.selectTask({ tache: 'EE_T1', mode: 'entrainement' }))
|
||||
await waitFor(() => expect(result.current.step).toBe('choosing-subject'))
|
||||
act(() => result.current.selectRandom([]))
|
||||
await waitFor(() => expect(result.current.step).toBe('task-selected'))
|
||||
|
||||
act(() => result.current.reset())
|
||||
|
|
|
|||
|
|
@ -2,13 +2,15 @@
|
|||
* Hook d'orchestration du flux simulation EE.
|
||||
*
|
||||
* Séquence : createSimulation (POST /simulations)
|
||||
* → [choosing-subject] (sauf EO_T1 — sujet fixe)
|
||||
* → correctEe (POST /corrections/ee, timeout 30 s)
|
||||
*
|
||||
* State machine :
|
||||
* 'idle' → sélection de tâche disponible
|
||||
* 'task-selected' → formulaire de saisie visible
|
||||
* 'correcting' → correction en cours (30 s max)
|
||||
* 'done' → rapport disponible dans `report`
|
||||
* 'idle' → sélection de tâche disponible
|
||||
* 'choosing-subject' → écran SujetSelector (hors EO_T1)
|
||||
* 'task-selected' → formulaire de saisie visible
|
||||
* 'correcting' → correction en cours (30 s max)
|
||||
* 'done' → rapport disponible dans `report`
|
||||
*
|
||||
* Règle H : aucune logique métier ici — les gardes de quota et de plan
|
||||
* sont dans TaskSelector (UX) et dans le backend (autorité).
|
||||
|
|
@ -18,11 +20,24 @@ import { useState } from 'react'
|
|||
import { useMutation } from '@tanstack/react-query'
|
||||
import { createSimulation } from '@/entities/production/api'
|
||||
import { correctEe } from '@/entities/report/api'
|
||||
import type { CreateSimulationPayload, Production } from '@/entities/production/types'
|
||||
import type {
|
||||
CreateSimulationPayload,
|
||||
Production,
|
||||
SujetData,
|
||||
Tache,
|
||||
} from '@/entities/production/types'
|
||||
import type { Report } from '@/entities/report/types'
|
||||
import type { ApiError } from '@/shared/types/api'
|
||||
|
||||
export type SimulationStep = 'idle' | 'task-selected' | 'correcting' | 'done'
|
||||
export type SimulationStep =
|
||||
| 'idle'
|
||||
| 'choosing-subject'
|
||||
| 'task-selected'
|
||||
| 'correcting'
|
||||
| 'done'
|
||||
|
||||
/** Tâches qui ne passent pas par l'écran de choix de sujet. */
|
||||
const TACHES_SANS_CATALOGUE: Tache[] = ['EO_T1']
|
||||
|
||||
export function useSimulation() {
|
||||
const [step, setStep] = useState<SimulationStep>('idle')
|
||||
|
|
@ -32,7 +47,7 @@ export function useSimulation() {
|
|||
mutationFn: createSimulation,
|
||||
onSuccess: (data) => {
|
||||
setProduction(data)
|
||||
setStep('task-selected')
|
||||
setStep(TACHES_SANS_CATALOGUE.includes(data.tache) ? 'task-selected' : 'choosing-subject')
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -52,6 +67,31 @@ export function useSimulation() {
|
|||
correctMutation.mutate({ simulationId: production.id, contenu: texte, tache: production.tache })
|
||||
}
|
||||
|
||||
/** Remplace le sujet courant sans toucher à l'étape. */
|
||||
function changeSubject(sujet: SujetData): void {
|
||||
setProduction((p) => (p ? { ...p, sujet } : p))
|
||||
}
|
||||
|
||||
/** Choix manuel : remplace le sujet et passe à la saisie. */
|
||||
function selectSujet(sujet: SujetData): void {
|
||||
changeSubject(sujet)
|
||||
setStep('task-selected')
|
||||
}
|
||||
|
||||
/** Choix aléatoire côté client à partir d'une liste pré-chargée. */
|
||||
function selectRandom(sujets: SujetData[]): void {
|
||||
if (sujets.length > 0) {
|
||||
const pick = sujets[Math.floor(Math.random() * sujets.length)]
|
||||
changeSubject(pick)
|
||||
}
|
||||
setStep('task-selected')
|
||||
}
|
||||
|
||||
/** Retour à l'écran SujetSelector depuis SimulationForm. */
|
||||
function backToSubject(): void {
|
||||
setStep('choosing-subject')
|
||||
}
|
||||
|
||||
function reset(): void {
|
||||
setStep('idle')
|
||||
setProduction(null)
|
||||
|
|
@ -70,6 +110,10 @@ export function useSimulation() {
|
|||
correctError: correctMutation.error as ApiError | null,
|
||||
selectTask,
|
||||
submitText,
|
||||
changeSubject,
|
||||
selectSujet,
|
||||
selectRandom,
|
||||
backToSubject,
|
||||
reset,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue