From 822b02a2d1fe32f3575a0b9297d4d54e7be4c680 Mon Sep 17 00:00:00 2001 From: Hermann_Kitio Date: Sat, 25 Apr 2026 21:10:39 +0300 Subject: [PATCH] =?UTF-8?q?fix(rapport,eo):=20conclusion=20ScoreHero=203?= =?UTF-8?q?=20=C3=A9tats=20+=20persistance=20simulation=5Fid=20pour=20resu?= =?UTF-8?q?me=20EO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/rapport/ScoreHero.tsx | 7 +++- .../rapport/__tests__/ScoreHero.test.tsx | 33 +++++++++++++++++++ .../state/SimulationFlowProvider.tsx | 9 +++++ .../state/__tests__/simulationFlowT1.test.tsx | 29 +++++++++++++++- 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 src/features/simulations/components/rapport/__tests__/ScoreHero.test.tsx diff --git a/src/features/simulations/components/rapport/ScoreHero.tsx b/src/features/simulations/components/rapport/ScoreHero.tsx index a27a69b..f214d2f 100644 --- a/src/features/simulations/components/rapport/ScoreHero.tsx +++ b/src/features/simulations/components/rapport/ScoreHero.tsx @@ -22,6 +22,7 @@ const NCLC_MIN_SCORE: Record = { 9: 14, 10: 16 } export function ScoreHero({ score, nclc, nclcCible }: Props) { const { points, atteint } = ecartVsCible(score, nclcCible) + const depasse = atteint && nclc > nclcCible const seuilCible = NCLC_MIN_SCORE[nclcCible] ?? 14 const percent = Math.max(0, Math.min(100, (score / 20) * 100)) const seuilPercent = (seuilCible / 20) * 100 @@ -88,7 +89,11 @@ export function ScoreHero({ score, nclc, nclcCible }: Props) { {/* Encart d'écart */} - {atteint ? ( + {depasse ? ( +

+ Objectif dépassé — continuez vers NCLC {nclc + 1}. +

+ ) : atteint ? (

Objectif NCLC {nclcCible} atteint.

diff --git a/src/features/simulations/components/rapport/__tests__/ScoreHero.test.tsx b/src/features/simulations/components/rapport/__tests__/ScoreHero.test.tsx new file mode 100644 index 0000000..9e98bb4 --- /dev/null +++ b/src/features/simulations/components/rapport/__tests__/ScoreHero.test.tsx @@ -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() + expect(screen.getByText(/2 points avant NCLC/i)).toBeInTheDocument() + expect(screen.getByText(/9\+/)).toBeInTheDocument() + }) + + it('NCLC atteint = cible : affiche « Objectif NCLC {cible} atteint. »', () => { + render() + expect(screen.getByText('Objectif NCLC 9 atteint.')).toBeInTheDocument() + }) + + it('NCLC atteint > cible : affiche « Objectif dépassé — continuez vers NCLC {nclc+1}. »', () => { + render() + expect(screen.getByText('Objectif dépassé — continuez vers NCLC 11.')).toBeInTheDocument() + }) +}) diff --git a/src/features/simulations/state/SimulationFlowProvider.tsx b/src/features/simulations/state/SimulationFlowProvider.tsx index 803dd44..3b4bbc1 100644 --- a/src/features/simulations/state/SimulationFlowProvider.tsx +++ b/src/features/simulations/state/SimulationFlowProvider.tsx @@ -105,6 +105,15 @@ export function SimulationFlowProvider({ children }: { children: ReactNode }) { // 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({ mutationFn: createSimulation, onSuccess: (data) => { diff --git a/src/features/simulations/state/__tests__/simulationFlowT1.test.tsx b/src/features/simulations/state/__tests__/simulationFlowT1.test.tsx index 3e45e01..b090027 100644 --- a/src/features/simulations/state/__tests__/simulationFlowT1.test.tsx +++ b/src/features/simulations/state/__tests__/simulationFlowT1.test.tsx @@ -8,7 +8,7 @@ */ 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 { MemoryRouter } from 'react-router-dom' 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') }) + 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', () => { const { result } = renderHook(() => useSimulationFlow(), { wrapper: createWrapper() })