feat(simulations): config par tâche + hook useTimer + 7 tests

This commit is contained in:
Hermann_Kitio 2026-04-21 00:16:38 +03:00
parent 869668a1ba
commit 24968f542d
3 changed files with 199 additions and 0 deletions

View file

@ -0,0 +1,100 @@
/**
* Tests du hook useTimer logique de décompte critique (auto-submit à l'expiration).
*/
import { act, renderHook } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useTimer } from '../useTimer'
describe('useTimer', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('initialise secondesRestantes à dureeMinutes × 60', () => {
const { result } = renderHook(() => useTimer(10, true))
expect(result.current.secondesRestantes).toBe(600)
expect(result.current.isExpired).toBe(false)
})
it('décrémente de 1 par seconde quand active=true', () => {
const { result } = renderHook(() => useTimer(1, true))
expect(result.current.secondesRestantes).toBe(60)
act(() => {
vi.advanceTimersByTime(1000)
})
expect(result.current.secondesRestantes).toBe(59)
act(() => {
vi.advanceTimersByTime(3000)
})
expect(result.current.secondesRestantes).toBe(56)
})
it('ne décrémente pas quand active=false', () => {
const { result } = renderHook(() => useTimer(1, false))
expect(result.current.secondesRestantes).toBe(60)
act(() => {
vi.advanceTimersByTime(5000)
})
expect(result.current.secondesRestantes).toBe(60)
expect(result.current.isExpired).toBe(false)
})
it('passe isExpired=true quand secondesRestantes atteint 0', () => {
const { result } = renderHook(() => useTimer(1 / 60, true)) // 1 seconde
expect(result.current.secondesRestantes).toBe(1)
act(() => {
vi.advanceTimersByTime(1000)
})
expect(result.current.secondesRestantes).toBe(0)
expect(result.current.isExpired).toBe(true)
})
it("n'évolue pas sous zéro", () => {
const { result } = renderHook(() => useTimer(1 / 60, true)) // 1 seconde
act(() => {
vi.advanceTimersByTime(5000)
})
expect(result.current.secondesRestantes).toBe(0)
expect(result.current.isExpired).toBe(true)
})
it('reset() restaure la valeur initiale', () => {
const { result } = renderHook(() => useTimer(1, true))
act(() => {
vi.advanceTimersByTime(10000)
})
expect(result.current.secondesRestantes).toBe(50)
act(() => {
result.current.reset()
})
expect(result.current.secondesRestantes).toBe(60)
expect(result.current.isExpired).toBe(false)
})
it('reprend le décompte quand active passe de false à true sans reset', () => {
const { result, rerender } = renderHook(({ active }) => useTimer(1, active), {
initialProps: { active: false },
})
act(() => {
vi.advanceTimersByTime(3000)
})
expect(result.current.secondesRestantes).toBe(60)
rerender({ active: true })
act(() => {
vi.advanceTimersByTime(2000)
})
expect(result.current.secondesRestantes).toBe(58)
})
})

View file

@ -0,0 +1,47 @@
/**
* Hook de décompte en secondes piloté par un flag `active`.
*
* Règle H : logique isolée du composant, testable unitairement.
*
* Comportement :
* - Initialise `secondesRestantes` à `dureeMinutes * 60`.
* - Décrémente de 1 chaque seconde tant que `active === true` et non expiré.
* - `isExpired` passe à true lorsque `secondesRestantes` atteint 0.
* - `reset()` restaure la valeur initiale et relance le décompte si `active`.
*/
import { useCallback, useEffect, useRef, useState } from 'react'
export interface TimerState {
secondesRestantes: number
isExpired: boolean
reset: () => void
}
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)
dureeRef.current = dureeSecondes
useEffect(() => {
if (!active) return
if (secondesRestantes <= 0) return
const id = setInterval(() => {
setSecondesRestantes((prev) => (prev <= 0 ? 0 : prev - 1))
}, 1000)
return () => clearInterval(id)
}, [active, secondesRestantes])
const reset = useCallback(() => {
setSecondesRestantes(dureeRef.current)
}, [])
return {
secondesRestantes,
isExpired: secondesRestantes <= 0,
reset,
}
}