fix(rapport,eo): conclusion ScoreHero 3 états + persistance simulation_id pour resume EO

This commit is contained in:
Hermann_Kitio 2026-04-25 21:10:39 +03:00
parent 5188714235
commit 822b02a2d1
4 changed files with 76 additions and 2 deletions

View file

@ -22,6 +22,7 @@ const NCLC_MIN_SCORE: Record<number, number> = { 9: 14, 10: 16 }
export function ScoreHero({ score, nclc, nclcCible }: Props) { export function ScoreHero({ score, nclc, nclcCible }: Props) {
const { points, atteint } = ecartVsCible(score, nclcCible) const { points, atteint } = ecartVsCible(score, nclcCible)
const depasse = atteint && nclc > nclcCible
const seuilCible = NCLC_MIN_SCORE[nclcCible] ?? 14 const seuilCible = NCLC_MIN_SCORE[nclcCible] ?? 14
const percent = Math.max(0, Math.min(100, (score / 20) * 100)) const percent = Math.max(0, Math.min(100, (score / 20) * 100))
const seuilPercent = (seuilCible / 20) * 100 const seuilPercent = (seuilCible / 20) * 100
@ -88,7 +89,11 @@ export function ScoreHero({ score, nclc, nclcCible }: Props) {
</div> </div>
{/* Encart d'écart */} {/* Encart d'écart */}
{atteint ? ( {depasse ? (
<p className="rounded-md border border-success/30 bg-success-soft px-3 py-2 text-sm text-success">
Objectif dépassé continuez vers NCLC {nclc + 1}.
</p>
) : atteint ? (
<p className="rounded-md border border-success/30 bg-success-soft px-3 py-2 text-sm text-success"> <p className="rounded-md border border-success/30 bg-success-soft px-3 py-2 text-sm text-success">
Objectif NCLC {nclcCible} atteint. Objectif NCLC {nclcCible} atteint.
</p> </p>

View file

@ -0,0 +1,33 @@
/**
* Tests ScoreHero (Sprint 4.5).
*
* Couvre les 3 états de l'encart de conclusion :
* - NCLC atteint < NCLC cible : message « X points avant NCLC »
* - NCLC atteint = NCLC cible : message « Objectif NCLC atteint. »
* - NCLC atteint > NCLC cible : message « Objectif dépassé continuez vers NCLC {nclc+1}. »
*/
import { describe, it, expect } from 'vitest'
import { render, screen, cleanup } from '@testing-library/react'
import { afterEach } from 'vitest'
import { ScoreHero } from '../ScoreHero'
afterEach(cleanup)
describe('ScoreHero — encart de conclusion', () => {
it('NCLC atteint < cible : affiche « X points avant NCLC {cible}+ »', () => {
render(<ScoreHero score={12} nclc={8} nclcCible={9} />)
expect(screen.getByText(/2 points avant NCLC/i)).toBeInTheDocument()
expect(screen.getByText(/9\+/)).toBeInTheDocument()
})
it('NCLC atteint = cible : affiche « Objectif NCLC {cible} atteint. »', () => {
render(<ScoreHero score={14} nclc={9} nclcCible={9} />)
expect(screen.getByText('Objectif NCLC 9 atteint.')).toBeInTheDocument()
})
it('NCLC atteint > cible : affiche « Objectif dépassé — continuez vers NCLC {nclc+1}. »', () => {
render(<ScoreHero score={17} nclc={10} nclcCible={9} />)
expect(screen.getByText('Objectif dépassé — continuez vers NCLC 11.')).toBeInTheDocument()
})
})

View file

@ -105,6 +105,15 @@ export function SimulationFlowProvider({ children }: { children: ReactNode }) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
// Persiste l'ID de simulation dès qu'une production est active.
// Permet le resume au refresh pour TOUS les flows (EE + EO_T1 + EO_T3),
// indépendamment du composant qui rend le formulaire.
useEffect(() => {
if (production?.id) {
localStorage.setItem(LS_SIMULATION_ID_KEY, production.id)
}
}, [production?.id])
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: createSimulation, mutationFn: createSimulation,
onSuccess: (data) => { onSuccess: (data) => {

View file

@ -8,7 +8,7 @@
*/ */
import React from 'react' import React from 'react'
import { renderHook, act } from '@testing-library/react' import { renderHook, act, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { MemoryRouter } from 'react-router-dom' import { MemoryRouter } from 'react-router-dom'
import { describe, it, expect, vi, beforeEach } from 'vitest' import { describe, it, expect, vi, beforeEach } from 'vitest'
@ -78,6 +78,33 @@ describe('SimulationFlowProvider T1 — Sprint 4c-2', () => {
expect(result.current.presentationT1).toBe('présentation persistée') expect(result.current.presentationT1).toBe('présentation persistée')
}) })
it('hydrate la simulation EO_T1 + présentation depuis localStorage au refresh', async () => {
localStorage.setItem('expria_simulation_id', 'sim-eo-t1-42')
localStorage.setItem(LS_KEY, 'présentation persistée')
mockGetState.mockResolvedValueOnce({
simulation_id: 'sim-eo-t1-42',
tache: 'EO_T1',
mode: 'entrainement',
created_at: '2026-04-25T10:00:00.000Z',
sujet: null,
contenu: null,
rapport: null,
nclc_cible: 9,
exercices: null,
exercices_status: 'pending',
modele: null,
modele_status: 'pending',
})
const { result } = renderHook(() => useSimulationFlow(), { wrapper: createWrapper() })
await waitFor(() => {
expect(result.current.production?.id).toBe('sim-eo-t1-42')
})
expect(result.current.presentationT1).toBe('présentation persistée')
expect(result.current.step).toBe('task-selected')
})
it('reset() remet presentationT1 à null et nettoie localStorage', () => { it('reset() remet presentationT1 à null et nettoie localStorage', () => {
const { result } = renderHook(() => useSimulationFlow(), { wrapper: createWrapper() }) const { result } = renderHook(() => useSimulationFlow(), { wrapper: createWrapper() })