import { test } from 'node:test' import assert from 'node:assert/strict' import { useTaskProgressLoop } from '../src/shared/composables/useTaskProgressLoop.ts' 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 interface Item { taskId?: number status?: string label?: string } interface TerminalEvent { taskId: number detail: Item status: string } interface Harness { loop: ReturnType> terminalCalls: TerminalEvent[] errors: unknown[] } async function makeLoop( items: Item[], terminalImpl?: (event: TerminalEvent) => void, resetIds: number[] = [1, 2, 3], ): Promise { const terminalCalls: TerminalEvent[] = [] const errors: unknown[] = [] const loop = useTaskProgressLoop({ scope: `multi-term-${scopeCounter++}`, fetchProgress: async (ids) => ({ items: items.filter((item) => ids.includes(item.taskId ?? -1)), }), extractTaskId: (d) => d?.taskId, extractStatus: (d) => d?.status, getIntervalMs: () => 60000, onTerminal: (taskId, detail, status) => { const event = { taskId, detail: detail ?? {}, status } terminalCalls.push(event) terminalImpl?.(event) }, onError: (e) => { errors.push(e) }, }) loop.reset(resetIds) await new Promise((r) => globalThis.setTimeout(r, 0)) return { loop, terminalCalls, errors } } test('test_multi_terminal_each_called', async () => { setupWindow() const h = await makeLoop([ { taskId: 1, status: 'SUCCESS' }, { taskId: 2, status: 'FAILED' }, { taskId: 3, status: 'SUCCESS' }, ]) await h.loop.refreshOnce() assert.equal(h.terminalCalls.length, 3, '三个终态各回调一次') assert.deepEqual(h.terminalCalls.map((e) => e.taskId), [1, 2, 3]) h.loop.dispose() }) test('test_multi_terminal_correct_detail', async () => { setupWindow() const h = await makeLoop([ { taskId: 1, status: 'SUCCESS', label: 'a' }, { taskId: 2, status: 'FAILED', label: 'b' }, ]) await h.loop.refreshOnce() assert.equal(h.terminalCalls[0].detail.label, 'a', '回调携带各自 detail') assert.equal(h.terminalCalls[1].detail.label, 'b') h.loop.dispose() }) test('test_multi_terminal_correct_status', async () => { setupWindow() const h = await makeLoop([ { taskId: 1, status: 'SUCCESS' }, { taskId: 2, status: 'FAILED' }, ]) await h.loop.refreshOnce() assert.equal(h.terminalCalls[0].status, 'SUCCESS') assert.equal(h.terminalCalls[1].status, 'FAILED', '各自 status 分别传入') h.loop.dispose() }) test('test_multi_terminal_order', async () => { setupWindow() // 轮询集合按 [3,2,1] 注册,响应按 [1,2,3] 返回:回调必须跟随响应顺序 const h = await makeLoop( [ { taskId: 1, status: 'FAILED' }, { taskId: 2, status: 'SUCCESS' }, { taskId: 3, status: 'SUCCESS' }, ], undefined, [3, 2, 1], ) await h.loop.refreshOnce() assert.deepEqual( h.terminalCalls.map((e) => e.taskId), [1, 2, 3], '回调顺序与响应顺序一致(非 taskIds 注册顺序)', ) h.loop.dispose() }) test('test_multi_terminal_one_fails_other_runs', async () => { setupWindow() const h = await makeLoop( [ { taskId: 1, status: 'SUCCESS' }, { taskId: 2, status: 'FAILED' }, ], (event) => { if (event.taskId === 1) throw new Error('boom') }, ) await h.loop.refreshOnce() assert.equal(h.terminalCalls.length, 2, '一个抛错不影响另一个回调') assert.equal(h.terminalCalls[1].taskId, 2) h.loop.dispose() }) test('test_multi_terminal_all_removed', async () => { setupWindow() const h = await makeLoop( [ { taskId: 1, status: 'SUCCESS' }, { taskId: 2, status: 'FAILED' }, ], undefined, [1, 2], ) await h.loop.refreshOnce() assert.deepEqual(h.loop.taskIds.value, [], '全部终态任务移除') h.loop.dispose() }) test('test_multi_terminal_await_sequence', async () => { setupWindow() const order: string[] = [] const h = await makeLoop( [ { taskId: 1, status: 'SUCCESS' }, { taskId: 2, status: 'SUCCESS' }, ], (event) => { order.push(`cb:${event.taskId}`) }, ) await h.loop.refreshOnce() assert.deepEqual(order, ['cb:1', 'cb:2'], '回调按响应顺序依次执行') h.loop.dispose() }) test('test_multi_terminal_async_on_terminal', async () => { setupWindow() const order: string[] = [] let finished = 0 const items = [ { taskId: 1, status: 'SUCCESS' }, { taskId: 2, status: 'SUCCESS' }, ] const loop = useTaskProgressLoop({ scope: `multi-term-async-${scopeCounter++}`, fetchProgress: async (ids) => ({ items: items.filter((item) => ids.includes(item.taskId ?? -1)), }), extractTaskId: (d) => d?.taskId, extractStatus: (d) => d?.status, getIntervalMs: () => 60000, onTerminal: async (taskId) => { order.push(`start:${taskId}`) await new Promise((r) => globalThis.setTimeout(r, 10)) order.push(`end:${taskId}`) finished += 1 }, }) // reset 触发 watch 自动轮询一轮;等待该轮全部回调完成(勿手动 refreshOnce 双发) loop.reset([1, 2]) const deadline = Date.now() + 3000 while (finished < 2 && Date.now() < deadline) { await new Promise((r) => globalThis.setTimeout(r, 5)) } assert.equal(finished, 2, '两个异步回调均完成') assert.deepEqual(order, ['start:1', 'end:1', 'start:2', 'end:2'], '异步回调逐个 await,不并发') loop.dispose() })