task-40: 页面隐藏降频与可见恢复测试

隐藏 60s/可见 10s 间隔自适应、切回可见立即刷新、开关关闭不刷
新、延迟刷新生效、隐藏仍轮询、事件绑定/解绑、隐藏不触发即时
刷新。修复 scheduleNextDelayed 被轮询定时器阻塞导致延迟前台刷
新永不触发的缺陷。共 8 个测试。
This commit is contained in:
2026-08-31 20:58:05 +08:00
parent 93943bb51a
commit d33316f093
2 changed files with 190 additions and 1 deletions
@@ -312,7 +312,7 @@ export function useTaskProgressLoop<TDetail>(
}
function scheduleNextDelayed(delayMs: number) {
if (disposed || pollTimer != null) return
if (disposed) return
clearPollTimer()
const run = () => {
pollTimer = null
@@ -0,0 +1,189 @@
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()
})