import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest' import { useKeyedDebouncedSearch } from '@/composables/useKeyedDebouncedSearch' const flushPromises = () => Promise.resolve() describe('useKeyedDebouncedSearch', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('为不同 key 独立防抖触发搜索', async () => { const search = vi.fn().mockResolvedValue([]) const onSuccess = vi.fn() const searcher = useKeyedDebouncedSearch({ delay: 100, search, onSuccess }) searcher.trigger('a', 'foo') searcher.trigger('b', 'bar') expect(search).not.toHaveBeenCalled() vi.advanceTimersByTime(100) await flushPromises() expect(search).toHaveBeenCalledTimes(2) expect(search).toHaveBeenNthCalledWith( 1, 'foo', expect.objectContaining({ key: 'a', signal: expect.any(AbortSignal) }) ) expect(search).toHaveBeenNthCalledWith( 2, 'bar', expect.objectContaining({ key: 'b', signal: expect.any(AbortSignal) }) ) expect(onSuccess).toHaveBeenCalledTimes(2) }) it('同 key 新请求会取消旧请求并忽略过期响应', async () => { const resolves: Array<(value: string[]) => void> = [] const search = vi.fn().mockImplementation( () => new Promise((resolve) => { resolves.push(resolve) }) ) const onSuccess = vi.fn() const searcher = useKeyedDebouncedSearch({ delay: 50, search, onSuccess }) searcher.trigger('rule-1', 'first') vi.advanceTimersByTime(50) await flushPromises() searcher.trigger('rule-1', 'second') vi.advanceTimersByTime(50) await flushPromises() expect(search).toHaveBeenCalledTimes(2) resolves[1](['second']) await flushPromises() expect(onSuccess).toHaveBeenCalledTimes(1) expect(onSuccess).toHaveBeenLastCalledWith('rule-1', ['second']) resolves[0](['first']) await flushPromises() expect(onSuccess).toHaveBeenCalledTimes(1) }) it('clearKey 会取消未执行任务', () => { const search = vi.fn().mockResolvedValue([]) const onSuccess = vi.fn() const searcher = useKeyedDebouncedSearch({ delay: 100, search, onSuccess }) searcher.trigger('a', 'foo') searcher.clearKey('a') vi.advanceTimersByTime(100) expect(search).not.toHaveBeenCalled() expect(onSuccess).not.toHaveBeenCalled() }) })