test_runner: add Date to the supported mock APIs

signed-off-by: Lucas Santos <lhs.santoss@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/48638
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Erick Wendel <erick.workspace@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
Lucas Santos
2023-10-23 13:23:12 +02:00
committed by GitHub
parent 25576b5118
commit 45a0b153b3
4 changed files with 1179 additions and 771 deletions

View File

@@ -505,7 +505,7 @@ This allows developers to write more reliable and
predictable tests for time-dependent functionality.
The example below shows how to mock `setTimeout`.
Using `.enable(['setTimeout']);`
Using `.enable({ apis: ['setTimeout'] });`
it will mock the `setTimeout` functions in the [node:timers](./timers.md) and
[node:timers/promises](./timers.md#timers-promises-api) modules,
as well as from the Node.js global context.
@@ -522,7 +522,7 @@ test('mocks setTimeout to be executed synchronously without having to actually w
const fn = mock.fn();
// Optionally choose what to mock
mock.timers.enable(['setTimeout']);
mock.timers.enable({ apis: ['setTimeout'] });
setTimeout(fn, 9999);
assert.strictEqual(fn.mock.callCount(), 0);
@@ -546,7 +546,7 @@ test('mocks setTimeout to be executed synchronously without having to actually w
const fn = mock.fn();
// Optionally choose what to mock
mock.timers.enable(['setTimeout']);
mock.timers.enable({ apis: ['setTimeout'] });
setTimeout(fn, 9999);
assert.strictEqual(fn.mock.callCount(), 0);
@@ -575,7 +575,7 @@ test('mocks setTimeout to be executed synchronously without having to actually w
const fn = context.mock.fn();
// Optionally choose what to mock
context.mock.timers.enable(['setTimeout']);
context.mock.timers.enable({ apis: ['setTimeout'] });
setTimeout(fn, 9999);
assert.strictEqual(fn.mock.callCount(), 0);
@@ -593,7 +593,7 @@ test('mocks setTimeout to be executed synchronously without having to actually w
const fn = context.mock.fn();
// Optionally choose what to mock
context.mock.timers.enable(['setTimeout']);
context.mock.timers.enable({ apis: ['setTimeout'] });
setTimeout(fn, 9999);
assert.strictEqual(fn.mock.callCount(), 0);
@@ -603,6 +603,220 @@ test('mocks setTimeout to be executed synchronously without having to actually w
});
```
### Dates
The mock timers API also allows the mocking of the `Date` object. This is a
useful feature for testing time-dependent functionality, or to simulate
internal calendar functions such as `Date.now()`.
The dates implementation is also part of the [`MockTimers`][] class. Refer to it
for a full list of methods and features.
**Note:** Dates and timers are dependent when mocked together. This means that
if you have both the `Date` and `setTimeout` mocked, advancing the time will
also advance the mocked date as they simulate a single internal clock.
The example below show how to mock the `Date` object and obtain the current
`Date.now()` value.
```mjs
import assert from 'node:assert';
import { test } from 'node:test';
test('mocks the Date object', (context) => {
// Optionally choose what to mock
context.mock.timers.enable({ apis: ['Date'] });
// If not specified, the initial date will be based on 0 in the UNIX epoch
assert.strictEqual(Date.now(), 0);
// Advance in time will also advance the date
context.mock.timers.tick(9999);
assert.strictEqual(Date.now(), 9999);
});
```
```cjs
const assert = require('node:assert');
const { test } = require('node:test');
test('mocks the Date object', (context) => {
// Optionally choose what to mock
context.mock.timers.enable({ apis: ['Date'] });
// If not specified, the initial date will be based on 0 in the UNIX epoch
assert.strictEqual(Date.now(), 0);
// Advance in time will also advance the date
context.mock.timers.tick(9999);
assert.strictEqual(Date.now(), 9999);
});
```
If there is no initial epoch set, the initial date will be based on 0 in the
Unix epoch. This is January 1st, 1970, 00:00:00 UTC. You can set an initial date
by passing a `now` property to the `.enable()` method. This value will be used
as the initial date for the mocked `Date` object. It can either be a positive
integer, or another Date object.
```mjs
import assert from 'node:assert';
import { test } from 'node:test';
test('mocks the Date object with initial time', (context) => {
// Optionally choose what to mock
context.mock.timers.enable({ apis: ['Date'], now: 100 });
assert.strictEqual(Date.now(), 100);
// Advance in time will also advance the date
context.mock.timers.tick(200);
assert.strictEqual(Date.now(), 300);
});
```
```cjs
const assert = require('node:assert');
const { test } = require('node:test');
test('mocks the Date object with initial time', (context) => {
// Optionally choose what to mock
context.mock.timers.enable({ apis: ['Date'], now: 100 });
assert.strictEqual(Date.now(), 100);
// Advance in time will also advance the date
context.mock.timers.tick(200);
assert.strictEqual(Date.now(), 300);
});
```
You can use the `.setTime()` method to manually move the mocked date to another
time. This method only accepts a positive integer.
**Note:** This method will execute any mocked timers that are in the past
from the new time.
In the below example we are setting a new time for the mocked date.
```mjs
import assert from 'node:assert';
import { test } from 'node:test';
test('sets the time of a date object', (context) => {
// Optionally choose what to mock
context.mock.timers.enable({ apis: ['Date'], now: 100 });
assert.strictEqual(Date.now(), 100);
// Advance in time will also advance the date
context.mock.timers.setTime(1000);
context.mock.timers.tick(200);
assert.strictEqual(Date.now(), 1200);
});
```
```cjs
const assert = require('node:assert');
const { test } = require('node:test');
test('sets the time of a date object', (context) => {
// Optionally choose what to mock
context.mock.timers.enable({ apis: ['Date'], now: 100 });
assert.strictEqual(Date.now(), 100);
// Advance in time will also advance the date
context.mock.timers.setTime(1000);
context.mock.timers.tick(200);
assert.strictEqual(Date.now(), 1200);
});
```
If you have any timer that's set to run in the past, it will be executed as if
the `.tick()` method has been called. This is useful if you want to test
time-dependent functionality that's already in the past.
```mjs
import assert from 'node:assert';
import { test } from 'node:test';
test('runs timers as setTime passes ticks', (context) => {
// Optionally choose what to mock
context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
const fn = context.mock.fn();
setTimeout(fn, 1000);
context.mock.timers.setTime(800);
// Timer is not executed as the time is not yet reached
assert.strictEqual(fn.mock.callCount(), 0);
assert.strictEqual(Date.now(), 800);
context.mock.timers.setTime(1200);
// Timer is executed as the time is now reached
assert.strictEqual(fn.mock.callCount(), 1);
assert.strictEqual(Date.now(), 1200);
});
```
```cjs
const assert = require('node:assert');
const { test } = require('node:test');
test('runs timers as setTime passes ticks', (context) => {
// Optionally choose what to mock
context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
const fn = context.mock.fn();
setTimeout(fn, 1000);
context.mock.timers.setTime(800);
// Timer is not executed as the time is not yet reached
assert.strictEqual(fn.mock.callCount(), 0);
assert.strictEqual(Date.now(), 800);
context.mock.timers.setTime(1200);
// Timer is executed as the time is now reached
assert.strictEqual(fn.mock.callCount(), 1);
assert.strictEqual(Date.now(), 1200);
});
```
Using `.runAll()` will execute all timers that are currently in the queue. This
will also advance the mocked date to the time of the last timer that was
executed as if the time has passed.
```mjs
import assert from 'node:assert';
import { test } from 'node:test';
test('runs timers as setTime passes ticks', (context) => {
// Optionally choose what to mock
context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
const fn = context.mock.fn();
setTimeout(fn, 1000);
setTimeout(fn, 2000);
setTimeout(fn, 3000);
context.mock.timers.runAll();
// All timers are executed as the time is now reached
assert.strictEqual(fn.mock.callCount(), 3);
assert.strictEqual(Date.now(), 3000);
});
```
```cjs
const assert = require('node:assert');
const { test } = require('node:test');
test('runs timers as setTime passes ticks', (context) => {
// Optionally choose what to mock
context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
const fn = context.mock.fn();
setTimeout(fn, 1000);
setTimeout(fn, 2000);
setTimeout(fn, 3000);
context.mock.timers.runAll();
// All timers are executed as the time is now reached
assert.strictEqual(fn.mock.callCount(), 3);
assert.strictEqual(Date.now(), 3000);
});
```
## Test reporters
<!-- YAML
@@ -1576,37 +1790,52 @@ Mocking timers is a technique commonly used in software testing to simulate and
control the behavior of timers, such as `setInterval` and `setTimeout`,
without actually waiting for the specified time intervals.
MockTimers is also able to mock the `Date` object.
The [`MockTracker`][] provides a top-level `timers` export
which is a `MockTimers` instance.
### `timers.enable([timers])`
### `timers.enable([enableOptions])`
<!-- YAML
added:
- v20.4.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/48638
description: Updated parameters to be an option object with available APIs
and the default initial epoch.
-->
Enables timer mocking for the specified timers.
* `timers` {Array} An optional array containing the timers to mock.
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.
* `enableOptions` {Object} Optional configuration options for enabling timer
mocking. The following properties are supported:
* `apis` {Array} An optional array containing the timers to mock.
The currently supported timer values are `'setInterval'`, `'setTimeout'`, `'setImmediate'`,
and `'Date'`. **Default:** `['setInterval', 'setTimeout', 'setImmediate', 'Date']`.
If no array is provided, all time related APIs (`'setInterval'`, `'clearInterval'`,
`'setTimeout'`, `'clearTimeout'`, and `'Date'`) will be mocked by default.
* `now` {number | Date} An optional number or Date object representing the
initial time (in milliseconds) to use as the value
for `Date.now()`. **Default:** `0`.
**Note:** When you enable mocking for a specific timer, its associated
clear function will also be implicitly mocked.
Example usage:
**Note:** Mocking `Date` will affect the behavior of the mocked timers
as they use the same internal clock.
Example usage without setting initial time:
```mjs
import { mock } from 'node:test';
mock.timers.enable(['setInterval']);
mock.timers.enable({ apis: ['setInterval'] });
```
```cjs
const { mock } = require('node:test');
mock.timers.enable(['setInterval']);
mock.timers.enable({ apis: ['setInterval'] });
```
The above example enables mocking for the `setInterval` timer and
@@ -1615,12 +1844,36 @@ and `clearInterval` functions from [node:timers](./timers.md),
[node:timers/promises](./timers.md#timers-promises-api), and
`globalThis` will be mocked.
Example usage with initial time set
```mjs
import { mock } from 'node:test';
mock.timers.enable({ apis: ['Date'], now: 1000 });
```
```cjs
const { mock } = require('node:test');
mock.timers.enable({ apis: ['Date'], now: 1000 });
```
Example usage with initial Date object as time set
```mjs
import { mock } from 'node:test';
mock.timers.enable({ apis: ['Date'], now: new Date() });
```
```cjs
const { mock } = require('node:test');
mock.timers.enable({ apis: ['Date'], now: new Date() });
```
Alternatively, if you call `mock.timers.enable()` without any parameters:
All timers (`'setInterval'`, `'clearInterval'`, `'setTimeout'`, and `'clearTimeout'`)
will be mocked. The `setInterval`, `clearInterval`, `setTimeout`, and `clearTimeout`
functions from `node:timers`, `node:timers/promises`,
and `globalThis` will be mocked.
and `globalThis` will be mocked. As well as the global `Date` object.
### `timers.reset()`
@@ -1677,7 +1930,7 @@ import { test } from 'node:test';
test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
const fn = context.mock.fn();
context.mock.timers.enable(['setTimeout']);
context.mock.timers.enable({ apis: ['setTimeout'] });
setTimeout(fn, 9999);
@@ -1696,7 +1949,7 @@ const { test } = require('node:test');
test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
const fn = context.mock.fn();
context.mock.timers.enable(['setTimeout']);
context.mock.timers.enable({ apis: ['setTimeout'] });
setTimeout(fn, 9999);
assert.strictEqual(fn.mock.callCount(), 0);
@@ -1716,7 +1969,7 @@ import { test } from 'node:test';
test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
const fn = context.mock.fn();
context.mock.timers.enable(['setTimeout']);
context.mock.timers.enable({ apis: ['setTimeout'] });
const nineSecs = 9000;
setTimeout(fn, nineSecs);
@@ -1735,7 +1988,7 @@ const { test } = require('node:test');
test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
const fn = context.mock.fn();
context.mock.timers.enable(['setTimeout']);
context.mock.timers.enable({ apis: ['setTimeout'] });
const nineSecs = 9000;
setTimeout(fn, nineSecs);
@@ -1748,6 +2001,48 @@ test('mocks setTimeout to be executed synchronously without having to actually w
});
```
Advancing time using `.tick` will also advance the time for any `Date` object
created after the mock was enabled (if `Date` was also set to be mocked).
```mjs
import assert from 'node:assert';
import { test } from 'node:test';
test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
const fn = context.mock.fn();
context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
setTimeout(fn, 9999);
assert.strictEqual(fn.mock.callCount(), 0);
assert.strictEqual(Date.now(), 0);
// Advance in time
context.mock.timers.tick(9999);
assert.strictEqual(fn.mock.callCount(), 1);
assert.strictEqual(Date.now(), 9999);
});
```
```cjs
const assert = require('node:assert');
const { test } = require('node:test');
test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
const fn = context.mock.fn();
context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
setTimeout(fn, 9999);
assert.strictEqual(fn.mock.callCount(), 0);
assert.strictEqual(Date.now(), 0);
// Advance in time
context.mock.timers.tick(9999);
assert.strictEqual(fn.mock.callCount(), 1);
assert.strictEqual(Date.now(), 9999);
});
```
#### Using clear functions
As mentioned, all clear functions from timers (`clearTimeout` and `clearInterval`)
@@ -1761,7 +2056,7 @@ test('mocks setTimeout to be executed synchronously without having to actually w
const fn = context.mock.fn();
// Optionally choose what to mock
context.mock.timers.enable(['setTimeout']);
context.mock.timers.enable({ apis: ['setTimeout'] });
const id = setTimeout(fn, 9999);
// Implicity mocked as well
@@ -1781,7 +2076,7 @@ test('mocks setTimeout to be executed synchronously without having to actually w
const fn = context.mock.fn();
// Optionally choose what to mock
context.mock.timers.enable(['setTimeout']);
context.mock.timers.enable({ apis: ['setTimeout'] });
const id = setTimeout(fn, 9999);
// Implicity mocked as well
@@ -1815,7 +2110,7 @@ test('mocks setTimeout to be executed synchronously without having to actually w
const nodeTimerPromiseSpy = context.mock.fn();
// Optionally choose what to mock
context.mock.timers.enable(['setTimeout']);
context.mock.timers.enable({ apis: ['setTimeout'] });
setTimeout(globalTimeoutObjectSpy, 9999);
nodeTimers.setTimeout(nodeTimerSpy, 9999);
@@ -1842,7 +2137,7 @@ test('mocks setTimeout to be executed synchronously without having to actually w
const nodeTimerPromiseSpy = context.mock.fn();
// Optionally choose what to mock
context.mock.timers.enable(['setTimeout']);
context.mock.timers.enable({ apis: ['setTimeout'] });
setTimeout(globalTimeoutObjectSpy, 9999);
nodeTimers.setTimeout(nodeTimerSpy, 9999);
@@ -1865,7 +2160,7 @@ import assert from 'node:assert';
import { test } from 'node:test';
import nodeTimersPromises from 'node:timers/promises';
test('should tick five times testing a real use case', async (context) => {
context.mock.timers.enable(['setInterval']);
context.mock.timers.enable({ apis: ['setInterval'] });
const expectedIterations = 3;
const interval = 1000;
@@ -1897,7 +2192,7 @@ const assert = require('node:assert');
const { test } = require('node:test');
const nodeTimersPromises = require('node:timers/promises');
test('should tick five times testing a real use case', async (context) => {
context.mock.timers.enable(['setInterval']);
context.mock.timers.enable({ apis: ['setInterval'] });
const expectedIterations = 3;
const interval = 1000;
@@ -1931,7 +2226,8 @@ added:
- v20.4.0
-->
Triggers all pending mocked timers immediately.
Triggers all pending mocked timers immediately. If the `Date` object is also
mocked, it will also advance the `Date` object to the furthest timer's time.
The example below triggers all pending timers immediately,
causing them to execute without any delay.
@@ -1941,7 +2237,7 @@ import assert from 'node:assert';
import { test } from 'node:test';
test('runAll functions following the given order', (context) => {
context.mock.timers.enable(['setTimeout']);
context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
const results = [];
setTimeout(() => results.push(1), 9999);
@@ -1953,8 +2249,9 @@ test('runAll functions following the given order', (context) => {
assert.deepStrictEqual(results, []);
context.mock.timers.runAll();
assert.deepStrictEqual(results, [3, 2, 1]);
// The Date object is also advanced to the furthest timer's time
assert.strictEqual(Date.now(), 9999);
});
```
@@ -1963,7 +2260,7 @@ const assert = require('node:assert');
const { test } = require('node:test');
test('runAll functions following the given order', (context) => {
context.mock.timers.enable(['setTimeout']);
context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
const results = [];
setTimeout(() => results.push(1), 9999);
@@ -1975,8 +2272,9 @@ test('runAll functions following the given order', (context) => {
assert.deepStrictEqual(results, []);
context.mock.timers.runAll();
assert.deepStrictEqual(results, [3, 2, 1]);
// The Date object is also advanced to the furthest timer's time
assert.strictEqual(Date.now(), 9999);
});
```
@@ -1985,6 +2283,92 @@ triggering timers in the context of timer mocking.
It does not have any effect on real-time system
clocks or actual timers outside of the mocking environment.
### `timers.setTime(milliseconds)`
<!-- YAML
added:
- REPLACEME
-->
Sets the current Unix timestamp that will be used as reference for any mocked
`Date` objects.
```mjs
import assert from 'node:assert';
import { test } from 'node:test';
test('runAll functions following the given order', (context) => {
const now = Date.now();
const setTime = 1000;
// Date.now is not mocked
assert.deepStrictEqual(Date.now(), now);
context.mock.timers.enable({ apis: ['Date'] });
context.mock.timers.setTime(setTime);
// Date.now is now 1000
assert.strictEqual(Date.now(), setTime);
});
```
```cjs
const assert = require('node:assert');
const { test } = require('node:test');
test('setTime replaces current time', (context) => {
const now = Date.now();
const setTime = 1000;
// Date.now is not mocked
assert.deepStrictEqual(Date.now(), now);
context.mock.timers.enable({ apis: ['Date'] });
context.mock.timers.setTime(setTime);
// Date.now is now 1000
assert.strictEqual(Date.now(), setTime);
});
```
#### Dates and Timers working together
Dates and timer objects are dependent on each other. If you use `setTime()` to
pass the current time to the mocked `Date` object, the set timers with
`setTimeout` and `setInterval` will **not** be affected.
However, the `tick` method **will** advanced the mocked `Date` object.
```mjs
import assert from 'node:assert';
import { test } from 'node:test';
test('runAll functions following the given order', (context) => {
context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
const results = [];
setTimeout(() => results.push(1), 9999);
assert.deepStrictEqual(results, []);
context.mock.timers.setTime(12000);
assert.deepStrictEqual(results, []);
// The date is advanced but the timers don't tick
assert.strictEqual(Date.now(), 12000);
});
```
```cjs
const assert = require('node:assert');
const { test } = require('node:test');
test('runAll functions following the given order', (context) => {
context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
const results = [];
setTimeout(() => results.push(1), 9999);
assert.deepStrictEqual(results, []);
context.mock.timers.setTime(12000);
assert.deepStrictEqual(results, []);
// The date is advanced but the timers don't tick
assert.strictEqual(Date.now(), 12000);
});
```
## Class: `TestsStream`
<!-- YAML

View File

@@ -38,6 +38,10 @@ module.exports = class PriorityQueue {
return this.#heap[1];
}
peekBottom() {
return this.#heap[this.#size];
}
percolateDown(pos) {
const compare = this.#compare;
const setPosition = this.#setPosition;

View File

@@ -8,27 +8,31 @@ const {
ArrayPrototypeAt,
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
DateNow,
DatePrototypeGetTime,
DatePrototypeToString,
FunctionPrototypeApply,
FunctionPrototypeBind,
FunctionPrototypeToString,
globalThis,
NumberIsNaN,
ObjectDefineProperty,
ObjectDefineProperties,
ObjectGetOwnPropertyDescriptor,
ObjectGetOwnPropertyDescriptors,
Promise,
Symbol,
SymbolAsyncIterator,
SymbolDispose,
globalThis,
} = primordials;
const {
validateAbortSignal,
validateArray,
validateNumber,
} = require('internal/validators');
const {
AbortError,
codes: {
ERR_INVALID_STATE,
ERR_INVALID_ARG_VALUE,
},
codes: { ERR_INVALID_STATE, ERR_INVALID_ARG_VALUE },
} = require('internal/errors');
const PriorityQueue = require('internal/priority_queue');
@@ -37,6 +41,10 @@ const nodeTimersPromises = require('timers/promises');
const EventEmitter = require('events');
let kResistStopPropagation;
// Internal reference to the MockTimers class inside MockDate
let kMock;
// Initial epoch to which #now should be set to
const kInitialEpoch = 0;
function compareTimersLists(a, b) {
return (a.runAt - b.runAt) || (a.id - b.id);
@@ -50,7 +58,10 @@ function abortIt(signal) {
return new AbortError(undefined, { __proto__: null, cause: signal.reason });
}
const SUPPORTED_TIMERS = ['setTimeout', 'setInterval', 'setImmediate'];
/**
* @enum {('setTimeout'|'setInterval'|'setImmediate'|'Date')[]} Supported timers
*/
const SUPPORTED_APIS = ['setTimeout', 'setInterval', 'setImmediate', 'Date'];
const TIMERS_DEFAULT_INTERVAL = {
__proto__: null,
setImmediate: -1,
@@ -75,10 +86,12 @@ class MockTimers {
#realTimersClearImmediate;
#realPromisifiedSetImmediate;
#nativeDateDescriptor;
#timersInContext = [];
#isEnabled = false;
#currentTimer = 1;
#now = DateNow();
#now = kInitialEpoch;
#executionQueue = new PriorityQueue(compareTimersLists, setPosition);
@@ -86,222 +99,12 @@ class MockTimers {
#clearTimeout = FunctionPrototypeBind(this.#clearTimer, this);
#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');
}
#createTimer(isInterval, callback, delay, ...args) {
const timerId = this.#currentTimer++;
this.#executionQueue.insert({
__proto__: null,
id: timerId,
callback,
runAt: this.#now + delay,
interval: isInterval,
args,
});
return timerId;
}
#clearTimer(position) {
this.#executionQueue.removeAt(position);
}
async * #setIntervalPromisified(interval, startTime, options) {
const context = this;
const emitter = new EventEmitter();
if (options?.signal) {
validateAbortSignal(options.signal, 'options.signal');
if (options.signal.aborted) {
throw abortIt(options.signal);
}
const onAbort = (reason) => {
emitter.emit('data', { __proto__: null, aborted: true, reason });
};
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
options.signal.addEventListener('abort', onAbort, {
__proto__: null,
once: true,
[kResistStopPropagation]: true,
});
}
const eventIt = EventEmitter.on(emitter, 'data');
const callback = () => {
startTime += interval;
emitter.emit('data', startTime);
};
const timerId = this.#createTimer(true, callback, interval, options);
const clearListeners = () => {
emitter.removeAllListeners();
context.#clearTimer(timerId);
};
const iterator = {
__proto__: null,
[SymbolAsyncIterator]() {
return this;
},
async next() {
const result = await eventIt.next();
const value = ArrayPrototypeAt(result.value, 0);
if (value?.aborted) {
iterator.return();
throw abortIt(options.signal);
}
return {
__proto__: null,
done: result.done,
value,
};
},
async return() {
clearListeners();
return eventIt.return();
},
};
yield* iterator;
}
#promisifyTimer({ timerFn, clearFn, ms, result, options }) {
return new Promise((resolve, reject) => {
if (options?.signal) {
try {
validateAbortSignal(options.signal, 'options.signal');
} catch (err) {
return reject(err);
}
if (options.signal.aborted) {
return reject(abortIt(options.signal));
}
}
const onabort = () => {
clearFn(id);
return reject(abortIt(options.signal));
};
const id = timerFn(() => {
return resolve(result);
}, ms);
if (options?.signal) {
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
options.signal.addEventListener('abort', onabort, {
__proto__: null,
once: true,
[kResistStopPropagation]: true,
});
}
});
}
#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,
toFake: {
__proto__: null,
setTimeout: () => {
this.#storeOriginalSetTimeout();
globalThis.setTimeout = this.#setTimeout;
globalThis.clearTimeout = this.#clearTimeout;
nodeTimers.setTimeout = this.#setTimeout;
nodeTimers.clearTimeout = this.#clearTimeout;
nodeTimersPromises.setTimeout = FunctionPrototypeBind(
this.#setTimeoutPromisified,
this,
);
},
setInterval: () => {
this.#storeOriginalSetInterval();
globalThis.setInterval = this.#setInterval;
globalThis.clearInterval = this.#clearInterval;
nodeTimers.setInterval = this.#setInterval;
nodeTimers.clearInterval = this.#clearInterval;
nodeTimersPromises.setInterval = FunctionPrototypeBind(
this.#setIntervalPromisified,
this,
);
},
setImmediate: () => {
this.#storeOriginalSetImmediate();
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,
setTimeout: () => {
this.#restoreOriginalSetTimeout();
},
setInterval: () => {
this.#restoreOriginalSetInterval();
},
setImmediate: () => {
this.#restoreSetImmediate();
},
},
};
const target = activate ? options.toFake : options.toReal;
ArrayPrototypeForEach(this.#timersInContext, (timer) => target[timer]());
this.#isEnabled = activate;
}
#restoreSetImmediate() {
ObjectDefineProperty(
globalThis,
@@ -455,6 +258,351 @@ class MockTimers {
);
}
#createTimer(isInterval, callback, delay, ...args) {
const timerId = this.#currentTimer++;
this.#executionQueue.insert({
__proto__: null,
id: timerId,
callback,
runAt: this.#now + delay,
interval: isInterval,
args,
});
return timerId;
}
#clearTimer(position) {
this.#executionQueue.removeAt(position);
}
#createDate() {
kMock ??= Symbol('MockTimers');
const NativeDateConstructor = this.#nativeDateDescriptor.value;
/**
* Function to mock the Date constructor, treats cases as per ECMA-262
* and returns a Date object with a mocked implementation
* @typedef {Date} MockDate
* @returns {MockDate} a mocked Date object
*/
function MockDate(year, month, date, hours, minutes, seconds, ms) {
const mockTimersSource = MockDate[kMock];
const nativeDate = mockTimersSource.#nativeDateDescriptor.value;
// As of the fake-timers implementation for Sinon
// ref https://github.com/sinonjs/fake-timers/blob/a4c757f80840829e45e0852ea1b17d87a998388e/src/fake-timers-src.js#L456
// This covers the Date constructor called as a function ref.
// ECMA-262 Edition 5.1 section 15.9.2.
// and ECMA-262 Edition 14 Section 21.4.2.1
// replaces 'this instanceof MockDate' with a more reliable check
// from ECMA-262 Edition 14 Section 13.3.12.1 NewTarget
if (!new.target) {
return DatePrototypeToString(new nativeDate(mockTimersSource.#now));
}
// Cases where Date is called as a constructor
// This is intended as a defensive implementation to avoid
// having unexpected returns
switch (arguments.length) {
case 0:
return new nativeDate(MockDate[kMock].#now);
case 1:
return new nativeDate(year);
case 2:
return new nativeDate(year, month);
case 3:
return new nativeDate(year, month, date);
case 4:
return new nativeDate(year, month, date, hours);
case 5:
return new nativeDate(year, month, date, hours, minutes);
case 6:
return new nativeDate(year, month, date, hours, minutes, seconds);
default:
return new nativeDate(year, month, date, hours, minutes, seconds, ms);
}
}
// Prototype is read-only, and non assignable through Object.defineProperties
// eslint-disable-next-line no-unused-vars -- used to get the prototype out of the object
const { prototype, ...dateProps } = ObjectGetOwnPropertyDescriptors(NativeDateConstructor);
// Binds all the properties of Date to the MockDate function
ObjectDefineProperties(
MockDate,
dateProps,
);
MockDate.now = function now() {
return MockDate[kMock].#now;
};
// This is just to print the function { native code } in the console
// when the user prints the function and not the internal code
MockDate.toString = function toString() {
return FunctionPrototypeToString(MockDate[kMock].#nativeDateDescriptor.value);
};
// We need to polute the prototype of this
ObjectDefineProperties(MockDate, {
__proto__: null,
[kMock]: {
__proto__: null,
enumerable: false,
configurable: false,
writable: false,
value: this,
},
isMock: {
__proto__: null,
enumerable: true,
configurable: false,
writable: false,
value: true,
},
});
MockDate.prototype = NativeDateConstructor.prototype;
MockDate.parse = NativeDateConstructor.parse;
MockDate.UTC = NativeDateConstructor.UTC;
MockDate.prototype.toUTCString = NativeDateConstructor.prototype.toUTCString;
return MockDate;
}
async * #setIntervalPromisified(interval, startTime, options) {
const context = this;
const emitter = new EventEmitter();
if (options?.signal) {
validateAbortSignal(options.signal, 'options.signal');
if (options.signal.aborted) {
throw abortIt(options.signal);
}
const onAbort = (reason) => {
emitter.emit('data', { __proto__: null, aborted: true, reason });
};
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
options.signal.addEventListener('abort', onAbort, {
__proto__: null,
once: true,
[kResistStopPropagation]: true,
});
}
const eventIt = EventEmitter.on(emitter, 'data');
const callback = () => {
startTime += interval;
emitter.emit('data', startTime);
};
const timerId = this.#createTimer(true, callback, interval, options);
const clearListeners = () => {
emitter.removeAllListeners();
context.#clearTimer(timerId);
};
const iterator = {
__proto__: null,
[SymbolAsyncIterator]() {
return this;
},
async next() {
const result = await eventIt.next();
const value = ArrayPrototypeAt(result.value, 0);
if (value?.aborted) {
iterator.return();
throw abortIt(options.signal);
}
return {
__proto__: null,
done: result.done,
value,
};
},
async return() {
clearListeners();
return eventIt.return();
},
};
yield* iterator;
}
#setImmediate(callback, ...args) {
return this.#createTimer(
false,
callback,
TIMERS_DEFAULT_INTERVAL.setImmediate,
...args,
);
}
#promisifyTimer({ timerFn, clearFn, ms, result, options }) {
return new Promise((resolve, reject) => {
if (options?.signal) {
try {
validateAbortSignal(options.signal, 'options.signal');
} catch (err) {
return reject(err);
}
if (options.signal.aborted) {
return reject(abortIt(options.signal));
}
}
const onabort = () => {
clearFn(id);
return reject(abortIt(options.signal));
};
const id = timerFn(() => {
return resolve(result);
}, ms);
if (options?.signal) {
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
options.signal.addEventListener('abort', onabort, {
__proto__: null,
once: true,
[kResistStopPropagation]: true,
});
}
});
}
#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,
});
}
#assertTimersAreEnabled() {
if (!this.#isEnabled) {
throw new ERR_INVALID_STATE(
'You should enable MockTimers first by calling the .enable function',
);
}
}
#assertTimeArg(time) {
if (time < 0) {
throw new ERR_INVALID_ARG_VALUE('time', 'positive integer', time);
}
}
#isValidDateWithGetTime(maybeDate) {
// Validation inspired on https://github.com/inspect-js/is-date-object/blob/main/index.js#L3-L11
try {
DatePrototypeGetTime(maybeDate);
return true;
} catch {
return false;
}
}
#toggleEnableTimers(activate) {
const options = {
__proto__: null,
toFake: {
__proto__: null,
setTimeout: () => {
this.#storeOriginalSetTimeout();
globalThis.setTimeout = this.#setTimeout;
globalThis.clearTimeout = this.#clearTimeout;
nodeTimers.setTimeout = this.#setTimeout;
nodeTimers.clearTimeout = this.#clearTimeout;
nodeTimersPromises.setTimeout = FunctionPrototypeBind(
this.#setTimeoutPromisified,
this,
);
},
setInterval: () => {
this.#storeOriginalSetInterval();
globalThis.setInterval = this.#setInterval;
globalThis.clearInterval = this.#clearInterval;
nodeTimers.setInterval = this.#setInterval;
nodeTimers.clearInterval = this.#clearInterval;
nodeTimersPromises.setInterval = FunctionPrototypeBind(
this.#setIntervalPromisified,
this,
);
},
setImmediate: () => {
this.#storeOriginalSetImmediate();
// setImmediate functions needs to bind MockTimers
// otherwise it will throw an error when called
// "Receiver must be an instance of MockTimers"
// because #setImmediate is the only function here
// that calls #createTimer and it's not bound to MockTimers
globalThis.setImmediate = FunctionPrototypeBind(
this.#setImmediate,
this,
);
globalThis.clearImmediate = this.#clearImmediate;
nodeTimers.setImmediate = FunctionPrototypeBind(
this.#setImmediate,
this,
);
nodeTimers.clearImmediate = this.#clearImmediate;
nodeTimersPromises.setImmediate = FunctionPrototypeBind(
this.#setImmediatePromisified,
this,
);
},
Date: () => {
this.#nativeDateDescriptor = ObjectGetOwnPropertyDescriptor(globalThis, 'Date');
globalThis.Date = this.#createDate();
},
},
toReal: {
__proto__: null,
setTimeout: () => {
this.#restoreOriginalSetTimeout();
},
setInterval: () => {
this.#restoreOriginalSetInterval();
},
setImmediate: () => {
this.#restoreSetImmediate();
},
Date: () => {
ObjectDefineProperty(globalThis, 'Date', this.#nativeDateDescriptor);
},
},
};
const target = activate ? options.toFake : options.toReal;
ArrayPrototypeForEach(this.#timersInContext, (timer) => target[timer]());
this.#isEnabled = activate;
}
/**
* Advances the virtual time of MockTimers by the specified duration (in milliseconds).
* This method simulates the passage of time and triggers any scheduled timers that are due.
@@ -463,19 +611,8 @@ class MockTimers {
* @throws {ERR_INVALID_ARG_VALUE} If a negative time value is provided.
*/
tick(time = 1) {
if (!this.#isEnabled) {
throw new ERR_INVALID_STATE(
'You should enable MockTimers first by calling the .enable function',
);
}
if (time < 0) {
throw new ERR_INVALID_ARG_VALUE(
'time',
'positive integer',
time,
);
}
this.#assertTimersAreEnabled();
this.#assertTimeArg(time);
this.#now += time;
let timer = this.#executionQueue.peek();
@@ -496,36 +633,68 @@ class MockTimers {
}
/**
* Enables MockTimers for the specified timers.
* @param {string[]} timers - An array of timer types to enable, e.g., ['setTimeout', 'setInterval'].
* @throws {ERR_INVALID_STATE} If MockTimers are already enabled.
* @throws {ERR_INVALID_ARG_VALUE} If an unsupported timer type is specified.
* @typedef {{apis: SUPPORTED_APIS;now: number | Date;}} EnableOptions Options to enable the timers
* @property {SUPPORTED_APIS} apis List of timers to enable, defaults to all
* @property {number | Date} now The epoch to which the timers should be set to, defaults to 0
*/
enable(timers = SUPPORTED_TIMERS) {
/**
* Enables the MockTimers replacing the native timers with the fake ones.
* @param {EnableOptions} options
*/
enable(options = { __proto__: null, apis: SUPPORTED_APIS, now: 0 }) {
const internalOptions = { __proto__: null, ...options };
if (this.#isEnabled) {
throw new ERR_INVALID_STATE(
'MockTimers is already enabled!',
);
throw new ERR_INVALID_STATE('MockTimers is already enabled!');
}
validateArray(timers, 'timers');
if (NumberIsNaN(internalOptions.now)) {
throw new ERR_INVALID_ARG_VALUE('now', internalOptions.now, `epoch must be a positive integer received ${internalOptions.now}`);
}
if (!internalOptions.now) {
internalOptions.now = 0;
}
if (!internalOptions.apis) {
internalOptions.apis = SUPPORTED_APIS;
}
validateArray(internalOptions.apis, 'options.apis');
// Check that the timers passed are supported
ArrayPrototypeForEach(timers, (timer) => {
if (!ArrayPrototypeIncludes(SUPPORTED_TIMERS, timer)) {
ArrayPrototypeForEach(internalOptions.apis, (timer) => {
if (!ArrayPrototypeIncludes(SUPPORTED_APIS, timer)) {
throw new ERR_INVALID_ARG_VALUE(
'timers',
'options.apis',
timer,
`option ${timer} is not supported`,
);
}
});
this.#timersInContext = internalOptions.apis;
// Checks if the second argument is the initial time
if (this.#isValidDateWithGetTime(internalOptions.now)) {
this.#now = DatePrototypeGetTime(internalOptions.now);
} else if (validateNumber(internalOptions.now, 'initialTime') === undefined) {
this.#assertTimeArg(internalOptions.now);
this.#now = internalOptions.now;
}
this.#timersInContext = timers;
this.#now = DateNow();
this.#toggleEnableTimers(true);
}
/**
* Sets the current time to the given epoch.
* @param {number} time The epoch to set the current time to.
*/
setTime(time = kInitialEpoch) {
validateNumber(time, 'time');
this.#assertTimeArg(time);
this.#assertTimersAreEnabled();
this.#now = time;
}
/**
* An alias for `this.reset()`, allowing the disposal of the `MockTimers` instance.
*/
@@ -543,6 +712,7 @@ class MockTimers {
this.#toggleEnableTimers(false);
this.#timersInContext = [];
this.#now = kInitialEpoch;
let timer = this.#executionQueue.peek();
while (timer) {
@@ -556,13 +726,10 @@ class MockTimers {
* @throws {ERR_INVALID_STATE} If MockTimers are not enabled.
*/
runAll() {
if (!this.#isEnabled) {
throw new ERR_INVALID_STATE(
'You should enable MockTimers first by calling the .enable function',
);
}
this.tick(Infinity);
this.#assertTimersAreEnabled();
const longestTimer = this.#executionQueue.peekBottom();
if (!longestTimer) return;
this.tick(longestTimer.runAt - this.#now);
}
}

View File

@@ -11,7 +11,7 @@ describe('Mock Timers Test Suite', () => {
describe('MockTimers API', () => {
it('should throw an error if trying to enable a timer that is not supported', (t) => {
assert.throws(() => {
t.mock.timers.enable(['DOES_NOT_EXIST']);
t.mock.timers.enable({ apis: ['DOES_NOT_EXIST'] });
}, {
code: 'ERR_INVALID_ARG_VALUE',
});
@@ -46,6 +46,7 @@ describe('Mock Timers Test Suite', () => {
code: 'ERR_INVALID_ARG_VALUE',
});
});
it('should check that propertyDescriptor gets back after resetting timers', (t) => {
const getDescriptor = (ctx, fn) => Object.getOwnPropertyDescriptor(ctx, fn);
const getCurrentTimersDescriptors = () => {
@@ -107,6 +108,7 @@ describe('Mock Timers Test Suite', () => {
const fn = t.mock.fn();
global.setTimeout(fn, 1000);
t.mock.timers.reset();
assert.deepStrictEqual(Date.now, globalThis.Date.now);
assert.throws(() => {
t.mock.timers.tick(1000);
}, {
@@ -166,14 +168,34 @@ describe('Mock Timers Test Suite', () => {
assert.strictEqual(timeoutFn.mock.callCount(), 1);
assert.strictEqual(intervalFn.mock.callCount(), 1);
});
});
it('should increase the epoch as the tick run for runAll', async (t) => {
const timeoutFn = t.mock.fn();
const intervalFn = t.mock.fn();
t.mock.timers.enable();
global.setTimeout(timeoutFn, 1111);
const id = global.setInterval(intervalFn, 9999);
t.mock.timers.runAll();
global.clearInterval(id);
assert.strictEqual(timeoutFn.mock.callCount(), 1);
assert.strictEqual(intervalFn.mock.callCount(), 1);
assert.strictEqual(Date.now(), 9999);
});
it('should not error if there are not timers to run', (t) => {
t.mock.timers.enable();
t.mock.timers.runAll();
// Should not throw
});
});
});
describe('globals/timers', () => {
describe('setTimeout Suite', () => {
it('should advance in time and trigger timers when calling the .tick function', (t) => {
mock.timers.enable(['setTimeout']);
mock.timers.enable({ apis: ['setTimeout'] });
const fn = mock.fn();
@@ -185,7 +207,7 @@ describe('Mock Timers Test Suite', () => {
});
it('should advance in time and trigger timers when calling the .tick function multiple times', (t) => {
t.mock.timers.enable(['setTimeout']);
t.mock.timers.enable({ apis: ['setTimeout'] });
const fn = t.mock.fn();
global.setTimeout(fn, 2000);
@@ -199,7 +221,7 @@ describe('Mock Timers Test Suite', () => {
});
it('should work with the same params as the original setTimeout', (t) => {
t.mock.timers.enable(['setTimeout']);
t.mock.timers.enable({ apis: ['setTimeout'] });
const fn = t.mock.fn();
const args = ['a', 'b', 'c'];
global.setTimeout(fn, 2000, ...args);
@@ -221,12 +243,11 @@ describe('Mock Timers Test Suite', () => {
done();
}), timeout);
});
});
describe('clearTimeout Suite', () => {
it('should not advance in time if clearTimeout was invoked', (t) => {
t.mock.timers.enable(['setTimeout']);
t.mock.timers.enable({ apis: ['setTimeout'] });
const fn = mock.fn();
@@ -240,7 +261,7 @@ describe('Mock Timers Test Suite', () => {
describe('setInterval Suite', () => {
it('should tick three times using fake setInterval', (t) => {
t.mock.timers.enable(['setInterval']);
t.mock.timers.enable({ apis: ['setInterval'] });
const fn = t.mock.fn();
const id = global.setInterval(fn, 200);
@@ -255,7 +276,7 @@ describe('Mock Timers Test Suite', () => {
});
it('should work with the same params as the original setInterval', (t) => {
t.mock.timers.enable(['setInterval']);
t.mock.timers.enable({ apis: ['setInterval'] });
const fn = t.mock.fn();
const args = ['a', 'b', 'c'];
const id = global.setInterval(fn, 200, ...args);
@@ -270,13 +291,12 @@ describe('Mock Timers Test Suite', () => {
assert.deepStrictEqual(fn.mock.calls[0].arguments, args);
assert.deepStrictEqual(fn.mock.calls[1].arguments, args);
assert.deepStrictEqual(fn.mock.calls[2].arguments, args);
});
});
describe('clearInterval Suite', () => {
it('should not advance in time if clearInterval was invoked', (t) => {
t.mock.timers.enable(['setInterval']);
t.mock.timers.enable({ apis: ['setInterval'] });
const fn = mock.fn();
const id = global.setInterval(fn, 200);
@@ -299,7 +319,7 @@ describe('Mock Timers Test Suite', () => {
});
it('should work with the same params as the original setImmediate', (t) => {
t.mock.timers.enable(['setImmediate']);
t.mock.timers.enable({ apis: ['setImmediate'] });
const fn = t.mock.fn();
const args = ['a', 'b', 'c'];
global.setImmediate(fn, ...args);
@@ -310,7 +330,7 @@ describe('Mock Timers Test Suite', () => {
});
it('should not advance in time if clearImmediate was invoked', (t) => {
t.mock.timers.enable(['setImmediate']);
t.mock.timers.enable({ apis: ['setImmediate'] });
const id = global.setImmediate(common.mustNotCall());
global.clearImmediate(id);
@@ -318,13 +338,13 @@ describe('Mock Timers Test Suite', () => {
});
it('should advance in time and trigger timers when calling the .tick function', (t) => {
t.mock.timers.enable(['setImmediate']);
t.mock.timers.enable({ apis: ['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']);
t.mock.timers.enable({ apis: ['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));
@@ -338,7 +358,7 @@ describe('Mock Timers Test Suite', () => {
});
it('should execute setImmediate first if setTimeout was also called', (t) => {
t.mock.timers.enable(['setImmediate', 'setTimeout']);
t.mock.timers.enable({ apis: ['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));
@@ -351,524 +371,357 @@ describe('Mock Timers Test Suite', () => {
assert.deepStrictEqual(order, ['f1', 'f2']);
});
});
});
describe('timers Suite', () => {
describe('setTimeout Suite', () => {
it('should advance in time and trigger timers when calling the .tick function multiple times', (t) => {
t.mock.timers.enable(['setTimeout']);
const fn = t.mock.fn();
const { setTimeout } = nodeTimers;
setTimeout(fn, 2000);
describe('timers/promises', () => {
describe('setTimeout Suite', () => {
it('should advance in time and trigger timers when calling the .tick function multiple times', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
t.mock.timers.tick(1000);
t.mock.timers.tick(500);
t.mock.timers.tick(500);
const p = nodeTimersPromises.setTimeout(2000);
assert.strictEqual(fn.mock.callCount(), 1);
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/setTimeout', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const expectedResult = 'result';
const controller = new AbortController();
const p = nodeTimersPromises.setTimeout(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/setTimeout received an aborted signal', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const expectedResult = 'result';
const controller = new AbortController();
const p = nodeTimersPromises.setTimeout(2000, expectedResult, {
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: ['setTimeout'] });
const expectedResult = 'result';
const controller = new AbortController();
const p = nodeTimersPromises.setTimeout(2000, expectedResult, {
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: ['setTimeout'] });
const expectedResult = 'result';
const p = nodeTimersPromises.setTimeout(2000, expectedResult, {
ref: true,
signal: {},
});
await assert.rejects(() => p, {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
});
});
});
it('should work with the same params as the original timers.setTimeout', (t) => {
t.mock.timers.enable(['setTimeout']);
const fn = t.mock.fn();
const { setTimeout } = nodeTimers;
const args = ['a', 'b', 'c'];
setTimeout(fn, 2000, ...args);
describe('setInterval Suite', () => {
it('should tick three times using fake setInterval', async (t) => {
t.mock.timers.enable({ apis: ['setInterval'] });
t.mock.timers.tick(1000);
t.mock.timers.tick(500);
t.mock.timers.tick(500);
const interval = 100;
const intervalIterator = nodeTimersPromises.setInterval(interval, Date.now());
assert.strictEqual(fn.mock.callCount(), 1);
assert.deepStrictEqual(fn.mock.calls[0].arguments, args);
});
});
const first = intervalIterator.next();
const second = intervalIterator.next();
const third = intervalIterator.next();
describe('clearTimeout Suite', () => {
it('should not advance in time if clearTimeout was invoked', (t) => {
t.mock.timers.enable(['setTimeout']);
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
const fn = mock.fn();
const { setTimeout, clearTimeout } = nodeTimers;
const id = setTimeout(fn, 2000);
clearTimeout(id);
t.mock.timers.tick(2000);
const results = await Promise.all([
first,
second,
third,
]);
assert.strictEqual(fn.mock.callCount(), 0);
});
});
const finished = await intervalIterator.return();
assert.deepStrictEqual(finished, { done: true, value: undefined });
results.forEach((result) => {
assert.strictEqual(typeof result.value, 'number');
assert.strictEqual(result.done, false);
});
});
it('should tick five times testing a real use case', async (t) => {
t.mock.timers.enable({ apis: ['setInterval'] });
describe('setInterval Suite', () => {
it('should tick three times using fake setInterval', (t) => {
t.mock.timers.enable(['setInterval']);
const fn = t.mock.fn();
const expectedIterations = 5;
const interval = 1000;
const startedAt = Date.now();
async function run() {
const times = [];
for await (const time of nodeTimersPromises.setInterval(interval, startedAt)) {
times.push(time);
if (times.length === expectedIterations) break;
}
return times;
}
const id = nodeTimers.setInterval(fn, 200);
const r = run();
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
t.mock.timers.tick(200);
t.mock.timers.tick(200);
t.mock.timers.tick(200);
t.mock.timers.tick(200);
const timeResults = await r;
assert.strictEqual(timeResults.length, expectedIterations);
for (let it = 1; it < expectedIterations; it++) {
assert.strictEqual(timeResults[it - 1], startedAt + (interval * it));
}
});
nodeTimers.clearInterval(id);
it('should abort operation given an abort controller signal', async (t) => {
t.mock.timers.enable({ apis: ['setInterval'] });
assert.strictEqual(fn.mock.callCount(), 4);
});
const interval = 100;
const abortController = new AbortController();
const intervalIterator = nodeTimersPromises.setInterval(interval, Date.now(), {
signal: abortController.signal,
});
it('should work with the same params as the original timers.setInterval', (t) => {
t.mock.timers.enable(['setInterval']);
const fn = t.mock.fn();
const args = ['a', 'b', 'c'];
const id = nodeTimers.setInterval(fn, 200, ...args);
const first = intervalIterator.next();
const second = intervalIterator.next();
t.mock.timers.tick(200);
t.mock.timers.tick(200);
t.mock.timers.tick(200);
t.mock.timers.tick(200);
t.mock.timers.tick(interval);
abortController.abort();
t.mock.timers.tick(interval);
nodeTimers.clearInterval(id);
const firstResult = await first;
// Interval * 2 because value can be a little bit greater than interval
assert.ok(firstResult.value < Date.now() + interval * 2);
assert.strictEqual(firstResult.done, false);
assert.strictEqual(fn.mock.callCount(), 4);
assert.deepStrictEqual(fn.mock.calls[0].arguments, args);
assert.deepStrictEqual(fn.mock.calls[1].arguments, args);
assert.deepStrictEqual(fn.mock.calls[2].arguments, args);
assert.deepStrictEqual(fn.mock.calls[3].arguments, args);
await assert.rejects(() => second, {
name: 'AbortError',
});
});
});
});
it('should abort operation when .abort is called before calling setInterval', async (t) => {
t.mock.timers.enable({ apis: ['setInterval'] });
describe('clearInterval Suite', () => {
it('should not advance in time if clearInterval was invoked', (t) => {
t.mock.timers.enable(['setInterval']);
const interval = 100;
const abortController = new AbortController();
abortController.abort();
const intervalIterator = nodeTimersPromises.setInterval(interval, Date.now(), {
signal: abortController.signal,
});
const fn = mock.fn();
const { setInterval, clearInterval } = nodeTimers;
const id = setInterval(fn, 200);
clearInterval(id);
t.mock.timers.tick(200);
const first = intervalIterator.next();
t.mock.timers.tick(interval);
assert.strictEqual(fn.mock.callCount(), 0);
});
});
await assert.rejects(() => first, {
name: 'AbortError',
});
});
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 abort operation given an abort controller signal on a real use case', async (t) => {
t.mock.timers.enable({ apis: ['setInterval'] });
const controller = new AbortController();
const signal = controller.signal;
const interval = 200;
const expectedIterations = 2;
const startedAt = Date.now();
const timeResults = [];
async function run() {
const it = nodeTimersPromises.setInterval(interval, startedAt, { signal });
for await (const time of it) {
timeResults.push(time);
if (timeResults.length === 5) break;
}
}
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);
const r = run();
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
controller.abort();
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
assert.strictEqual(fn.mock.callCount(), 1);
assert.deepStrictEqual(fn.mock.calls[0].arguments, args);
});
await assert.rejects(() => r, {
name: 'AbortError',
});
assert.strictEqual(timeResults.length, expectedIterations);
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']);
for (let it = 1; it < expectedIterations; it++) {
assert.strictEqual(timeResults[it - 1], startedAt + (interval * it));
}
});
});
});
});
describe('timers/promises', () => {
describe('setTimeout Suite', () => {
it('should advance in time and trigger timers when calling the .tick function multiple times', async (t) => {
t.mock.timers.enable(['setTimeout']);
const p = nodeTimersPromises.setTimeout(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/setTimeout', async (t) => {
t.mock.timers.enable(['setTimeout']);
const expectedResult = 'result';
const controller = new AbortController();
const p = nodeTimersPromises.setTimeout(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/setTimeout received an aborted signal', async (t) => {
t.mock.timers.enable(['setTimeout']);
const expectedResult = 'result';
const controller = new AbortController();
const p = nodeTimersPromises.setTimeout(2000, expectedResult, {
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 wasn\'t called', async (t) => {
t.mock.timers.enable(['setTimeout']);
const expectedResult = 'result';
const controller = new AbortController();
const p = nodeTimersPromises.setTimeout(2000, expectedResult, {
ref: true,
signal: controller.signal
});
controller.abort();
await assert.rejects(() => p, {
name: 'AbortError',
});
});
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();
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(['setTimeout']);
const expectedResult = 'result';
const p = nodeTimersPromises.setTimeout(2000, expectedResult, {
ref: true,
signal: {}
});
await assert.rejects(() => p, {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE'
});
});
describe('Date Suite', () => {
it('should return the initial UNIX epoch if not specified', (t) => {
t.mock.timers.enable({ apis: ['Date'] });
const date = new Date();
assert.strictEqual(date.getTime(), 0);
assert.strictEqual(Date.now(), 0);
});
describe('setInterval Suite', () => {
it('should tick three times using fake setInterval', async (t) => {
t.mock.timers.enable(['setInterval']);
const interval = 100;
const intervalIterator = nodeTimersPromises.setInterval(interval, Date.now());
const first = intervalIterator.next();
const second = intervalIterator.next();
const third = intervalIterator.next();
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
const results = await Promise.all([
first,
second,
third,
]);
const finished = await intervalIterator.return();
assert.deepStrictEqual(finished, { done: true, value: undefined });
results.forEach((result) => {
assert.strictEqual(typeof result.value, 'number');
assert.strictEqual(result.done, false);
});
});
it('should tick five times testing a real use case', async (t) => {
t.mock.timers.enable(['setInterval']);
const expectedIterations = 5;
const interval = 1000;
const startedAt = Date.now();
async function run() {
const times = [];
for await (const time of nodeTimersPromises.setInterval(interval, startedAt)) {
times.push(time);
if (times.length === expectedIterations) break;
}
return times;
}
const r = run();
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
const timeResults = await r;
assert.strictEqual(timeResults.length, expectedIterations);
for (let it = 1; it < expectedIterations; it++) {
assert.strictEqual(timeResults[it - 1], startedAt + (interval * it));
}
});
it('should abort operation given an abort controller signal', async (t) => {
t.mock.timers.enable(['setInterval']);
const interval = 100;
const abortController = new AbortController();
const intervalIterator = nodeTimersPromises.setInterval(interval, Date.now(), {
signal: abortController.signal
});
const first = intervalIterator.next();
const second = intervalIterator.next();
t.mock.timers.tick(interval);
abortController.abort();
t.mock.timers.tick(interval);
const firstResult = await first;
// Interval * 2 because value can be a little bit greater than interval
assert.ok(firstResult.value < Date.now() + interval * 2);
assert.strictEqual(firstResult.done, false);
await assert.rejects(() => second, {
name: 'AbortError',
});
});
it('should abort operation when .abort is called before calling setInterval', async (t) => {
t.mock.timers.enable(['setInterval']);
const interval = 100;
const abortController = new AbortController();
abortController.abort();
const intervalIterator = nodeTimersPromises.setInterval(interval, Date.now(), {
signal: abortController.signal
});
const first = intervalIterator.next();
t.mock.timers.tick(interval);
await assert.rejects(() => first, {
name: 'AbortError',
});
});
it('should abort operation given an abort controller signal on a real use case', async (t) => {
t.mock.timers.enable(['setInterval']);
const controller = new AbortController();
const signal = controller.signal;
const interval = 200;
const expectedIterations = 2;
const startedAt = Date.now();
const timeResults = [];
async function run() {
const it = nodeTimersPromises.setInterval(interval, startedAt, { signal });
for await (const time of it) {
timeResults.push(time);
if (timeResults.length === 5) break;
}
}
const r = run();
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
controller.abort();
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
t.mock.timers.tick(interval);
await assert.rejects(() => r, {
name: 'AbortError',
});
assert.strictEqual(timeResults.length, expectedIterations);
for (let it = 1; it < expectedIterations; it++) {
assert.strictEqual(timeResults[it - 1], startedAt + (interval * it));
}
});
it('should throw an error if setTime is called without enabling timers', (t) => {
assert.throws(
() => {
t.mock.timers.setTime(100);
},
{ code: 'ERR_INVALID_STATE' }
);
});
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();
it('should throw an error if epoch passed to enable is not valid', (t) => {
assert.throws(
() => {
t.mock.timers.enable({ now: -1 });
},
{ code: 'ERR_INVALID_ARG_VALUE' }
);
t.mock.timers.tick(5555);
assert.throws(
() => {
t.mock.timers.enable({ now: 'string' });
},
{ code: 'ERR_INVALID_ARG_TYPE' }
);
p.then(common.mustCall((result) => {
assert.strictEqual(result, undefined);
done();
}, 1));
});
assert.throws(
() => {
t.mock.timers.enable({ now: NaN });
},
{ code: 'ERR_INVALID_ARG_VALUE' }
);
});
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
});
it('should replace the original Date with the mocked one', (t) => {
t.mock.timers.enable({ apis: ['Date'] });
assert.ok(Date.isMock);
});
t.mock.timers.tick(500);
it('should return the ticked time when calling Date.now after tick', (t) => {
t.mock.timers.enable({ apis: ['Date'] });
const time = 100;
t.mock.timers.tick(time);
assert.strictEqual(Date.now(), time);
});
const result = await p;
assert.strictEqual(result, expectedResult);
});
it('should return the Date as string when calling it as a function', (t) => {
t.mock.timers.enable({ apis: ['Date'] });
const returned = Date();
// Matches the format: 'Mon Jan 01 1970 00:00:00'
// We don't care about the date, just the format
assert.ok(/\w{3}\s\w{3}\s\d{1,2}\s\d{2,4}\s\d{1,2}:\d{2}:\d{2}/.test(returned));
});
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
});
it('should return the date with different argument calls', (t) => {
t.mock.timers.enable({ apis: ['Date'] });
assert.strictEqual(new Date(0).getTime(), 0);
assert.strictEqual(new Date(100).getTime(), 100);
assert.strictEqual(new Date('1970-01-01T00:00:00.000Z').getTime(), 0);
assert.strictEqual(new Date(1970, 0).getFullYear(), 1970);
assert.strictEqual(new Date(1970, 0).getMonth(), 0);
assert.strictEqual(new Date(1970, 0, 1).getDate(), 1);
assert.strictEqual(new Date(1970, 0, 1, 11).getHours(), 11);
assert.strictEqual(new Date(1970, 0, 1, 11, 10).getMinutes(), 10);
assert.strictEqual(new Date(1970, 0, 1, 11, 10, 45).getSeconds(), 45);
assert.strictEqual(new Date(1970, 0, 1, 11, 10, 45, 898).getMilliseconds(), 898);
assert.strictEqual(new Date(1970, 0, 1, 11, 10, 45, 898).toDateString(), 'Thu Jan 01 1970');
});
controller.abort();
t.mock.timers.tick(0);
it('should return native code when calling Date.toString', (t) => {
t.mock.timers.enable({ apis: ['Date'] });
assert.strictEqual(Date.toString(), 'function Date() { [native code] }');
});
await assert.rejects(() => p, {
name: 'AbortError',
});
it('should start with a custom epoch if the second argument is specified', (t) => {
t.mock.timers.enable({ apis: ['Date'], now: 100 });
const date1 = new Date();
assert.strictEqual(date1.getTime(), 100);
});
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
});
t.mock.timers.reset();
t.mock.timers.enable({ apis: ['Date'], now: new Date(200) });
const date2 = new Date();
assert.strictEqual(date2.getTime(), 200);
});
controller.abort();
it('should replace epoch if setTime is lesser than now and not tick', (t) => {
t.mock.timers.enable();
const fn = t.mock.fn();
const id = setTimeout(fn, 1000);
t.mock.timers.setTime(800);
assert.strictEqual(Date.now(), 800);
t.mock.timers.setTime(500);
assert.strictEqual(Date.now(), 500);
assert.strictEqual(fn.mock.callCount(), 0);
clearTimeout(id);
});
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');
});
it('should not tick time when setTime is called', (t) => {
t.mock.timers.enable();
const fn = t.mock.fn();
const id = setTimeout(fn, 1000);
t.mock.timers.setTime(1200);
assert.strictEqual(Date.now(), 1200);
assert.strictEqual(fn.mock.callCount(), 0);
clearTimeout(id);
});
});
});