/** * Tests du hook useAutosave — FTD-21. * * Valide : * - debounce 30 s (pas de save avant, save après) * - flush immédiat sur `beforeunload` * - cleanup du listener au unmount * - enabled=false → aucun appel */ import { act, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@/entities/production/api', () => ({ autosaveContenu: vi.fn().mockResolvedValue(undefined), })) import { autosaveContenu } from '@/entities/production/api' import { useAutosave } from '../useAutosave' const mocked = vi.mocked(autosaveContenu) describe('useAutosave', () => { beforeEach(() => { vi.useFakeTimers() mocked.mockClear() mocked.mockResolvedValue(undefined) }) afterEach(() => { vi.useRealTimers() }) it("debounce 30 s : pas d'appel avant, appel après", async () => { const { rerender } = renderHook( ({ contenu }: { contenu: string }) => useAutosave('sim-1', contenu, true), { initialProps: { contenu: '' } }, ) rerender({ contenu: 'hello world' }) // Avant 30 s : aucun appel await act(async () => { await vi.advanceTimersByTimeAsync(29_000) }) expect(mocked).not.toHaveBeenCalled() // Après 30 s : save await act(async () => { await vi.advanceTimersByTimeAsync(2_000) }) expect(mocked).toHaveBeenCalledTimes(1) expect(mocked).toHaveBeenCalledWith('sim-1', 'hello world') }) it('flush immédiat sur beforeunload', async () => { const { rerender } = renderHook( ({ contenu }: { contenu: string }) => useAutosave('sim-1', contenu, true), { initialProps: { contenu: '' } }, ) rerender({ contenu: 'texte à sauvegarder' }) await act(async () => { window.dispatchEvent(new Event('beforeunload')) await Promise.resolve() }) expect(mocked).toHaveBeenCalledTimes(1) expect(mocked).toHaveBeenCalledWith('sim-1', 'texte à sauvegarder') }) it('cleanup : unmount retire le listener beforeunload', async () => { const { unmount, rerender } = renderHook( ({ contenu }: { contenu: string }) => useAutosave('sim-1', contenu, true), { initialProps: { contenu: '' } }, ) rerender({ contenu: 'texte' }) unmount() await act(async () => { window.dispatchEvent(new Event('beforeunload')) await Promise.resolve() }) expect(mocked).not.toHaveBeenCalled() }) it('enabled=false : aucun appel, même après 30 s', async () => { const { rerender } = renderHook( ({ contenu, enabled }: { contenu: string; enabled: boolean }) => useAutosave('sim-1', contenu, enabled), { initialProps: { contenu: 'texte', enabled: false } }, ) rerender({ contenu: 'texte modifié', enabled: false }) await act(async () => { await vi.advanceTimersByTimeAsync(31_000) }) expect(mocked).not.toHaveBeenCalled() }) it('simulationId null : aucun appel', async () => { renderHook(() => useAutosave(null, 'texte', true)) await act(async () => { await vi.advanceTimersByTimeAsync(31_000) }) expect(mocked).not.toHaveBeenCalled() }) it("dédoublonnage : pas de second appel si le contenu n'a pas changé", async () => { const { rerender } = renderHook( ({ contenu }: { contenu: string }) => useAutosave('sim-1', contenu, true), { initialProps: { contenu: '' } }, ) rerender({ contenu: 'identique' }) await act(async () => { await vi.advanceTimersByTimeAsync(31_000) }) expect(mocked).toHaveBeenCalledTimes(1) // Rerender avec même contenu → debounce ne repart pas (le contenu ref ne change pas côté React) // Simule une édition puis retour au texte initial (déjà sauvegardé) rerender({ contenu: 'identique' }) await act(async () => { await vi.advanceTimersByTimeAsync(31_000) }) expect(mocked).toHaveBeenCalledTimes(1) }) })