diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index fa91c9759ecc15..249486e8737ffe 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -62,9 +62,15 @@ function abortIt(signal) { } /** - * @enum {('setTimeout'|'setInterval'|'setImmediate'|'Date')[]} Supported timers + * @enum {('setTimeout'|'setInterval'|'setImmediate'|'Date'|'scheduler.wait')[]} Supported timers */ -const SUPPORTED_APIS = ['setTimeout', 'setInterval', 'setImmediate', 'Date']; +const SUPPORTED_APIS = [ + 'setTimeout', + 'setInterval', + 'setImmediate', + 'Date', + 'scheduler.wait' +]; const TIMERS_DEFAULT_INTERVAL = { __proto__: null, setImmediate: -1, @@ -115,6 +121,8 @@ class MockTimers { #realTimersClearImmediate; #realPromisifiedSetImmediate; + #realPromisifiedSchedulerWait; + #nativeDateDescriptor; #timersInContext = []; @@ -130,6 +138,7 @@ class MockTimers { #clearInterval = FunctionPrototypeBind(this.#clearTimer, this); #clearImmediate = FunctionPrototypeBind(this.#clearTimer, this); + constructor() { emitExperimentalWarning('The MockTimers API'); } @@ -218,6 +227,15 @@ class MockTimers { ); } + #restoreOriginalSchedulerWait() { + + ObjectDefineProperty( + nodeTimersPromises.scheduler, + 'wait', + this.#realPromisifiedSchedulerWait, + ); + } + #storeOriginalSetImmediate() { this.#realSetImmediate = ObjectGetOwnPropertyDescriptor( globalThis, @@ -287,6 +305,13 @@ class MockTimers { ); } + #storeOriginalSchedulerWait() { + this.#realPromisifiedSchedulerWait = ObjectGetOwnPropertyDescriptor( + nodeTimersPromises.scheduler, + 'wait', + ); + } + #createTimer(isInterval, callback, delay, ...args) { const timerId = this.#currentTimer++; const opts = { @@ -472,6 +497,13 @@ class MockTimers { ); } + #schedulerWait(delay, options) { + // Calling timersPromises.scheduler.wait(delay, options) + // is equivalent to calling + // timersPromises.setTimeout(delay, undefined, options). + return this.#setTimeoutPromisified(delay, undefined, options) + } + #promisifyTimer({ timerFn, clearFn, ms, result, options }) { return new Promise((resolve, reject) => { if (options?.signal) { @@ -613,6 +645,13 @@ class MockTimers { this.#nativeDateDescriptor = ObjectGetOwnPropertyDescriptor(globalThis, 'Date'); globalThis.Date = this.#createDate(); }, + 'scheduler.wait': () => { + this.#storeOriginalSchedulerWait(); + nodeTimersPromises.scheduler.wait = FunctionPrototypeBind( + this.#schedulerWait, + this + ) + }, }, toReal: { __proto__: null, @@ -628,6 +667,9 @@ class MockTimers { Date: () => { ObjectDefineProperty(globalThis, 'Date', this.#nativeDateDescriptor); }, + 'scheduler.wait': () => { + this.#restoreOriginalSchedulerWait() + } }, }; diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index e2a86a5263636a..5eac91fb9ef68f 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -594,6 +594,185 @@ describe('Mock Timers Test Suite', () => { }); }); + describe('scheduler.wait Suite', () => { + it('should advance in time and trigger timers when calling the .tick function multiple times', async (t) => { + t.mock.timers.enable({ apis: ['scheduler.wait'] }); + const p = nodeTimersPromises.scheduler.wait(2000); + + t.mock.timers.tick(1000); + t.mock.timers.tick(500); + t.mock.timers.tick(500); + t.mock.timers.tick(500); + + p.then(common.mustCall((result) => { + assert.strictEqual(result, undefined); + })); + }); + + // it('should work with the same params as the original timers/promises/scheduler/wait', async (t) => { + // t.mock.timers.enable({ apis: ['scheduler.wait'] }); + // const expectedResult = 'result'; + // const controller = new AbortController(); + // const p = nodeTimersPromises.scheduler.wait(2000, expectedResult, { + // ref: true, + // signal: controller.signal, + // }); + + // t.mock.timers.tick(1000); + // t.mock.timers.tick(500); + // t.mock.timers.tick(500); + // t.mock.timers.tick(500); + + // const result = await p; + // assert.strictEqual(result, expectedResult); + // }); + + // it('should abort operation if timers/promises/scheduler.wait received an aborted signal', async (t) => { + // t.mock.timers.enable({ apis: ['scheduler.wait'] }); + // const expectedResult = 'result'; + // const controller = new AbortController(); + // const p = nodeTimersPromises.scheduler.wait(2000, { + // ref: true, + // signal: controller.signal, + // }); + + // t.mock.timers.tick(1000); + // controller.abort(); + // t.mock.timers.tick(500); + // t.mock.timers.tick(500); + // t.mock.timers.tick(500); + // await assert.rejects(() => p, { + // name: 'AbortError', + // }); + // }); + + + // it('should abort operation even if the .tick was not called', async (t) => { + // t.mock.timers.enable({ apis: ['scheduler.wait'] }); + // const controller = new AbortController(); + // const p = nodeTimersPromises.scheduler.wait(2000, { + // ref: true, + // signal: controller.signal, + // }); + + // controller.abort(); + + // await assert.rejects(() => p, { + // name: 'AbortError', + // }); + // }); + + // it('should abort operation when .abort is called before calling setInterval', async (t) => { + // t.mock.timers.enable({ apis: ['setTimeout'] }); + // const expectedResult = 'result'; + // const controller = new AbortController(); + // controller.abort(); + // const p = nodeTimersPromises.setTimeout(2000, expectedResult, { + // ref: true, + // signal: controller.signal, + // }); + + // await assert.rejects(() => p, { + // name: 'AbortError', + // }); + // }); + + // it('should reject given an an invalid signal instance', async (t) => { + // t.mock.timers.enable({ apis: ['scheduler.wait'] }); + // const p = nodeTimersPromises.scheduler.wait(2000, { + // ref: true, + // signal: {}, + // }); + + // await assert.rejects(() => p, { + // name: 'TypeError', + // code: 'ERR_INVALID_ARG_TYPE', + // }); + // }); + + // // Test for https://github.com/nodejs/node/issues/50365 + // it('should not affect other timers when aborting', async (t) => { + // const f1 = t.mock.fn(); + // const f2 = t.mock.fn(); + // t.mock.timers.enable({ apis: ['scheduler.wait'] }); + // const ac = new AbortController(); + + // // id 1 & pos 1 in priority queue + // nodeTimersPromises.scheduler.wait(100, { signal: ac.signal }).then(f1, f1); + // // id 2 & pos 1 in priority queue (id 1 is moved to pos 2) + // nodeTimersPromises.scheduler.wait(50).then(f2, f2); + + // ac.abort(); // BUG: will remove timer at pos 1 not timer with id 1! + + // t.mock.timers.runAll(); + // await nodeTimersPromises.setImmediate(); // let promises settle + + // // First scheduler.wait is aborted + // assert.strictEqual(f1.mock.callCount(), 1); + // assert.strictEqual(f1.mock.calls[0].arguments[0].code, 'ABORT_ERR'); + + // // Second scheduler.wait should resolve, but never settles, because it was eronously removed by ac.abort() + // assert.strictEqual(f2.mock.callCount(), 1); + // }); + + // // Test for https://github.com/nodejs/node/issues/50365 + // it('should not affect other timers when aborted after triggering', async (t) => { + // const f1 = t.mock.fn(); + // const f2 = t.mock.fn(); + // t.mock.timers.enable({ apis: ['setTimeout'] }); + // const ac = new AbortController(); + + // // id 1 & pos 1 in priority queue + // nodeTimersPromises.setTimeout(50, true, { signal: ac.signal }).then(f1, f1); + // // id 2 & pos 2 in priority queue + // nodeTimersPromises.setTimeout(100).then(f2, f2); + + // // First setTimeout resolves + // t.mock.timers.tick(50); + // await nodeTimersPromises.setImmediate(); // let promises settle + // assert.strictEqual(f1.mock.callCount(), 1); + // assert.strictEqual(f1.mock.calls[0].arguments.length, 1); + // assert.strictEqual(f1.mock.calls[0].arguments[0], true); + + // // Now timer with id 2 will be at pos 1 in priority queue + // ac.abort(); // BUG: will remove timer at pos 1 not timer with id 1! + + // // Second setTimeout should resolve, but never settles, because it was eronously removed by ac.abort() + // t.mock.timers.runAll(); + // await nodeTimersPromises.setImmediate(); // let promises settle + // assert.strictEqual(f2.mock.callCount(), 1); + // }); + + // it('should not affect other timers when clearing timeout inside own callback', (t) => { + // t.mock.timers.enable({ apis: ['setTimeout'] }); + // const f = t.mock.fn(); + + // const timer = nodeTimers.setTimeout(() => { + // f(); + // // Clearing the already-expired timeout should do nothing + // nodeTimers.clearTimeout(timer); + // }, 50); + // nodeTimers.setTimeout(f, 50); + // nodeTimers.setTimeout(f, 50); + + // t.mock.timers.runAll(); + // assert.strictEqual(f.mock.callCount(), 3); + // }); + + // it('should allow clearing timeout inside own callback', (t) => { + // t.mock.timers.enable({ apis: ['setTimeout'] }); + // const f = t.mock.fn(); + + // const timer = nodeTimers.setTimeout(() => { + // f(); + // nodeTimers.clearTimeout(timer); + // }, 50); + + // t.mock.timers.runAll(); + // assert.strictEqual(f.mock.callCount(), 1); + // }); + }); + describe('setInterval Suite', () => { it('should tick three times using fake setInterval', async (t) => { t.mock.timers.enable({ apis: ['setInterval'] });