task-46: 双页面同 key 行为记录测试
只读断言现状(不改实现):同 storageKey 双页面各自独立轮询集合、 互不实时同步、storage 写互相覆盖、dispose 一方不影响另一方、 不同 key 完全隔离。共 8 个测试。
This commit is contained in:
@@ -0,0 +1,184 @@
|
|||||||
|
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))
|
||||||
|
|
||||||
|
let scopeCounter = 0
|
||||||
|
|
||||||
|
/** 双页面共用同一个 storage Map(模拟同一浏览器标签页内两个页面共享 localStorage) */
|
||||||
|
function makeSharedStorage() {
|
||||||
|
const storage = new Map<string, string>()
|
||||||
|
return {
|
||||||
|
map: storage,
|
||||||
|
install() {
|
||||||
|
;(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),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item = { taskId?: number; status?: string }
|
||||||
|
|
||||||
|
interface LoopHarness {
|
||||||
|
loop: ReturnType<typeof useTaskProgressLoop<Item>>
|
||||||
|
fetchCount: () => number
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeLoop(scope: string, items: Item[] = []): LoopHarness {
|
||||||
|
let fetchCount = 0
|
||||||
|
const loop = useTaskProgressLoop<Item>({
|
||||||
|
scope,
|
||||||
|
storageKey: 'shared: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,
|
||||||
|
fetchCount: () => {
|
||||||
|
fetchCount += 0
|
||||||
|
return fetchCount
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('test_two_loops_same_key_independent', async () => {
|
||||||
|
const shared = makeSharedStorage()
|
||||||
|
shared.install()
|
||||||
|
const a = makeLoop(`dual-a-${scopeCounter++}`)
|
||||||
|
const b = makeLoop(`dual-b-${scopeCounter++}`)
|
||||||
|
a.loop.reset([1])
|
||||||
|
b.loop.reset([2])
|
||||||
|
await tick(10)
|
||||||
|
assert.deepEqual(a.loop.taskIds.value, [1], 'A 页面集合独立')
|
||||||
|
assert.deepEqual(b.loop.taskIds.value, [2], 'B 页面集合独立')
|
||||||
|
assert.deepEqual(shared.map.get('shared:key'), JSON.stringify([2]), 'storage 只反映最后写入方(现状)')
|
||||||
|
a.loop.dispose()
|
||||||
|
b.loop.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_shared_key_no_crash', async () => {
|
||||||
|
const shared = makeSharedStorage()
|
||||||
|
shared.install()
|
||||||
|
shared.map.set('shared:key', JSON.stringify([9]))
|
||||||
|
const a = makeLoop(`dual-crash-a-${scopeCounter++}`)
|
||||||
|
const b = makeLoop(`dual-crash-b-${scopeCounter++}`)
|
||||||
|
await tick(10)
|
||||||
|
assert.equal(a.loop.taskIds.value.length, 1, '同 key 共存不崩溃')
|
||||||
|
assert.equal(b.loop.taskIds.value.length, 1)
|
||||||
|
a.loop.dispose()
|
||||||
|
b.loop.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_shared_key_last_write', async () => {
|
||||||
|
const shared = makeSharedStorage()
|
||||||
|
shared.install()
|
||||||
|
const a = makeLoop(`dual-lw-a-${scopeCounter++}`)
|
||||||
|
const b = makeLoop(`dual-lw-b-${scopeCounter++}`)
|
||||||
|
a.loop.reset([1, 2])
|
||||||
|
await tick(5)
|
||||||
|
b.loop.reset([3])
|
||||||
|
await tick(5)
|
||||||
|
assert.deepEqual(shared.map.get('shared:key'), JSON.stringify([3]), '后写入覆盖先写入(现状,不保证合并)')
|
||||||
|
a.loop.dispose()
|
||||||
|
b.loop.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_shared_key_restore_both', async () => {
|
||||||
|
const shared = makeSharedStorage()
|
||||||
|
shared.install()
|
||||||
|
shared.map.set('shared:key', JSON.stringify([7]))
|
||||||
|
const a = makeLoop(`dual-restore-a-${scopeCounter++}`)
|
||||||
|
const b = makeLoop(`dual-restore-b-${scopeCounter++}`)
|
||||||
|
await tick(5)
|
||||||
|
assert.deepEqual(a.loop.taskIds.value, [7], '双方都从同一 key 恢复同一集合')
|
||||||
|
assert.deepEqual(b.loop.taskIds.value, [7])
|
||||||
|
a.loop.dispose()
|
||||||
|
b.loop.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_shared_key_no_live_sync', async () => {
|
||||||
|
const shared = makeSharedStorage()
|
||||||
|
shared.install()
|
||||||
|
const a = makeLoop(`dual-nosync-a-${scopeCounter++}`)
|
||||||
|
const b = makeLoop(`dual-nosync-b-${scopeCounter++}`)
|
||||||
|
a.loop.reset([1])
|
||||||
|
await tick(10)
|
||||||
|
assert.deepEqual(a.loop.taskIds.value, [1])
|
||||||
|
// B 页面内存集合仍是空:无 storage 事件实时同步(现状)
|
||||||
|
assert.deepEqual(b.loop.taskIds.value, [], 'A 的改动不会实时同步到 B 内存(已知边界)')
|
||||||
|
a.loop.dispose()
|
||||||
|
b.loop.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_shared_key_dispose_one', async () => {
|
||||||
|
const shared = makeSharedStorage()
|
||||||
|
shared.install()
|
||||||
|
const a = makeLoop(`dual-dispose-a-${scopeCounter++}`)
|
||||||
|
const b = makeLoop(`dual-dispose-b-${scopeCounter++}`)
|
||||||
|
a.loop.reset([1])
|
||||||
|
b.loop.reset([2])
|
||||||
|
await tick(5)
|
||||||
|
a.loop.dispose()
|
||||||
|
const bIds = b.loop.taskIds.value
|
||||||
|
assert.deepEqual(bIds, [2], 'A 卸载后 B 集合不受影响')
|
||||||
|
b.loop.reset([2, 3])
|
||||||
|
assert.deepEqual(b.loop.taskIds.value, [2, 3], 'B 继续正常工作')
|
||||||
|
b.loop.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_documented_boundary', async () => {
|
||||||
|
const shared = makeSharedStorage()
|
||||||
|
shared.install()
|
||||||
|
const a = makeLoop(`dual-doc-a-${scopeCounter++}`)
|
||||||
|
const b = makeLoop(`dual-doc-b-${scopeCounter++}`)
|
||||||
|
a.loop.reset([1])
|
||||||
|
b.loop.reset([2])
|
||||||
|
await tick(10)
|
||||||
|
// 现状边界:同 key 双页面各自持有内存集合,storage 是最后写入方,互不等待
|
||||||
|
assert.deepEqual(a.loop.taskIds.value, [1], '内存集合各自独立')
|
||||||
|
assert.deepEqual(b.loop.taskIds.value, [2])
|
||||||
|
assert.equal(shared.map.get('shared:key'), JSON.stringify([2]))
|
||||||
|
a.loop.dispose()
|
||||||
|
b.loop.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_shared_key_separate_storage', async () => {
|
||||||
|
const shared = makeSharedStorage()
|
||||||
|
shared.install()
|
||||||
|
const makeKeyedLoop = (scope: string, storageKey: string): LoopHarness => {
|
||||||
|
const loop = useTaskProgressLoop<Item>({
|
||||||
|
scope,
|
||||||
|
storageKey,
|
||||||
|
fetchProgress: async (ids) => ({ items: ids.map((id) => ({ taskId: id, status: 'RUNNING' })) }),
|
||||||
|
extractTaskId: (d) => d?.taskId,
|
||||||
|
extractStatus: (d) => d?.status,
|
||||||
|
getIntervalMs: () => 60000,
|
||||||
|
})
|
||||||
|
return { loop, fetchCount: () => 0 }
|
||||||
|
}
|
||||||
|
const a = makeKeyedLoop(`dual-sep-a-${scopeCounter++}`, 'key:a')
|
||||||
|
const b = makeKeyedLoop(`dual-sep-b-${scopeCounter++}`, 'key:b')
|
||||||
|
a.loop.reset([1])
|
||||||
|
b.loop.reset([2])
|
||||||
|
await tick(10)
|
||||||
|
assert.deepEqual(a.loop.taskIds.value, [1])
|
||||||
|
assert.deepEqual(b.loop.taskIds.value, [2])
|
||||||
|
assert.equal(shared.map.get('key:a'), JSON.stringify([1]), '不同 key 完全隔离')
|
||||||
|
assert.equal(shared.map.get('key:b'), JSON.stringify([2]))
|
||||||
|
a.loop.dispose()
|
||||||
|
b.loop.dispose()
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user