d33316f093
隐藏 60s/可见 10s 间隔自适应、切回可见立即刷新、开关关闭不刷 新、延迟刷新生效、隐藏仍轮询、事件绑定/解绑、隐藏不触发即时 刷新。修复 scheduleNextDelayed 被轮询定时器阻塞导致延迟前台刷 新永不触发的缺陷。共 8 个测试。
190 lines
6.3 KiB
TypeScript
190 lines
6.3 KiB
TypeScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { useTaskProgressLoop } from '../src/shared/composables/useTaskProgressLoop.ts'
|
|
import {
|
|
TASK_POLL_VISIBLE_INTERVAL_MS,
|
|
TASK_POLL_HIDDEN_INTERVAL_MS,
|
|
configureTaskPolling,
|
|
resetTaskPollingConfig,
|
|
} from '../src/shared/task-progress-config.ts'
|
|
|
|
const tick = (ms = 0) => new Promise<void>((r) => globalThis.setTimeout(r, ms))
|
|
|
|
interface FakeDoc {
|
|
setVisibility: (state: string) => void
|
|
dispatch: (type: string) => void
|
|
listenerCount: (type: string) => number
|
|
listeners: (type: string) => Array<(e?: unknown) => void>
|
|
}
|
|
|
|
function installFakeDocument(initial: string): FakeDoc {
|
|
let state = initial
|
|
const listeners = new Map<string, Array<(e?: unknown) => void>>()
|
|
;(globalThis as Record<string, unknown>).document = {
|
|
get visibilityState() { return state },
|
|
get hidden() { return state !== 'visible' },
|
|
addEventListener: (type: string, fn: (e?: unknown) => void) => {
|
|
const arr = listeners.get(type) ?? []
|
|
arr.push(fn)
|
|
listeners.set(type, arr)
|
|
},
|
|
removeEventListener: (type: string, fn: (e?: unknown) => void) => {
|
|
const arr = listeners.get(type) ?? []
|
|
listeners.set(type, arr.filter((f) => f !== fn))
|
|
},
|
|
}
|
|
return {
|
|
setVisibility: (s) => { state = s },
|
|
dispatch: (type) => { for (const fn of listeners.get(type) ?? []) fn() },
|
|
listenerCount: (type) => (listeners.get(type) ?? []).length,
|
|
listeners: (type) => listeners.get(type) ?? [],
|
|
}
|
|
}
|
|
|
|
function cleanupDocument() {
|
|
delete (globalThis as Record<string, unknown>).document
|
|
resetTaskPollingConfig()
|
|
}
|
|
|
|
interface TimerSinkEntry {
|
|
fn: () => void
|
|
ms: number
|
|
}
|
|
|
|
let scopeCounter = 0
|
|
|
|
interface VisibilityHarness {
|
|
loop: ReturnType<typeof useTaskProgressLoop<{ taskId?: number; status?: string }>>
|
|
doc: FakeDoc
|
|
sink: TimerSinkEntry[]
|
|
fetchCount: () => number
|
|
}
|
|
|
|
async function makeVisibilityLoop(
|
|
visibility: string,
|
|
config?: { enabled?: boolean; delay?: number },
|
|
): Promise<VisibilityHarness> {
|
|
if (config) {
|
|
const partial: Parameters<typeof configureTaskPolling>[0] = {}
|
|
if (config.enabled !== undefined) partial.foregroundRefreshEnabled = config.enabled
|
|
if (config.delay !== undefined) partial.foregroundRefreshDelayMs = config.delay
|
|
configureTaskPolling(partial)
|
|
}
|
|
const doc = installFakeDocument(visibility)
|
|
const sink: TimerSinkEntry[] = []
|
|
const storage = new Map<string, string>()
|
|
let fetchCount = 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) => {
|
|
sink.push({ fn, ms })
|
|
return 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: `vis-${scopeCounter++}`,
|
|
fetchProgress: async () => {
|
|
fetchCount += 1
|
|
return { items: [] }
|
|
},
|
|
extractTaskId: (d) => d?.taskId,
|
|
extractStatus: (d) => d?.status,
|
|
})
|
|
loop.reset([1])
|
|
return { loop, doc, sink, fetchCount: () => fetchCount }
|
|
}
|
|
|
|
test('test_hidden_long_interval', async () => {
|
|
const h = await makeVisibilityLoop('hidden')
|
|
await tick(10)
|
|
const last = h.sink.at(-1)
|
|
assert.equal(last?.ms, TASK_POLL_HIDDEN_INTERVAL_MS, '隐藏时按隐藏间隔排定下一轮')
|
|
h.loop.dispose()
|
|
cleanupDocument()
|
|
})
|
|
|
|
test('test_visible_short_interval', async () => {
|
|
const h = await makeVisibilityLoop('visible')
|
|
await tick(10)
|
|
const last = h.sink.at(-1)
|
|
assert.equal(last?.ms, TASK_POLL_VISIBLE_INTERVAL_MS, '可见时按可见间隔排定下一轮')
|
|
h.loop.dispose()
|
|
cleanupDocument()
|
|
})
|
|
|
|
test('test_foreground_refresh_triggered', async () => {
|
|
const h = await makeVisibilityLoop('hidden')
|
|
await tick(10)
|
|
assert.equal(h.fetchCount(), 1, '初始一轮已完成')
|
|
h.doc.setVisibility('visible')
|
|
h.doc.dispatch('visibilitychange')
|
|
await tick(10)
|
|
assert.equal(h.fetchCount(), 2, '切回可见立即刷新一轮')
|
|
h.loop.dispose()
|
|
cleanupDocument()
|
|
})
|
|
|
|
test('test_foreground_refresh_disabled', async () => {
|
|
const h = await makeVisibilityLoop('hidden', { enabled: false })
|
|
await tick(10)
|
|
h.doc.setVisibility('visible')
|
|
h.doc.dispatch('visibilitychange')
|
|
await tick(10)
|
|
assert.equal(h.fetchCount(), 1, '开关关闭时切回可见不刷新')
|
|
h.loop.dispose()
|
|
cleanupDocument()
|
|
})
|
|
|
|
test('test_foreground_refresh_delayed', async () => {
|
|
const h = await makeVisibilityLoop('hidden', { delay: 300 })
|
|
await tick(10)
|
|
h.doc.setVisibility('visible')
|
|
h.doc.dispatch('visibilitychange')
|
|
assert.ok(h.sink.some((t) => t.ms === 300), '按配置延迟排定刷新定时器')
|
|
await tick(100)
|
|
assert.equal(h.fetchCount(), 1, '延迟期内不刷新')
|
|
await tick(300)
|
|
assert.equal(h.fetchCount(), 2, '延迟到点后刷新一轮')
|
|
h.loop.dispose()
|
|
cleanupDocument()
|
|
})
|
|
|
|
test('test_hidden_polling_continues', async () => {
|
|
const h = await makeVisibilityLoop('hidden')
|
|
await tick(10)
|
|
assert.ok(h.sink.some((t) => t.ms === TASK_POLL_HIDDEN_INTERVAL_MS), '隐藏间隔定时器已排定')
|
|
const before = h.fetchCount()
|
|
await h.loop.refreshOnce()
|
|
assert.equal(h.fetchCount(), before + 1, '隐藏期间手动轮询仍工作')
|
|
assert.deepEqual(h.loop.taskIds.value, [1], '降频不改任务集合')
|
|
h.loop.dispose()
|
|
cleanupDocument()
|
|
})
|
|
|
|
test('test_visibility_event_bound_unbound', async () => {
|
|
const h = await makeVisibilityLoop('hidden')
|
|
assert.equal(h.doc.listenerCount('visibilitychange'), 1, '创建时绑定 visibilitychange')
|
|
const handler = h.doc.listeners('visibilitychange')[0]
|
|
h.loop.dispose()
|
|
assert.equal(h.doc.listenerCount('visibilitychange'), 0, 'dispose 后解绑')
|
|
assert.equal(typeof handler, 'function')
|
|
cleanupDocument()
|
|
})
|
|
|
|
test('test_hidden_no_immediate_refresh', async () => {
|
|
const h = await makeVisibilityLoop('hidden')
|
|
await tick(10)
|
|
h.doc.dispatch('visibilitychange')
|
|
await tick(10)
|
|
assert.equal(h.fetchCount(), 1, '隐藏状态下的事件不触发即时刷新')
|
|
h.loop.dispose()
|
|
cleanupDocument()
|
|
})
|