Files
crawler-plugin/frontend-vue/tests/polling-dual-page-key.test.ts
T
huangzd1997 969c5a2a67 task-46: 双页面同 key 行为记录测试
只读断言现状(不改实现):同 storageKey 双页面各自独立轮询集合、
互不实时同步、storage 写互相覆盖、dispose 一方不影响另一方、
不同 key 完全隔离。共 8 个测试。
2026-08-31 21:17:59 +08:00

185 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
})