task-44: 轻量端点切换能力测试
fetchProgress 切轻量端点(taskId/status/fileStatus)后请求走轻量 实现;轻量结构解析、默认仍走旧端点、缺可选字段容错、终态判定、 缓存写入、onUpdate 收到、新旧结构同轮兼容。共 8 个测试。
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { useTaskProgressLoop } from '../src/shared/composables/useTaskProgressLoop.ts'
|
||||
import { createProgressResponseCache } from '../src/shared/progress-response-cache.ts'
|
||||
|
||||
const tick = (ms = 0) => new Promise<void>((r) => globalThis.setTimeout(r, ms))
|
||||
|
||||
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
|
||||
|
||||
/** 轻量端点条目:只含 taskId/status/fileStatus 三字段 */
|
||||
interface LightItem {
|
||||
taskId?: number
|
||||
status?: string
|
||||
fileStatus?: string
|
||||
}
|
||||
|
||||
/** 旧端点完整条目:带明细字段 */
|
||||
interface FullItem {
|
||||
taskId?: number
|
||||
status?: string
|
||||
fileStatus?: string
|
||||
fileName?: string
|
||||
progress?: number
|
||||
shopName?: string
|
||||
}
|
||||
|
||||
type AnyItem = LightItem | FullItem
|
||||
|
||||
test('test_light_fetch_used', async () => {
|
||||
setupWindow()
|
||||
// 切换:把 fetchProgress 指向轻量端点(模拟页面替换 fetch 实现)
|
||||
const storage = new Map<string, string>()
|
||||
let lightCalls = 0
|
||||
let heavyCalls = 0
|
||||
;(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),
|
||||
}
|
||||
const loop = useTaskProgressLoop<AnyItem>({
|
||||
scope: `light-switch-${scopeCounter++}`,
|
||||
fetchProgress: async (ids) => {
|
||||
lightCalls += 1
|
||||
return { items: ids.map((id) => ({ taskId: id, status: 'RUNNING', fileStatus: 'PROCESSING' })) }
|
||||
},
|
||||
extractTaskId: (d) => d?.taskId,
|
||||
extractStatus: (d) => d?.status,
|
||||
getIntervalMs: () => 60000,
|
||||
})
|
||||
loop.reset([1])
|
||||
await tick(10)
|
||||
assert.equal(lightCalls, 1, '切换到轻量端点后请求走轻量实现')
|
||||
assert.equal(heavyCalls, 0)
|
||||
assert.equal(loop.taskStatuses.value[1], 'RUNNING', '轻量响应正常落地')
|
||||
loop.dispose()
|
||||
})
|
||||
|
||||
test('test_light_detail_shape_parsed', async () => {
|
||||
setupWindow()
|
||||
const updates: AnyItem[] = []
|
||||
const loop = useTaskProgressLoop<AnyItem>({
|
||||
scope: `light-shape-${scopeCounter++}`,
|
||||
fetchProgress: async () => ({
|
||||
items: [{ taskId: 3, status: 'QUEUED', fileStatus: 'PENDING' }],
|
||||
}),
|
||||
extractTaskId: (d) => d?.taskId,
|
||||
extractStatus: (d) => d?.status,
|
||||
getIntervalMs: () => 60000,
|
||||
onUpdate: (_id, detail) => { updates.push(detail) },
|
||||
})
|
||||
loop.reset([3])
|
||||
await tick(10)
|
||||
await loop.refreshOnce()
|
||||
assert.equal(loop.taskStatuses.value[3], 'QUEUED', '轻量结构提取 taskId/status')
|
||||
assert.equal(updates[0]?.fileStatus, 'PENDING', 'onUpdate 收到完整轻量条目')
|
||||
loop.dispose()
|
||||
})
|
||||
|
||||
test('test_light_fallback_to_old', async () => {
|
||||
setupWindow()
|
||||
let heavyCalls = 0
|
||||
let lightCalls = 0
|
||||
const loop = useTaskProgressLoop<AnyItem>({
|
||||
scope: `light-old-${scopeCounter++}`,
|
||||
// 默认 fetchProgress 是旧端点(完整明细)
|
||||
fetchProgress: async () => {
|
||||
heavyCalls += 1
|
||||
return {
|
||||
items: [{ taskId: 1, status: 'RUNNING', fileName: 'a.xlsx', progress: 50, shopName: 's1' }],
|
||||
}
|
||||
},
|
||||
extractTaskId: (d) => d?.taskId,
|
||||
extractStatus: (d) => d?.status,
|
||||
getIntervalMs: () => 60000,
|
||||
})
|
||||
loop.reset([1])
|
||||
await tick(10)
|
||||
assert.equal(heavyCalls, 1, '默认仍走旧端点')
|
||||
assert.equal(lightCalls, 0, '未切换不调用轻量端点')
|
||||
assert.equal(loop.taskStatuses.value[1], 'RUNNING')
|
||||
loop.dispose()
|
||||
})
|
||||
|
||||
test('test_light_missing_optional', async () => {
|
||||
setupWindow()
|
||||
const loop = useTaskProgressLoop<AnyItem>({
|
||||
scope: `light-opt-${scopeCounter++}`,
|
||||
// 轻量条目缺 fileStatus 等可选字段
|
||||
fetchProgress: async () => ({ items: [{ taskId: 5, status: 'RUNNING' }] }),
|
||||
extractTaskId: (d) => d?.taskId,
|
||||
extractStatus: (d) => d?.status,
|
||||
getIntervalMs: () => 60000,
|
||||
})
|
||||
loop.reset([5])
|
||||
await tick(10)
|
||||
await loop.refreshOnce()
|
||||
assert.equal(loop.taskStatuses.value[5], 'RUNNING', '缺可选字段不影响解析')
|
||||
assert.deepEqual(loop.taskIds.value, [5], '任务仍在轮询集合')
|
||||
loop.dispose()
|
||||
})
|
||||
|
||||
test('test_light_terminal_detection', async () => {
|
||||
setupWindow()
|
||||
const terminalCalls: Array<{ taskId: number; status: string }> = []
|
||||
const loop = useTaskProgressLoop<AnyItem>({
|
||||
scope: `light-term-${scopeCounter++}`,
|
||||
fetchProgress: async () => ({
|
||||
items: [{ taskId: 7, status: 'SUCCESS', fileStatus: 'COMPLETED' }],
|
||||
}),
|
||||
extractTaskId: (d) => d?.taskId,
|
||||
extractStatus: (d) => d?.status,
|
||||
getIntervalMs: () => 60000,
|
||||
onTerminal: (taskId, _d, status) => { terminalCalls.push({ taskId, status }) },
|
||||
})
|
||||
loop.reset([7])
|
||||
await tick(10)
|
||||
await loop.refreshOnce()
|
||||
assert.equal(terminalCalls.length, 1, '轻量 status 正常触发终态')
|
||||
assert.equal(terminalCalls[0].status, 'SUCCESS')
|
||||
assert.deepEqual(loop.taskIds.value, [], '终态任务移除')
|
||||
loop.dispose()
|
||||
})
|
||||
|
||||
test('test_light_progress_cache', async () => {
|
||||
setupWindow()
|
||||
const cache = createProgressResponseCache<AnyItem>({ ttlMs: 5000, maxEntries: 10 })
|
||||
const loop = useTaskProgressLoop<AnyItem>({
|
||||
scope: `light-cache-${scopeCounter++}`,
|
||||
fetchProgress: async () => ({
|
||||
items: [{ taskId: 9, status: 'RUNNING', fileStatus: 'PROCESSING' }],
|
||||
}),
|
||||
extractTaskId: (d) => d?.taskId,
|
||||
extractStatus: (d) => d?.status,
|
||||
getIntervalMs: () => 60000,
|
||||
progressCache: cache,
|
||||
})
|
||||
loop.reset([9])
|
||||
await tick(10)
|
||||
await loop.refreshOnce()
|
||||
assert.deepEqual(cache.get(9), { taskId: 9, status: 'RUNNING', fileStatus: 'PROCESSING' }, '轻量条目写入缓存')
|
||||
loop.dispose()
|
||||
})
|
||||
|
||||
test('test_light_on_update_called', async () => {
|
||||
setupWindow()
|
||||
const updates: Array<{ id: number; item: AnyItem }> = []
|
||||
const loop = useTaskProgressLoop<AnyItem>({
|
||||
scope: `light-update-${scopeCounter++}`,
|
||||
fetchProgress: async () => ({
|
||||
items: [
|
||||
{ taskId: 2, status: 'RUNNING', fileStatus: 'PROCESSING' },
|
||||
{ taskId: 4, status: 'PENDING', fileStatus: 'PENDING' },
|
||||
],
|
||||
}),
|
||||
extractTaskId: (d) => d?.taskId,
|
||||
extractStatus: (d) => d?.status,
|
||||
getIntervalMs: () => 60000,
|
||||
onUpdate: (taskId, detail) => { updates.push({ id: taskId, item: detail }) },
|
||||
})
|
||||
loop.reset([2, 4])
|
||||
// reset 触发的自动轮询一轮即覆盖全部条目(勿再手动 refreshOnce 双发)
|
||||
await tick(10)
|
||||
assert.deepEqual(updates.map((u) => u.id), [2, 4], '每条轻量条目都触发 onUpdate')
|
||||
assert.equal(updates[1].item.fileStatus, 'PENDING')
|
||||
loop.dispose()
|
||||
})
|
||||
|
||||
test('test_light_snapshot_compat', async () => {
|
||||
setupWindow()
|
||||
const loop = useTaskProgressLoop<AnyItem>({
|
||||
scope: `light-mix-${scopeCounter++}`,
|
||||
// 混合响应:轻量条目 + 旧端点完整条目
|
||||
fetchProgress: async () => ({
|
||||
items: [
|
||||
{ taskId: 1, status: 'SUCCESS', fileStatus: 'COMPLETED' },
|
||||
{ taskId: 2, status: 'RUNNING', fileName: 'b.xlsx', progress: 20, shopName: 's2' },
|
||||
{ taskId: 3, status: 'FAILED', fileStatus: 'FAILED', fileName: 'c.xlsx' },
|
||||
],
|
||||
}),
|
||||
extractTaskId: (d) => d?.taskId,
|
||||
extractStatus: (d) => d?.status,
|
||||
getIntervalMs: () => 60000,
|
||||
})
|
||||
loop.reset([1, 2, 3])
|
||||
await tick(10)
|
||||
await loop.refreshOnce()
|
||||
assert.deepEqual(loop.taskStatuses.value, { 1: 'SUCCESS', 2: 'RUNNING', 3: 'FAILED' }, '新旧结构同轮兼容')
|
||||
assert.deepEqual(loop.taskIds.value, [2], '仅非终态任务保留')
|
||||
loop.dispose()
|
||||
})
|
||||
Reference in New Issue
Block a user