/* DCTS 轮询器单测(node:test,零依赖) * * utils/polling.js 的 createPoller:指数退避、failCount 重置/增长、stop 清理。 * 通过 stubbing setTimeout / clearTimeout 捕获调度延迟,不真等 5s+。 * * 时序说明:start() 触发 schedule() 时 fn 是同步调用的(calls 立即生效), * 后续的 failCount 更新与 setTimeout 排期发生在微任务里——`await p.start()` * 之后测试续体排在其后,故断言时调度已完成(确定性)。 */ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createPoller } from '../src/utils/polling.js'; // 最小 document stub:node 环境无 DOM,轮询器只在 visibilitychange 处触碰 document。 function installDocStub() { const doc = { hidden: false, addEventListener() {}, removeEventListener() {} }; const prev = globalThis.document; globalThis.document = doc; return { doc, restore() { globalThis.document = prev; } }; } // 捕获 setTimeout 调用。fire(n) 手动执行第 n 次已排定的回调(await 其完整完成)。 function captureTimers() { const realSet = globalThis.setTimeout; const realClear = globalThis.clearTimeout; const scheduled = []; globalThis.setTimeout = (fn, ms) => { const id = { fn, ms }; scheduled.push(id); return id; }; globalThis.clearTimeout = (id) => { const i = scheduled.indexOf(id); if (i >= 0) scheduled.splice(i, 1); }; return { delays: () => scheduled.map(t => t.ms), fire: async (n) => { await scheduled[n].fn(); }, restore() { globalThis.setTimeout = realSet; globalThis.clearTimeout = realClear; }, }; } test('start() 立即执行一次 fn', async () => { const stub = installDocStub(); const timers = captureTimers(); let calls = 0; const p = createPoller(() => { calls++; return true; }, { baseMs: 5000 }); await p.start(); assert.equal(calls, 1); // 立即跑了一次(fn 同步调用) assert.equal(p.isRunning(), true); // 成功后下一轮间隔为 baseMs assert.deepEqual(timers.delays(), [5000]); p.stop(); timers.restore(); stub.restore(); }); test('fn 返回 false 触发指数退避,成功后重置', async () => { const stub = installDocStub(); const timers = captureTimers(); let ok = false; const p = createPoller(() => ok, { baseMs: 5000, maxMs: 60000 }); await p.start(); // 第一次失败 → failCount=1 → 5k*2^1 assert.deepEqual(timers.delays(), [10000]); await timers.fire(0); // 第二轮仍失败 → failCount=2 assert.deepEqual(timers.delays().slice(-1), [20000]); ok = true; await timers.fire(0); // 第三轮成功 → failCount 重置 assert.deepEqual(timers.delays().slice(-1), [5000]); p.stop(); timers.restore(); stub.restore(); }); test('fn 抛错同样计为失败', async () => { const stub = installDocStub(); const timers = captureTimers(); const p = createPoller(() => { throw new Error('boom'); }, { baseMs: 5000 }); await p.start(); assert.deepEqual(timers.delays().slice(-1), [10000]); p.stop(); timers.restore(); stub.restore(); }); test('stop() 清除挂起定时器且不再调度', async () => { const stub = installDocStub(); const timers = captureTimers(); let calls = 0; const p = createPoller(() => { calls++; return true; }, { baseMs: 5000 }); await p.start(); // calls=1,已排下一轮 assert.equal(timers.delays().length, 1); p.stop(); assert.equal(timers.delays().length, 0); // 挂起定时器已被清除 assert.equal(calls, 1); // fn 未再被调用 assert.equal(p.isRunning(), false); timers.restore(); stub.restore(); }); test('triggerNow() 立即触发一次并续排', async () => { const stub = installDocStub(); const timers = captureTimers(); let calls = 0; const p = createPoller(() => { calls++; return true; }, { baseMs: 5000 }); await p.start(); // calls=1 await p.triggerNow(); // calls=2 assert.equal(calls, 2); assert.equal(p.isRunning(), true); p.stop(); timers.restore(); stub.restore(); }); test('退避上限 maxMs 生效(长时间连续失败不无限增长)', async () => { const stub = installDocStub(); const timers = captureTimers(); const p = createPoller(() => false, { baseMs: 5000, maxMs: 60000, maxFails: 4 }); await p.start(); let maxDelay = 0; for (let i = 0; i < 10; i++) { maxDelay = Math.max(maxDelay, timers.delays()[timers.delays().length - 1] || 0); await timers.fire(0); } assert.ok(maxDelay <= 60000, `maxDelay=${maxDelay} 不应超过 60s`); assert.equal(maxDelay, 60000); // 5k*2^4 clamp 到 60s p.stop(); timers.restore(); stub.restore(); });