task-38: 同轮多任务终态分别回调测试

同一轮响应多任务同时终态:各自回调带各自 detail/status、
按响应顺序(非注册顺序)、一个抛错不影响其他、全部移除、
异步回调逐个 await。共 8 个测试。
This commit is contained in:
2026-08-31 20:52:02 +08:00
parent 01e00f4b98
commit 89fff86333
@@ -0,0 +1,204 @@
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<string, string>()
;(globalThis as Record<string, unknown>).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<typeof useTaskProgressLoop<Item>>
terminalCalls: TerminalEvent[]
errors: unknown[]
}
async function makeLoop(
items: Item[],
terminalImpl?: (event: TerminalEvent) => void,
resetIds: number[] = [1, 2, 3],
): Promise<Harness> {
const terminalCalls: TerminalEvent[] = []
const errors: unknown[] = []
const loop = useTaskProgressLoop<Item>({
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<void>((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<Item>({
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<void>((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<void>((r) => globalThis.setTimeout(r, 5))
}
assert.equal(finished, 2, '两个异步回调均完成')
assert.deepEqual(order, ['start:1', 'end:1', 'start:2', 'end:2'], '异步回调逐个 await,不并发')
loop.dispose()
})