diff --git a/doc/api/test.md b/doc/api/test.md index 5b694a4dc74668..e1c255532d2bc0 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -1579,9 +1579,10 @@ added: Enables timer mocking for the specified timers. * `timers` {Array} An optional array containing the timers to mock. - The currently supported timer values are `'setInterval'` and `'setTimeout'`. - If no array is provided, all timers (`'setInterval'`, `'clearInterval'`, `'setTimeout'`, - and `'clearTimeout'`) will be mocked by default. + The currently supported timer values are `'setInterval'`, `'setTimeout'`, + and `'setImmediate'`. If no value is provided, all timers (`'setInterval'`, + `'clearInterval'`, `'setTimeout'`, `'clearTimeout'`, `'setImmediate'`, + and `'clearImmediate'`) will be mocked by default. **Note:** When you enable mocking for a specific timer, its associated clear function will also be implicitly mocked. diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 311c30684d0ce6..a718bf34d5ba50 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -48,13 +48,19 @@ function abortIt(signal) { return new AbortError(undefined, { __proto__: null, cause: signal.reason }); } -const SUPPORTED_TIMERS = ['setTimeout', 'setInterval']; +const SUPPORTED_TIMERS = ['setTimeout', 'setInterval', 'setImmediate']; +const TIMERS_DEFAULT_INTERVAL = { + __proto__: null, + setImmediate: -1, +}; class MockTimers { #realSetTimeout; #realClearTimeout; #realSetInterval; #realClearInterval; + #realSetImmediate; + #realClearImmediate; #realPromisifiedSetTimeout; #realPromisifiedSetInterval; @@ -63,6 +69,9 @@ class MockTimers { #realTimersClearTimeout; #realTimersSetInterval; #realTimersClearInterval; + #realTimersSetImmediate; + #realTimersClearImmediate; + #realPromisifiedSetImmediate; #timersInContext = []; #isEnabled = false; @@ -76,6 +85,16 @@ class MockTimers { #setInterval = FunctionPrototypeBind(this.#createTimer, this, true); #clearInterval = FunctionPrototypeBind(this.#clearTimer, this); + #setImmediate = (callback, ...args) => { + return this.#createTimer( + false, + callback, + TIMERS_DEFAULT_INTERVAL.setImmediate, + ...args, + ); + }; + + #clearImmediate = FunctionPrototypeBind(this.#clearTimer, this); constructor() { emitExperimentalWarning('The MockTimers API'); } @@ -158,7 +177,7 @@ class MockTimers { yield* iterator; } - #setTimeoutPromisified(ms, result, options) { + #promisifyTimer({ timerFn, clearFn, ms, result, options }) { return new Promise((resolve, reject) => { if (options?.signal) { try { @@ -173,12 +192,12 @@ class MockTimers { } const onabort = () => { - this.#clearTimeout(id); + clearFn(id); return reject(abortIt(options.signal)); }; - const id = this.#setTimeout(() => { - return resolve(result || id); + const id = timerFn(() => { + return resolve(result); }, ms); if (options?.signal) { @@ -192,6 +211,28 @@ class MockTimers { }); } + #setImmediatePromisified(result, options) { + return this.#promisifyTimer({ + __proto__: null, + timerFn: FunctionPrototypeBind(this.#setImmediate, this), + clearFn: FunctionPrototypeBind(this.#clearImmediate, this), + ms: TIMERS_DEFAULT_INTERVAL.setImmediate, + result, + options, + }); + } + + #setTimeoutPromisified(ms, result, options) { + return this.#promisifyTimer({ + __proto__: null, + timerFn: FunctionPrototypeBind(this.#setTimeout, this), + clearFn: FunctionPrototypeBind(this.#clearTimeout, this), + ms, + result, + options, + }); + } + #toggleEnableTimers(activate) { const options = { __proto__: null, @@ -233,6 +274,23 @@ class MockTimers { this, ); }, + setImmediate: () => { + this.#realSetImmediate = globalThis.setImmediate; + this.#realClearImmediate = globalThis.clearImmediate; + this.#realTimersSetImmediate = nodeTimers.setImmediate; + this.#realTimersClearImmediate = nodeTimers.clearImmediate; + + globalThis.setImmediate = this.#setImmediate; + globalThis.clearImmediate = this.#clearImmediate; + + nodeTimers.setImmediate = this.#setImmediate; + nodeTimers.clearImmediate = this.#clearImmediate; + + nodeTimersPromises.setImmediate = FunctionPrototypeBind( + this.#setImmediatePromisified, + this, + ); + }, }, toReal: { __proto__: null, @@ -254,6 +312,15 @@ class MockTimers { nodeTimersPromises.setInterval = this.#realPromisifiedSetInterval; }, + setImmediate: () => { + globalThis.setImmediate = this.#realSetImmediate; + globalThis.clearImmediate = this.#realClearImmediate; + + nodeTimers.setImmediate = this.#realTimersSetImmediate; + nodeTimers.clearImmediate = this.#realTimersClearImmediate; + + nodeTimersPromises.setImmediate = this.#realPromisifiedSetImmediate; + }, }, }; diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index 883dd9d08ad752..7b37c6ae4b8d74 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -232,6 +232,70 @@ describe('Mock Timers Test Suite', () => { }); }); + describe('setImmediate Suite', () => { + it('should keep setImmediate working if timers are disabled', (t, done) => { + const now = Date.now(); + const timeout = 2; + const expected = () => now - timeout; + global.setImmediate(common.mustCall(() => { + assert.strictEqual(now - timeout, expected()); + done(); + })); + }); + + it('should work with the same params as the original setImmediate', (t) => { + t.mock.timers.enable(['setImmediate']); + const fn = t.mock.fn(); + const args = ['a', 'b', 'c']; + global.setImmediate(fn, ...args); + t.mock.timers.tick(9999); + + assert.strictEqual(fn.mock.callCount(), 1); + assert.deepStrictEqual(fn.mock.calls[0].arguments, args); + }); + + it('should not advance in time if clearImmediate was invoked', (t) => { + t.mock.timers.enable(['setImmediate']); + + const id = global.setImmediate(common.mustNotCall()); + global.clearImmediate(id); + t.mock.timers.tick(200); + }); + + it('should advance in time and trigger timers when calling the .tick function', (t) => { + t.mock.timers.enable(['setImmediate']); + global.setImmediate(common.mustCall(1)); + t.mock.timers.tick(0); + }); + + it('should execute in order if setImmediate is called multiple times', (t) => { + t.mock.timers.enable(['setImmediate']); + const order = []; + const fn1 = t.mock.fn(common.mustCall(() => order.push('f1'), 1)); + const fn2 = t.mock.fn(common.mustCall(() => order.push('f2'), 1)); + + global.setImmediate(fn1); + global.setImmediate(fn2); + + t.mock.timers.tick(0); + + assert.deepStrictEqual(order, ['f1', 'f2']); + }); + + it('should execute setImmediate first if setTimeout was also called', (t) => { + t.mock.timers.enable(['setImmediate', 'setTimeout']); + const order = []; + const fn1 = t.mock.fn(common.mustCall(() => order.push('f1'), 1)); + const fn2 = t.mock.fn(common.mustCall(() => order.push('f2'), 1)); + + global.setTimeout(fn2, 0); + global.setImmediate(fn1); + + t.mock.timers.tick(100); + + assert.deepStrictEqual(order, ['f1', 'f2']); + }); + }); }); describe('timers Suite', () => { @@ -331,6 +395,71 @@ describe('Mock Timers Test Suite', () => { assert.strictEqual(fn.mock.callCount(), 0); }); }); + + describe('setImmediate Suite', () => { + it('should keep setImmediate working if timers are disabled', (t, done) => { + const now = Date.now(); + const timeout = 2; + const expected = () => now - timeout; + nodeTimers.setImmediate(common.mustCall(() => { + assert.strictEqual(now - timeout, expected()); + done(); + }, 1)); + }); + + it('should work with the same params as the original setImmediate', (t) => { + t.mock.timers.enable(['setImmediate']); + const fn = t.mock.fn(); + const args = ['a', 'b', 'c']; + nodeTimers.setImmediate(fn, ...args); + t.mock.timers.tick(9999); + + assert.strictEqual(fn.mock.callCount(), 1); + assert.deepStrictEqual(fn.mock.calls[0].arguments, args); + }); + + it('should not advance in time if clearImmediate was invoked', (t) => { + t.mock.timers.enable(['setImmediate']); + + const id = nodeTimers.setImmediate(common.mustNotCall()); + nodeTimers.clearImmediate(id); + t.mock.timers.tick(200); + }); + + it('should advance in time and trigger timers when calling the .tick function', (t) => { + t.mock.timers.enable(['setImmediate']); + nodeTimers.setImmediate(common.mustCall(1)); + t.mock.timers.tick(0); + }); + + it('should execute in order if setImmediate is called multiple times', (t) => { + t.mock.timers.enable(['setImmediate']); + const order = []; + const fn1 = t.mock.fn(common.mustCall(() => order.push('f1'), 1)); + const fn2 = t.mock.fn(common.mustCall(() => order.push('f2'), 1)); + + nodeTimers.setImmediate(fn1); + nodeTimers.setImmediate(fn2); + + t.mock.timers.tick(0); + + assert.deepStrictEqual(order, ['f1', 'f2']); + }); + + it('should execute setImmediate first if setTimeout was also called', (t) => { + t.mock.timers.enable(['setImmediate', 'setTimeout']); + const order = []; + const fn1 = t.mock.fn(common.mustCall(() => order.push('f1'), 1)); + const fn2 = t.mock.fn(common.mustCall(() => order.push('f2'), 1)); + + nodeTimers.setTimeout(fn2, 0); + nodeTimers.setImmediate(fn1); + + t.mock.timers.tick(100); + + assert.deepStrictEqual(order, ['f1', 'f2']); + }); + }); }); describe('timers/promises', () => { @@ -346,7 +475,7 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.tick(500); p.then(common.mustCall((result) => { - assert.ok(result); + assert.strictEqual(result, undefined); })); }); @@ -404,7 +533,7 @@ describe('Mock Timers Test Suite', () => { }); - it('should abort operation when .abort is called before calling setInterval', async (t) => { + it('should abort operation when .abort is called before calling setTimeout', async (t) => { t.mock.timers.enable(['setTimeout']); const expectedResult = 'result'; const controller = new AbortController(); @@ -581,5 +710,110 @@ describe('Mock Timers Test Suite', () => { }); + describe('setImmediate Suite', () => { + it('should advance in time and trigger timers when calling the .tick function multiple times', (t, done) => { + t.mock.timers.enable(['setImmediate']); + const p = nodeTimersPromises.setImmediate(); + + t.mock.timers.tick(5555); + + p.then(common.mustCall((result) => { + assert.strictEqual(result, undefined); + done(); + }, 1)); + }); + + it('should work with the same params as the original timers/promises/setImmediate', async (t) => { + t.mock.timers.enable(['setImmediate']); + const expectedResult = 'result'; + const controller = new AbortController(); + const p = nodeTimersPromises.setImmediate(expectedResult, { + ref: true, + signal: controller.signal + }); + + t.mock.timers.tick(500); + + const result = await p; + assert.strictEqual(result, expectedResult); + }); + + it('should abort operation if timers/promises/setImmediate received an aborted signal', async (t) => { + t.mock.timers.enable(['setImmediate']); + const expectedResult = 'result'; + const controller = new AbortController(); + const p = nodeTimersPromises.setImmediate(expectedResult, { + ref: true, + signal: controller.signal + }); + + controller.abort(); + t.mock.timers.tick(0); + + await assert.rejects(() => p, { + name: 'AbortError', + }); + + }); + it('should abort operation even if the .tick wasn\'t called', async (t) => { + t.mock.timers.enable(['setImmediate']); + const expectedResult = 'result'; + const controller = new AbortController(); + const p = nodeTimersPromises.setImmediate(expectedResult, { + ref: true, + signal: controller.signal + }); + + controller.abort(); + + await assert.rejects(() => p, { + name: 'AbortError', + }); + }); + + it('should abort operation when .abort is called before calling setImmediate', async (t) => { + t.mock.timers.enable(['setImmediate']); + const expectedResult = 'result'; + const controller = new AbortController(); + controller.abort(); + const p = nodeTimersPromises.setImmediate(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(['setImmediate']); + const expectedResult = 'result'; + const p = nodeTimersPromises.setImmediate(expectedResult, { + ref: true, + signal: {} + }); + + await assert.rejects(() => p, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE' + }); + + }); + + it('should execute in order if setImmediate is called multiple times', async (t) => { + t.mock.timers.enable(['setImmediate']); + + const p1 = nodeTimersPromises.setImmediate('fn1'); + const p2 = nodeTimersPromises.setImmediate('fn2'); + + t.mock.timers.tick(0); + + const results = await Promise.race([p1, p2]); + + assert.strictEqual(results, 'fn1'); + }); + }); }); });