import { act, renderHook } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import { idleSlot, isStaleResponse, selectedSlotCleared, selectedSlotRequested, slotFailed, slotRequested, slotSucceeded, useReadResource, } from './readResource' afterEach(() => { vi.useRealTimers() }) async function settle() { await act(async () => { await Promise.resolve() }) } function deferred() { let resolve!: (value: T) => void const promise = new Promise((resolvePromise) => { resolve = resolvePromise }) return { promise, resolve } } describe('slot phase machine', () => { const ready = { ...slotSucceeded(['a']), total: 1 } it('silent refresh over ready data keeps the snapshot visible', () => { expect(slotRequested(ready, true).phase).toBe('refreshing') expect(slotRequested(ready, true).value).toEqual(['a']) expect(slotRequested(idleSlot([]), true).phase).toBe('loading') }) it('failure retains a non-empty snapshot and surfaces errors otherwise', () => { expect(slotFailed(ready, '网络错误')).toEqual({ ...ready, phase: 'ready', error: null }) expect(slotFailed(idleSlot([]), '网络错误').phase).toBe('error') expect(slotFailed(idleSlot(null), '不存在').error).toBe('不存在') }) }) describe('selected slot race guard', () => { it('resets the value when the selection changes and drops stale responses', () => { const first = selectedSlotRequested(selectedSlotCleared(0), 'a', 1, false) const second = selectedSlotRequested(first, 'b', 2, false) expect(second.value).toBeNull() expect(second.selectedId).toBe('b') expect(isStaleResponse(second, 'a', 1)).toBe(true) expect(isStaleResponse(second, 'b', 2)).toBe(false) }) }) describe('useReadResource', () => { it('loads on mount, keeps the snapshot on silent failure, and polls while gated on', async () => { vi.useFakeTimers() const fetcher = vi.fn() .mockResolvedValueOnce({ ok: true }) .mockRejectedValueOnce(new Error('网络错误')) const { result } = renderHook(() => useReadResource(fetcher, { pollMs: 5000 })) await settle() expect(result.current.slot).toEqual({ phase: 'ready', value: { ok: true }, error: null }) await act(async () => { await vi.advanceTimersByTimeAsync(5000) }) await settle() expect(fetcher).toHaveBeenCalledTimes(2) expect(result.current.slot.phase).toBe('ready') expect(result.current.slot.value).toEqual({ ok: true }) }) it('stops polling when the gate flips false', async () => { vi.useFakeTimers() const fetcher = vi.fn().mockResolvedValue('x') const { rerender } = renderHook( ({ polling }) => useReadResource(fetcher, { pollMs: 5000, polling }), { initialProps: { polling: true } }, ) await settle() expect(fetcher).toHaveBeenCalledTimes(1) rerender({ polling: false }) await act(async () => { await vi.advanceTimersByTimeAsync(15000) }) expect(fetcher).toHaveBeenCalledTimes(1) }) it('drops a stale response after the key changes', async () => { const first = deferred() const second = deferred() const fetchers: Record Promise> = { a: () => first.promise, b: () => second.promise, } const { result, rerender } = renderHook( ({ key }) => useReadResource(() => fetchers[key](), { key }), { initialProps: { key: 'a' } }, ) rerender({ key: 'b' }) await act(async () => { second.resolve('B') }) expect(result.current.slot.value).toBe('B') await act(async () => { first.resolve('A') }) expect(result.current.slot.value).toBe('B') }) it('clears the slot when the key becomes null', async () => { const fetcher = vi.fn().mockResolvedValue('x') const { result, rerender } = renderHook( ({ key }) => useReadResource(fetcher, { key }), { initialProps: { key: 'a' as string | null } }, ) await settle() expect(result.current.slot.phase).toBe('ready') rerender({ key: null }) expect(result.current.slot).toEqual({ phase: 'idle', value: null, error: null }) }) })