task-37: onTerminal 抛错语义测试
覆盖 onTerminal 回调抛错时的语义:不打断轮询、任务照常移除、 可重新 add 恢复、不影响同轮其他终态、抛错被吞掉不进 onError、 不自动重试、恢复后重新触发。共 8 个测试。
This commit is contained in:
@@ -0,0 +1,162 @@
|
|||||||
|
import { test } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { useTaskProgressLoop } from '../src/shared/composables/useTaskProgressLoop.ts'
|
||||||
|
import { createTaskPollingCoordinator } from '../src/shared/task-polling-coordinator.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 Harness {
|
||||||
|
loop: ReturnType<typeof useTaskProgressLoop<{ taskId?: number; status?: string }>>
|
||||||
|
terminalCalls: Array<{ taskId: number; status: string }>
|
||||||
|
errors: unknown[]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeLoop(
|
||||||
|
terminalImpl: (taskId: number, status: string) => void,
|
||||||
|
items: Array<{ taskId?: number; status?: string }>,
|
||||||
|
): Promise<Harness> {
|
||||||
|
const terminalCalls: Array<{ taskId: number; status: string }> = []
|
||||||
|
const errors: unknown[] = []
|
||||||
|
const loop = useTaskProgressLoop<{ taskId?: number; status?: string }>({
|
||||||
|
scope: `term-err-${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, _d, status) => {
|
||||||
|
terminalCalls.push({ taskId, status })
|
||||||
|
terminalImpl(taskId, status)
|
||||||
|
},
|
||||||
|
onError: (e) => { errors.push(e) },
|
||||||
|
})
|
||||||
|
loop.reset([1, 2])
|
||||||
|
await new Promise<void>((r) => globalThis.setTimeout(r, 0))
|
||||||
|
return { loop, terminalCalls, errors }
|
||||||
|
}
|
||||||
|
|
||||||
|
test('test_terminal_error_does_not_break_loop', async () => {
|
||||||
|
setupWindow()
|
||||||
|
const h = await makeLoop(
|
||||||
|
() => { throw new Error('refresh failed') },
|
||||||
|
[{ taskId: 1, status: 'SUCCESS' }],
|
||||||
|
)
|
||||||
|
await h.loop.refreshOnce()
|
||||||
|
await h.loop.refreshOnce()
|
||||||
|
assert.equal(h.errors.length, 0, 'onTerminal 抛错被吞掉,不进入 onError')
|
||||||
|
assert.deepEqual(h.loop.taskIds.value, [2], '循环仍运转')
|
||||||
|
h.loop.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_terminal_error_task_removed', async () => {
|
||||||
|
setupWindow()
|
||||||
|
const h = await makeLoop(
|
||||||
|
() => { throw new Error('boom') },
|
||||||
|
[{ taskId: 1, status: 'SUCCESS' }],
|
||||||
|
)
|
||||||
|
await h.loop.refreshOnce()
|
||||||
|
assert.deepEqual(h.loop.taskIds.value, [2], '抛错任务已从轮询集合移除')
|
||||||
|
h.loop.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_terminal_error_re_add_works', async () => {
|
||||||
|
setupWindow()
|
||||||
|
const h = await makeLoop(
|
||||||
|
() => { throw new Error('boom') },
|
||||||
|
[{ taskId: 1, status: 'SUCCESS' }],
|
||||||
|
)
|
||||||
|
await h.loop.refreshOnce()
|
||||||
|
h.loop.add(1)
|
||||||
|
assert.deepEqual(h.loop.taskIds.value, [2, 1], '页面可自行重新 add() 恢复')
|
||||||
|
h.loop.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_terminal_error_other_tasks', async () => {
|
||||||
|
setupWindow()
|
||||||
|
const h = await makeLoop(
|
||||||
|
(taskId) => {
|
||||||
|
if (taskId === 1) throw new Error('boom')
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{ taskId: 1, status: 'SUCCESS' },
|
||||||
|
{ taskId: 2, status: 'FAILED' },
|
||||||
|
],
|
||||||
|
)
|
||||||
|
await h.loop.refreshOnce()
|
||||||
|
assert.equal(h.terminalCalls.length, 2, '其他终态仍回调')
|
||||||
|
assert.equal(h.terminalCalls[1].taskId, 2)
|
||||||
|
assert.deepEqual(h.loop.taskIds.value, [], '全部终态任务移除')
|
||||||
|
h.loop.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_terminal_error_swallowed', async () => {
|
||||||
|
setupWindow()
|
||||||
|
const h = await makeLoop(
|
||||||
|
() => { throw new Error('boom') },
|
||||||
|
[{ taskId: 1, status: 'SUCCESS' }],
|
||||||
|
)
|
||||||
|
await h.loop.refreshOnce()
|
||||||
|
const result = await h.loop.refreshOnce()
|
||||||
|
assert.equal(result, undefined, 'refreshOnce 不因 onTerminal 抛错而 reject')
|
||||||
|
h.loop.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_terminal_error_then_success', async () => {
|
||||||
|
setupWindow()
|
||||||
|
let fail = true
|
||||||
|
const h = await makeLoop(
|
||||||
|
() => {
|
||||||
|
if (fail) throw new Error('boom')
|
||||||
|
},
|
||||||
|
[{ taskId: 2, status: 'SUCCESS' }],
|
||||||
|
)
|
||||||
|
await h.loop.refreshOnce()
|
||||||
|
fail = false
|
||||||
|
h.loop.add(2)
|
||||||
|
// add() 经 watch 触发一轮即时自动轮询,等待该轮完成(勿再手动 refreshOnce 以免双发)
|
||||||
|
await new Promise<void>((r) => globalThis.setTimeout(r, 0))
|
||||||
|
assert.equal(h.terminalCalls.length, 2, '后一轮正常触发')
|
||||||
|
assert.equal(h.terminalCalls[1].status, 'SUCCESS')
|
||||||
|
h.loop.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_terminal_error_not_retried', async () => {
|
||||||
|
setupWindow()
|
||||||
|
const h = await makeLoop(
|
||||||
|
() => { throw new Error('boom') },
|
||||||
|
[{ taskId: 1, status: 'SUCCESS' }],
|
||||||
|
)
|
||||||
|
await h.loop.refreshOnce()
|
||||||
|
await h.loop.refreshOnce()
|
||||||
|
assert.equal(h.terminalCalls.length, 1, '不自动重试回调')
|
||||||
|
h.loop.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_terminal_error_status_kept', async () => {
|
||||||
|
setupWindow()
|
||||||
|
const coordinator = createTaskPollingCoordinator({})
|
||||||
|
const h = await makeLoop(
|
||||||
|
() => { throw new Error('boom') },
|
||||||
|
[{ taskId: 1, status: 'SUCCESS' }],
|
||||||
|
)
|
||||||
|
// 用协调器包装后,状态保留在 taskStatuses
|
||||||
|
assert.equal(h.loop.taskStatuses.value[1], undefined, '移除后不再有状态')
|
||||||
|
assert.equal(coordinator.has(1), false, '协调器无残留')
|
||||||
|
h.loop.dispose()
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user