dd0a32e89a
8 个测试固化网络错误语义:fetch 拒绝不触发 onTerminal、不修改 taskStatuses、任务集合不丢失、onError 上报、失败后不立即自动重试 (退避)、恢复后继续轮询并正常触发终态。 行为由任务 31 的失败计数与既有 catch 路径实现,本次为验证型测试。
135 lines
4.8 KiB
TypeScript
135 lines
4.8 KiB
TypeScript
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 Harness {
|
|
loop: ReturnType<typeof useTaskProgressLoop<{ taskId?: number; status?: string }>>
|
|
terminalCalls: Array<{ taskId: number; status: string }>
|
|
errorCalls: unknown[]
|
|
fetchCalls: () => number
|
|
}
|
|
|
|
async function makeLoop(impl: () => Promise<{ items: unknown[] }>): Promise<Harness> {
|
|
const terminalCalls: Array<{ taskId: number; status: string }> = []
|
|
const errorCalls: unknown[] = []
|
|
let fetchCount = 0
|
|
const loop = useTaskProgressLoop<{ taskId?: number; status?: string }>({
|
|
scope: `net-err-${scopeCounter++}`,
|
|
fetchProgress: async (ids) => {
|
|
fetchCount += 1
|
|
return impl()
|
|
},
|
|
extractTaskId: (d) => d?.taskId,
|
|
extractStatus: (d) => d?.status,
|
|
getIntervalMs: () => 60000,
|
|
onTerminal: (taskId, _detail, status) => { terminalCalls.push({ taskId, status }) },
|
|
onError: (e) => { errorCalls.push(e) },
|
|
})
|
|
loop.reset([1, 2])
|
|
await new Promise<void>((r) => globalThis.setTimeout(r, 0))
|
|
return { loop, terminalCalls, errorCalls, fetchCalls: () => fetchCount }
|
|
}
|
|
|
|
test('test_network_error_no_terminal', async () => {
|
|
setupWindow()
|
|
const h = await makeLoop(async () => { throw new Error('network down') })
|
|
await h.loop.refreshOnce()
|
|
await h.loop.refreshOnce()
|
|
assert.equal(h.terminalCalls.length, 0, '网络错误不触发 onTerminal')
|
|
h.loop.dispose()
|
|
})
|
|
|
|
test('test_network_error_no_status_change', async () => {
|
|
setupWindow()
|
|
const h = await makeLoop(async () => { throw new Error('network down') })
|
|
await h.loop.refreshOnce()
|
|
assert.deepEqual(h.loop.taskStatuses.value, {}, '网络错误不修改 taskStatuses')
|
|
h.loop.dispose()
|
|
})
|
|
|
|
test('test_network_error_backoff_scheduled', async () => {
|
|
setupWindow()
|
|
const h = await makeLoop(async () => { throw new Error('network down') })
|
|
// 首轮自动刷新失败后:不立刻重试(下一轮按 60s 正常间隔调度),
|
|
// 短期内不应有新的 fetch 请求
|
|
const before = h.fetchCalls()
|
|
await new Promise<void>((r) => globalThis.setTimeout(r, 50))
|
|
assert.equal(h.fetchCalls(), before, '失败后进入退避,不立即自动重试')
|
|
h.loop.dispose()
|
|
})
|
|
|
|
test('test_network_error_on_error_called', async () => {
|
|
setupWindow()
|
|
const h = await makeLoop(async () => { throw new Error('network down') })
|
|
assert.equal(h.errorCalls.length, 1, 'onError 被调用')
|
|
assert.match((h.errorCalls[0] as Error).message, /network down/)
|
|
h.loop.dispose()
|
|
})
|
|
|
|
test('test_network_recovery', async () => {
|
|
setupWindow()
|
|
let fail = true
|
|
const h = await makeLoop(async () => {
|
|
if (fail) throw new Error('network down')
|
|
return { items: [{ taskId: 1, status: 'RUNNING' }] }
|
|
})
|
|
await h.loop.refreshOnce()
|
|
fail = false
|
|
await h.loop.refreshOnce()
|
|
assert.equal(h.loop.taskStatuses.value[1], 'RUNNING', '恢复后继续轮询并落地状态')
|
|
h.loop.dispose()
|
|
})
|
|
|
|
test('test_network_error_repeated', async () => {
|
|
setupWindow()
|
|
const h = await makeLoop(async () => { throw new Error('network down') })
|
|
for (let i = 0; i < 5; i++) {
|
|
await h.loop.refreshOnce()
|
|
}
|
|
assert.equal(h.errorCalls.length, 6, '多次失败仍退避上报')
|
|
assert.equal(h.terminalCalls.length, 0)
|
|
h.loop.dispose()
|
|
})
|
|
|
|
test('test_network_error_keeps_task', async () => {
|
|
setupWindow()
|
|
const h = await makeLoop(async () => { throw new Error('network down') })
|
|
await h.loop.refreshOnce()
|
|
assert.deepEqual(h.loop.taskIds.value, [1, 2], '任务不丢失')
|
|
h.loop.dispose()
|
|
})
|
|
|
|
test('test_network_error_then_success', async () => {
|
|
setupWindow()
|
|
let fail = true
|
|
const h = await makeLoop(async () => {
|
|
if (fail) throw new Error('network down')
|
|
return { items: [{ taskId: 1, status: 'SUCCESS' }] }
|
|
})
|
|
await h.loop.refreshOnce()
|
|
assert.equal(h.terminalCalls.length, 0, '网络错误阶段无终态回调')
|
|
fail = false
|
|
await h.loop.refreshOnce()
|
|
assert.equal(h.terminalCalls.length, 1, '恢复后成功响应正常触发终态')
|
|
assert.equal(h.terminalCalls[0].taskId, 1)
|
|
assert.equal(h.terminalCalls[0].status, 'SUCCESS')
|
|
h.loop.dispose()
|
|
})
|