import { test } from 'node:test' import assert from 'node:assert/strict' import { useTaskProgressLoop } from '../src/shared/composables/useTaskProgressLoop.ts' interface LoopCallbacks { onError?: (error: unknown, attempt?: number) => void fetchImpl?: () => Promise<{ items: unknown[] }> } function setupWindow() { const storage = new Map() ;(globalThis as Record).window = { localStorage: { getItem: (k: string) => storage.get(k) ?? null, setItem: (k: string, v: string) => { storage.set(k, v) }, removeItem: (k: string) => { storage.delete(k) }, }, setTimeout: (fn: () => void, ms: number) => globalThis.setTimeout(fn, ms), clearTimeout: (id: unknown) => globalThis.clearTimeout(id as number), setInterval: (fn: () => void, ms: number) => globalThis.setInterval(fn, ms), clearInterval: (id: unknown) => globalThis.clearInterval(id as number), } } let scopeCounter = 0 function tick() { return new Promise((resolve) => globalThis.setTimeout(resolve, 0)) } /** * 构造轮询循环并等待第一轮(构造时因任务从空到非空自动触发) * 完成后返回;后续手动 refreshOnce 的计数因此从稳定起点开始。 */ async function makeLoop(callbacks: LoopCallbacks) { const loop = useTaskProgressLoop<{ taskId?: number; status?: string }>({ scope: `failure-count-${scopeCounter++}`, fetchProgress: callbacks.fetchImpl ?? (async () => ({ items: [] })), extractTaskId: (detail) => detail?.taskId, extractStatus: (detail) => detail?.status, getIntervalMs: () => 60000, onError: callbacks.onError, }) loop.reset([1]) await tick() return loop } test('test_failure_count_increments', async () => { setupWindow() const attempts: number[] = [] const loop = await makeLoop({ onError: (_e, attempt) => attempts.push(attempt ?? -1), fetchImpl: async () => { throw new Error('net down') }, }) await loop.refreshOnce() await loop.refreshOnce() // [首轮自动刷新(0), 手动第1次(1), 手动第2次(2)] assert.deepEqual(attempts, [0, 1, 2], '连续失败 attempt 递增') loop.dispose() }) test('test_success_resets_count', async () => { setupWindow() const attempts: number[] = [] let fail = true const loop = await makeLoop({ onError: (_e, attempt) => attempts.push(attempt ?? -1), fetchImpl: async () => { if (fail) throw new Error('net down') return { items: [] } }, }) await loop.refreshOnce() await loop.refreshOnce() fail = false await loop.refreshOnce() fail = true await loop.refreshOnce() assert.deepEqual(attempts, [0, 1, 2, 0], '成功后计数归零重新从 0 起') loop.dispose() }) test('test_any_response_resets', async () => { setupWindow() const attempts: number[] = [] let fail = true const loop = await makeLoop({ onError: (_e, attempt) => attempts.push(attempt ?? -1), fetchImpl: async () => { if (fail) throw new Error('net down') return { items: [] } }, }) fail = false await loop.refreshOnce() assert.deepEqual(attempts, [0], '收到响应后失败计数归零') loop.dispose() }) test('test_on_error_receives_attempt', async () => { setupWindow() const received: Array<{ error: unknown; attempt?: number }> = [] const loop = await makeLoop({ onError: (error, attempt) => received.push({ error, attempt }), fetchImpl: async () => { throw new Error('boom') }, }) assert.equal(received.length, 1) assert.equal(received[0].attempt, 0) assert.match((received[0].error as Error).message, /boom/) loop.dispose() }) test('test_initial_count_zero', async () => { setupWindow() const attempts: number[] = [] const loop = await makeLoop({ onError: (_e, attempt) => attempts.push(attempt ?? -1), fetchImpl: async () => { throw new Error('x') }, }) assert.equal(attempts[0], 0, '首次失败 attempt 为 0') loop.dispose() }) test('test_interleaved_fail_success', async () => { setupWindow() const attempts: number[] = [] let fail = true const loop = await makeLoop({ onError: (_e, attempt) => attempts.push(attempt ?? -1), fetchImpl: async () => { if (fail) throw new Error('net') return { items: [] } }, }) for (let i = 0; i < 5; i++) { fail = i % 2 === 0 await loop.refreshOnce() } // [构造轮首轮失败(0), i=0 失败(1), i=2 失败(0), i=4 失败(0)] assert.deepEqual(attempts, [0, 1, 0, 0], '失败成功交错,每次失败从归零后的计数起') loop.dispose() }) test('test_count_bounded', async () => { setupWindow() const attempts: number[] = [] const loop = await makeLoop({ onError: (_e, attempt) => attempts.push(attempt ?? -1), fetchImpl: async () => { throw new Error('net') }, }) for (let i = 0; i < 150; i++) { await loop.refreshOnce() } assert.equal(attempts.length, 151) assert.equal(attempts[100], 100) const tail = attempts.slice(101) assert.ok(tail.every((a) => a === 100), '超过上限后 attempt 封顶不再增长') loop.dispose() }) test('test_dispose_clears_count', async () => { setupWindow() let onErrorCalls = 0 let fetchCalls = 0 const loop = await makeLoop({ onError: () => { onErrorCalls += 1 }, fetchImpl: async () => { fetchCalls += 1 throw new Error('net') }, }) await loop.refreshOnce() assert.equal(onErrorCalls, 2) loop.dispose() await loop.refreshOnce() assert.equal(fetchCalls, 2, 'dispose 后不再发请求') assert.equal(onErrorCalls, 2, 'dispose 后不再上报错误') })