import { effectScope, nextTick, ref } from 'vue' import { afterEach, describe, expect, it, vi } from 'vitest' import { useQuery } from '@/composables/useQuery' afterEach(() => vi.useRealTimers()) /** 测试中尚未完成的查询请求。 */ interface PendingRequest { /** 完成当前查询。 */ resolve: (value: string) => void /** 当前查询的取消信号。 */ signal: AbortSignal } describe('异步查询生命周期', () => { it('页面停留期间不重复请求,仍支持首次读取、手动刷新及查询目标切换', async () => { vi.useFakeTimers() const key = ref('first') const loader = vi.fn<(id: string) => Promise>(async id => id) const scope = effectScope() const query = scope.run(() => useQuery(key, loader))! await Promise.resolve() expect(query.data.value).toBe('first') await vi.advanceTimersByTimeAsync(30_000) expect(loader).toHaveBeenCalledTimes(1) await query.refresh() expect(loader).toHaveBeenCalledTimes(2) key.value = 'second' await nextTick() await Promise.resolve() expect(query.data.value).toBe('second') await vi.advanceTimersByTimeAsync(30_000) expect(loader).toHaveBeenCalledTimes(3) scope.stop() }) it('切换目标取消旧请求并忽略迟到响应', async () => { const key = ref('first') const requests = new Map() const loader = vi.fn<(id: string, signal: AbortSignal) => Promise>( (id, signal) => new Promise(resolve => { requests.set(id, { resolve, signal }) }) ) const scope = effectScope() const query = scope.run(() => useQuery(key, loader))! key.value = 'second' await nextTick() expect(requests.get('first')!.signal.aborted).toBe(true) requests.get('second')!.resolve('新项目') await Promise.resolve() requests.get('first')!.resolve('旧项目') await Promise.resolve() expect(query.data.value).toBe('新项目') scope.stop() expect(requests.get('second')!.signal.aborted).toBe(true) }) it('失败时保留已有数据,并向页面显示错误', async () => { const scope = effectScope() const loader = vi .fn<() => Promise>() .mockResolvedValueOnce('上次成功数据') .mockRejectedValueOnce(new Error('连接已断开')) const query = scope.run(() => useQuery(ref('p'), loader))! await Promise.resolve() await query.refresh() expect(query.data.value).toBe('上次成功数据') expect(query.error.value).toBe('连接已断开') scope.stop() }) })