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((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 void>>() ;(globalThis as Record).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).document resetTaskPollingConfig() } interface TimerSinkEntry { fn: () => void ms: number } let scopeCounter = 0 interface VisibilityHarness { loop: ReturnType> doc: FakeDoc sink: TimerSinkEntry[] /** 被 clearTimeout 取消过的定时器 id(用于断言隐藏时确实停了表) */ cleared: unknown[] fetchCount: () => number } async function makeVisibilityLoop( visibility: string, config?: { enabled?: boolean; delay?: number }, ): Promise { if (config) { const partial: Parameters[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 cleared: unknown[] = [] const storage = new Map() let fetchCount = 0 ;(globalThis as Record).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) => { cleared.push(id) 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, cleared, 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() }) test('test_hidden_stops_polling', async () => { const h = await makeVisibilityLoop('visible') await tick(10) const scheduledBefore = h.sink.length const before = h.fetchCount() h.doc.setVisibility('hidden') h.doc.dispatch('visibilitychange') await tick(10) assert.equal(h.fetchCount(), before, '隐藏后不再发起请求') assert.equal(h.sink.length, scheduledBefore, '隐藏后不再排定新的轮询定时器') assert.ok(h.cleared.length > 0, '隐藏时清掉了已排定的轮询定时器') h.loop.dispose() cleanupDocument() }) test('test_visible_restarts_polling', async () => { const h = await makeVisibilityLoop('visible') await tick(10) h.doc.setVisibility('hidden') h.doc.dispatch('visibilitychange') await tick(10) const before = h.fetchCount() h.doc.setVisibility('visible') h.doc.dispatch('visibilitychange') await tick(10) assert.equal(h.fetchCount(), before + 1, '恢复可见立即补拉一轮') assert.equal(h.sink.at(-1)?.ms, TASK_POLL_VISIBLE_INTERVAL_MS, '恢复可见后按可见间隔重启轮询') assert.deepEqual(h.loop.taskIds.value, [1], '停表期间任务集合不变') h.loop.dispose() cleanupDocument() })