task-41: storage 恢复与清理测试
恢复过滤非正数/非数字、空列表清除过期 key、终态同步 storage、 写失败不抛、坏 JSON 容错、恢复即轮询、无 key 不操作。修复恢复 出空列表时遗留 key 未清理的问题。共 8 个测试。
This commit is contained in:
@@ -108,7 +108,13 @@ function readIdsFromStorage(key?: string): number[] {
|
||||
const raw = window.localStorage.getItem(key)
|
||||
if (!raw) return []
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
return Array.isArray(parsed) ? parsed.filter((n): n is number => typeof n === 'number' && n > 0) : []
|
||||
if (!Array.isArray(parsed)) return []
|
||||
const ids = parsed.filter((n): n is number => typeof n === 'number' && n > 0)
|
||||
if (ids.length === 0) {
|
||||
// 恢复出空列表时顺手清掉过期 key(历史遗留的空数组)
|
||||
window.localStorage.removeItem(key)
|
||||
}
|
||||
return ids
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { useTaskProgressLoop } from '../src/shared/composables/useTaskProgressLoop.ts'
|
||||
|
||||
const tick = (ms = 0) => new Promise<void>((r) => globalThis.setTimeout(r, ms))
|
||||
|
||||
interface StorageLike {
|
||||
map?: Map<string, string>
|
||||
getItem: (k: string) => string | null
|
||||
setItem: (k: string, v: string) => void
|
||||
removeItem: (k: string) => void
|
||||
}
|
||||
|
||||
let scopeCounter = 0
|
||||
|
||||
interface StorageHarness {
|
||||
loop: ReturnType<typeof useTaskProgressLoop<{ taskId?: number; status?: string }>>
|
||||
storage: Map<string, string>
|
||||
fetchCount: () => number
|
||||
}
|
||||
|
||||
async function makeStorageLoop(
|
||||
storage: StorageLike,
|
||||
key: string,
|
||||
items: Array<{ taskId?: number; status?: string }> = [],
|
||||
): Promise<StorageHarness> {
|
||||
const storageMap = storage as unknown as { map: Map<string, string> }
|
||||
let fetchCount = 0
|
||||
;(globalThis as Record<string, unknown>).window = {
|
||||
localStorage: {
|
||||
getItem: (k: string) => storage.getItem(k),
|
||||
setItem: (k: string, v: string) => { storage.setItem(k, v) },
|
||||
removeItem: (k: string) => { storage.removeItem(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<{ taskId?: number; status?: string }>({
|
||||
scope: `storage-${scopeCounter++}`,
|
||||
storageKey: key,
|
||||
fetchProgress: async (ids) => ({
|
||||
items: items.filter((item) => ids.includes(item.taskId ?? -1)),
|
||||
}),
|
||||
extractTaskId: (d) => d?.taskId,
|
||||
extractStatus: (d) => d?.status,
|
||||
getIntervalMs: () => 60000,
|
||||
})
|
||||
return { loop, storage: storageMap.map, fetchCount: () => fetchCount }
|
||||
}
|
||||
|
||||
function makeStorage(initial?: Record<string, string>): StorageLike {
|
||||
const map = new Map<string, string>(Object.entries(initial ?? {}))
|
||||
return {
|
||||
map,
|
||||
getItem: (k) => map.get(k) ?? null,
|
||||
setItem: (k, v) => { map.set(k, v) },
|
||||
removeItem: (k) => { map.delete(k) },
|
||||
}
|
||||
}
|
||||
|
||||
test('test_storage_restore_valid_ids', async () => {
|
||||
const storage = makeStorage({ 'k:ids': JSON.stringify([3, 7]) })
|
||||
const h = await makeStorageLoop(storage, 'k:ids')
|
||||
assert.deepEqual(h.loop.taskIds.value, [3, 7], '初始化从 storage 恢复任务列表')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_storage_restore_filters_invalid', async () => {
|
||||
const storage = makeStorage({ 'k:ids': JSON.stringify([0, -5, 3.5, 'x', null, 9]) })
|
||||
const h = await makeStorageLoop(storage, 'k:ids')
|
||||
// 规格只过滤"非正数/非数字":3.5 是正有限数,保留
|
||||
assert.deepEqual(h.loop.taskIds.value, [3.5, 9], '过滤非正数/非数字,合法 id 保留')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_storage_restore_empty_clears', async () => {
|
||||
const storage = makeStorage({ 'k:ids': JSON.stringify([]) })
|
||||
const h = await makeStorageLoop(storage, 'k:ids')
|
||||
assert.deepEqual(h.loop.taskIds.value, [])
|
||||
await tick(10)
|
||||
assert.equal(h.storage.has('k:ids'), false, '空列表恢复后清除 storage key')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_storage_terminal_removes', async () => {
|
||||
const storage = makeStorage({ 'k:ids': JSON.stringify([5, 6]) })
|
||||
const h = await makeStorageLoop(
|
||||
storage,
|
||||
'k:ids',
|
||||
[
|
||||
{ taskId: 5, status: 'SUCCESS' },
|
||||
{ taskId: 6, status: 'RUNNING' },
|
||||
],
|
||||
)
|
||||
await tick(10)
|
||||
await h.loop.refreshOnce()
|
||||
assert.equal(h.storage.get('k:ids'), JSON.stringify([6]), '终态任务从 storage 同步移除')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_storage_write_failure_ignored', async () => {
|
||||
const storage = {
|
||||
getItem: (k: string) => (k === 'k:ids' ? '[]' : null),
|
||||
setItem: (_k: string, _v: string) => { throw new Error('quota exceeded') },
|
||||
removeItem: (_k: string) => { throw new Error('quota exceeded') },
|
||||
}
|
||||
const h = await makeStorageLoop(storage, 'k:ids')
|
||||
await tick(10)
|
||||
await h.loop.refreshOnce()
|
||||
assert.equal(h.loop.taskIds.value.length, 0)
|
||||
assert.doesNotThrow(() => h.loop.reset([1]))
|
||||
assert.deepEqual(h.loop.taskIds.value, [1], '写失败不影响功能,任务仍入集合')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_storage_bad_json', async () => {
|
||||
const storage = makeStorage({ 'k:ids': '{broken json' })
|
||||
const h = await makeStorageLoop(storage, 'k:ids')
|
||||
assert.deepEqual(h.loop.taskIds.value, [], '坏 JSON 按空处理,不抛错')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_storage_restore_starts_polling', async () => {
|
||||
let fetchCount = 0
|
||||
const storage = makeStorage({ 'k:ids': JSON.stringify([11]) })
|
||||
;(globalThis as Record<string, unknown>).window = {
|
||||
localStorage: {
|
||||
getItem: (k: string) => storage.getItem(k),
|
||||
setItem: (k: string, v: string) => { storage.setItem(k, v) },
|
||||
removeItem: (k: string) => { storage.removeItem(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<{ taskId?: number; status?: string }>({
|
||||
scope: `storage-poll-${scopeCounter++}`,
|
||||
storageKey: 'k:ids',
|
||||
fetchProgress: async () => {
|
||||
fetchCount += 1
|
||||
return { items: [] }
|
||||
},
|
||||
extractTaskId: (d) => d?.taskId,
|
||||
extractStatus: (d) => d?.status,
|
||||
getIntervalMs: () => 60000,
|
||||
})
|
||||
await tick(20)
|
||||
assert.equal(fetchCount, 1, '从 storage 恢复后自动开始轮询')
|
||||
loop.dispose()
|
||||
})
|
||||
|
||||
test('test_storage_no_key_noop', async () => {
|
||||
const storage = makeStorage({})
|
||||
const h = await makeStorageLoop(storage, 'k:ids')
|
||||
assert.deepEqual(h.loop.taskIds.value, [], '无 key 时任务列表为空')
|
||||
await tick(10)
|
||||
assert.equal(h.storage.has('k:ids'), false, '不写多余 key')
|
||||
assert.equal(h.fetchCount(), 0, '无任务不轮询')
|
||||
h.loop.dispose()
|
||||
})
|
||||
Reference in New Issue
Block a user