import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { nextTick } from 'vue' import { useDataTableServerState } from '../useDataTableServerState' const mockApiGet = vi.hoisted(() => vi.fn()) vi.stubGlobal('useApi', () => ({ get: mockApiGet })) function ldResponse(member: T[], totalItems?: number): { member: T[], totalItems: number } { return { member, totalItems: totalItems ?? member.length } } describe('useDataTableServerState', () => { beforeEach(() => { mockApiGet.mockReset() vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('fetch initial au premier reload() avec page=1 et perPage par defaut', async () => { mockApiGet.mockResolvedValueOnce(ldResponse([{ id: 1 }, { id: 2 }], 42)) const { items, totalItems, reload } = useDataTableServerState('/sites', { name: '' }) reload() await vi.runAllTimersAsync() expect(mockApiGet).toHaveBeenCalledWith( '/sites', { page: 1, itemsPerPage: 10 }, { toast: false }, ) expect(items.value).toHaveLength(2) expect(totalItems.value).toBe(42) }) it('omet les filtres a valeur vide dans les query params', async () => { mockApiGet.mockResolvedValueOnce(ldResponse([])) const { reload } = useDataTableServerState('/users', { username: '', isAdmin: null, }) reload() await vi.runAllTimersAsync() expect(mockApiGet).toHaveBeenCalledWith( '/users', { page: 1, itemsPerPage: 10 }, { toast: false }, ) }) it('inclut les filtres renseignes dans les query params', async () => { // mockResolvedValue (sans Once) : chaque fetch retourne une // reponse valide, y compris ceux declenches par le debounce des // mutations de filters qui precedent reload(). mockApiGet.mockResolvedValue(ldResponse([])) const { filters, reload } = useDataTableServerState('/users', { username: '', isAdmin: null, }) filters.value.username = 'alice' filters.value.isAdmin = true reload() await vi.runAllTimersAsync() // Le reload() ecrase les scheduleReload en cours (clearTimeout), // donc on verifie juste que la derniere requete emise porte bien // les filtres + les parametres de pagination. expect(mockApiGet).toHaveBeenLastCalledWith( '/users', { page: 1, itemsPerPage: 10, username: 'alice', isAdmin: true }, { toast: false }, ) }) it('change page declenche un fetch immediat (pas de debounce)', async () => { mockApiGet.mockResolvedValue(ldResponse([])) const { page, reload } = useDataTableServerState('/sites', {}) reload() await vi.runAllTimersAsync() expect(mockApiGet).toHaveBeenCalledTimes(1) page.value = 3 await nextTick() await vi.runAllTimersAsync() expect(mockApiGet).toHaveBeenCalledTimes(2) expect(mockApiGet).toHaveBeenLastCalledWith( '/sites', { page: 3, itemsPerPage: 10 }, { toast: false }, ) }) it('change filter debounce 300ms avant fetch', async () => { mockApiGet.mockResolvedValue(ldResponse([])) const { filters, reload } = useDataTableServerState('/sites', { name: '' }) reload() await vi.runAllTimersAsync() mockApiGet.mockClear() filters.value.name = 'a' await nextTick() // Pas encore de requete : debounce en cours. expect(mockApiGet).not.toHaveBeenCalled() filters.value.name = 'al' await nextTick() filters.value.name = 'ali' await nextTick() // Avance le timer de 200ms : toujours pas fetch. vi.advanceTimersByTime(200) expect(mockApiGet).not.toHaveBeenCalled() // Avance encore 100ms : debounce expire, fetch lance. vi.advanceTimersByTime(100) await vi.runAllTimersAsync() expect(mockApiGet).toHaveBeenCalledTimes(1) expect(mockApiGet).toHaveBeenCalledWith( '/sites', { page: 1, itemsPerPage: 10, name: 'ali' }, { toast: false }, ) }) it('changer un filtre reset page a 1', async () => { mockApiGet.mockResolvedValue(ldResponse([])) const { page, filters, reload } = useDataTableServerState('/sites', { name: '' }) reload() await vi.runAllTimersAsync() page.value = 5 await vi.runAllTimersAsync() mockApiGet.mockClear() filters.value.name = 'x' await nextTick() await vi.runAllTimersAsync() // Page doit etre revenue a 1 avant le fetch. expect(page.value).toBe(1) expect(mockApiGet).toHaveBeenLastCalledWith( '/sites', expect.objectContaining({ page: 1, name: 'x' }), { toast: false }, ) }) it('change perPage declenche un fetch immediat', async () => { mockApiGet.mockResolvedValue(ldResponse([])) const { perPage, reload } = useDataTableServerState('/sites', {}) reload() await vi.runAllTimersAsync() mockApiGet.mockClear() perPage.value = 25 await nextTick() await vi.runAllTimersAsync() expect(mockApiGet).toHaveBeenLastCalledWith( '/sites', { page: 1, itemsPerPage: 25 }, { toast: false }, ) }) it('race condition : seule la derniere reponse gagne', async () => { // Scenario : user tape tres vite, 2 requetes partent, la premiere // (plus ancienne) arrive apres la seconde. Le composable doit // ignorer la premiere. let resolveFirst!: (value: unknown) => void let resolveSecond!: (value: unknown) => void mockApiGet .mockImplementationOnce(() => new Promise((r) => { resolveFirst = r })) .mockImplementationOnce(() => new Promise((r) => { resolveSecond = r })) const { items, reload } = useDataTableServerState<{ id: number }>('/sites', {}) reload() // requete #1 reload() // requete #2 (annule #1 du point de vue du token) // Resout la seconde d'abord avec id=2 resolveSecond(ldResponse([{ id: 2 }])) await vi.runAllTimersAsync() expect(items.value).toEqual([{ id: 2 }]) // Resout la premiere apres avec id=1 : DOIT etre ignore. resolveFirst(ldResponse([{ id: 1 }])) await vi.runAllTimersAsync() expect(items.value).toEqual([{ id: 2 }]) }) })