fix(rapport,eo): conclusion ScoreHero 3 états + persistance simulation_id pour resume EO
This commit is contained in:
parent
5188714235
commit
822b02a2d1
4 changed files with 76 additions and 2 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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() })
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue