task-40: 页面隐藏降频与可见恢复测试
隐藏 60s/可见 10s 间隔自适应、切回可见立即刷新、开关关闭不刷 新、延迟刷新生效、隐藏仍轮询、事件绑定/解绑、隐藏不触发即时 刷新。修复 scheduleNextDelayed 被轮询定时器阻塞导致延迟前台刷 新永不触发的缺陷。共 8 个测试。
This commit is contained in:
@@ -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()
|
||||
})
|
||||
Reference in New Issue
Block a user