fix(lint): 4 erreurs ESLint corrigées — split SimulationFlowProvider, hook conditionnel, ref render, setState effect

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-23 03:05:14 +03:00
parent de69b3ff16
commit 79bbbdc4e8
10 changed files with 82 additions and 50 deletions

View file

@ -30,8 +30,11 @@ export function AppLayout({ children }: AppLayoutProps) {
const { data } = usePlan()
const plan: Plan = data?.plan ?? 'free'
// Ferme le drawer à chaque changement de route
// Ferme le drawer à chaque changement de route.
// Synchronisation UI → router state : pattern légitime (source externe = React
// Router). Bail-out React si déjà fermé = zéro cascading render en pratique.
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setIsMobileMenuOpen(false)
}, [location.pathname])

View file

@ -33,11 +33,14 @@ function gaugeColor(score: number): string {
}
export function MonProfilPreparation({ plan }: Props) {
// Garde explicite (cohérent avec la logique du hook qui a déjà `enabled`).
if (!hasAccess(plan, 'pattern_analysis')) return null
// Hook appelé inconditionnellement (règle React). Il court-circuite la
// requête backend via `enabled: hasAccess(plan, 'pattern_analysis')`,
// donc aucun appel parasite pour Free/Standard.
const { data, isLoading, isError } = usePatterns(plan)
// Garde explicite après le hook pour éviter un flash de contenu.
if (!hasAccess(plan, 'pattern_analysis')) return null
if (isLoading || isError || !data) {
return (
<Card variant="default" className="p-4">

View file

@ -5,7 +5,7 @@
* Le hook `usePatterns` est mocké pour isoler la présentation.
*/
import { describe, it, expect, vi, afterEach } from 'vitest'
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
import { render, screen, cleanup } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
@ -16,6 +16,16 @@ vi.mock('@/features/progression/hooks/usePatterns', () => ({
import { usePatterns } from '@/features/progression/hooks/usePatterns'
import { MonProfilPreparation } from '../MonProfilPreparation'
beforeEach(() => {
// Mock par défaut — usePatterns est appelé inconditionnellement depuis le
// composant (Règle des hooks). Les tests Premium surchargent ce mock.
vi.mocked(usePatterns).mockReturnValue({
data: undefined,
isLoading: false,
isError: false,
} as unknown as ReturnType<typeof usePatterns>)
})
afterEach(cleanup)
function renderWithRouter(ui: React.ReactNode) {

View file

@ -23,7 +23,7 @@ import { countWords, getSimulationConfig } from '../lib/simulationConfig'
import { useTimer } from '../hooks/useTimer'
import { useIdees } from '../hooks/useIdees'
import { useAutosave } from '../hooks/useAutosave'
import type { SimulationStep } from '../state/SimulationFlowProvider'
import type { SimulationStep } from '../state/simulationFlow'
import { SujetDisplay } from './SujetDisplay'
import { SpecialCharsKeyboard } from './SpecialCharsKeyboard'
import { TimerDisplay } from './TimerDisplay'

View file

@ -17,7 +17,8 @@ import { MemoryRouter } from 'react-router-dom'
import React from 'react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useSimulation } from '../useSimulation'
import { SimulationFlowProvider, useSimulationFlow } from '../../state/SimulationFlowProvider'
import { SimulationFlowProvider } from '../../state/SimulationFlowProvider'
import { useSimulationFlow } from '../../state/simulationFlow'
import {
createSimulation,
getSimulationState,

View file

@ -10,7 +10,7 @@
*/
import { useNavigate } from 'react-router-dom'
import { useSimulationFlow } from '../state/SimulationFlowProvider'
import { useSimulationFlow } from '../state/simulationFlow'
export function useSimulation() {
const navigate = useNavigate()

View file

@ -22,7 +22,9 @@ export function useTimer(dureeMinutes: number, active: boolean): TimerState {
const dureeSecondes = Math.max(0, Math.floor(dureeMinutes * 60))
const [secondesRestantes, setSecondesRestantes] = useState(dureeSecondes)
const dureeRef = useRef(dureeSecondes)
useEffect(() => {
dureeRef.current = dureeSecondes
}, [dureeSecondes])
useEffect(() => {
if (!active) return

View file

@ -18,7 +18,7 @@ import { Shuffle } from 'lucide-react'
import { Button } from '@/shared/ui/Button'
import { formatTache } from '@/entities/production/lib'
import type { SujetData } from '@/entities/production/types'
import { useSimulationFlow } from '../state/SimulationFlowProvider'
import { useSimulationFlow } from '../state/simulationFlow'
import { useSujets } from '../hooks/useSujets'
import { SujetCard } from '../components/SujetCard'

View file

@ -8,7 +8,7 @@
* Règle H : aucune logique métier les mutations s'appuient sur entities/.
*/
import { createContext, useContext, useEffect, useRef, useState, type ReactNode } from 'react'
import { useEffect, useRef, useState, type ReactNode } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useMutation } from '@tanstack/react-query'
import {
@ -17,43 +17,15 @@ import {
updateSujet as updateSujetApi,
} from '@/entities/production/api'
import { correctEe } from '@/entities/report/api'
import type {
CreateSimulationPayload,
Production,
SujetData,
Tache,
} from '@/entities/production/types'
import type { CreateSimulationPayload, Production, Tache } from '@/entities/production/types'
import type { Report } from '@/entities/report/types'
import type { ApiError } from '@/shared/types/api'
export type SimulationStep =
| 'idle'
| 'choosing-subject'
| 'task-selected'
| 'correcting'
| 'done'
import type { SujetData } from '@/entities/production/types'
import { SimulationFlowContext, type FlowValue, type SimulationStep } from './simulationFlow'
const TACHES_SANS_CATALOGUE: Tache[] = ['EO_T1']
const LS_SIMULATION_ID_KEY = 'expria_simulation_id'
interface FlowValue {
step: SimulationStep
production: Production | null
sujet: SujetData | null
report: Report | null
isCreating: boolean
isCorrecting: boolean
createError: ApiError | null
correctError: ApiError | null
selectTask: (payload: CreateSimulationPayload) => void
submitText: (texte: string, nclcCible?: 9 | 10) => void
changeSubject: (sujet: SujetData) => void
setStep: (step: SimulationStep) => void
reset: () => void
}
const SimulationFlowContext = createContext<FlowValue | null>(null)
export function SimulationFlowProvider({ children }: { children: ReactNode }) {
const [step, setStep] = useState<SimulationStep>('idle')
const [production, setProduction] = useState<Production | null>(null)
@ -178,11 +150,3 @@ export function SimulationFlowProvider({ children }: { children: ReactNode }) {
<SimulationFlowContext.Provider value={value}>{children}</SimulationFlowContext.Provider>
)
}
export function useSimulationFlow(): FlowValue {
const ctx = useContext(SimulationFlowContext)
if (!ctx) {
throw new Error('useSimulationFlow doit être utilisé dans un <SimulationFlowProvider>.')
}
return ctx
}

View file

@ -0,0 +1,49 @@
/**
* Types, contexte et hook du flux simulation.
*
* Extrait de SimulationFlowProvider.tsx pour respecter la règle
* `react-refresh/only-export-components` : un fichier de composant ne peut
* pas -exporter types / hooks / contextes.
*/
import { createContext, useContext } from 'react'
import type {
CreateSimulationPayload,
Production,
SujetData,
} from '@/entities/production/types'
import type { Report } from '@/entities/report/types'
import type { ApiError } from '@/shared/types/api'
export type SimulationStep =
| 'idle'
| 'choosing-subject'
| 'task-selected'
| 'correcting'
| 'done'
export interface FlowValue {
step: SimulationStep
production: Production | null
sujet: SujetData | null
report: Report | null
isCreating: boolean
isCorrecting: boolean
createError: ApiError | null
correctError: ApiError | null
selectTask: (payload: CreateSimulationPayload) => void
submitText: (texte: string, nclcCible?: 9 | 10) => void
changeSubject: (sujet: SujetData) => void
setStep: (step: SimulationStep) => void
reset: () => void
}
export const SimulationFlowContext = createContext<FlowValue | null>(null)
export function useSimulationFlow(): FlowValue {
const ctx = useContext(SimulationFlowContext)
if (!ctx) {
throw new Error('useSimulationFlow doit être utilisé dans un <SimulationFlowProvider>.')
}
return ctx
}