/* 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('triggerNow() 与在途 schedule 不并发执行 fn(inFlight 守卫)', async () => { const stub = installDocStub(); const timers = captureTimers(); let active = 0; // 当前正在执行的 fn 数(应恒 ≤1) let maxActive = 0; let calls = 0; // fn 是慢异步:用 gate 控制其完成时机,确保 triggerNow 落在它在途时。 let resolveFn = null; const fn = () => { active++; maxActive = Math.max(maxActive, active); calls++; return new Promise((resolve) => { resolveFn = resolve; }); }; const p = createPoller(fn, { baseMs: 5000 }); const startP = p.start(); // 触发首次 schedule(fn 在途,停在 await) // 让 start 的 schedule 进入 fn()——通过一次微任务边界。 await Promise.resolve(); await Promise.resolve(); // 此时 fn 在途(calls=1,active=1)。triggerNow 应受 inFlight 守卫直接 return, // 不再起一个新的 schedule 并发跑 fn。 const triggerP = p.triggerNow(); await Promise.resolve(); await Promise.resolve(); // 完成 fn(在途的 schedule 才会推进)。 resolveFn(true); await Promise.all([startP, triggerP]); active = 0; // fn 已完成 assert.equal(calls, 1, 'triggerNow 不得并发触发第二次 fn(inFlight 守卫)'); assert.equal(maxActive, 1, 'fn 不得并发执行(maxActive 应为 1)'); 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(); });