Skip to content

Commit

Permalink
test_runner: add initial draft for fakeTimers
Browse files Browse the repository at this point in the history
Signed-off-by: Erick Wendel <erick.workspace@gmail.com>
  • Loading branch information
ErickWendel committed Apr 28, 2023
1 parent b31d587 commit 568ad96
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 0 deletions.
110 changes: 110 additions & 0 deletions lib/internal/test_runner/fake_timers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use strict';

const {
DateNow,
SafeMap,
Symbol,
globalThis,
} = primordials;

class Timers {
constructor() {
this.timers = new SafeMap();

this.setTimeout = this.#createTimer.bind(this, false);
this.clearTimeout = this.#clearTimer.bind(this);
this.setInterval = this.#createTimer.bind(this, true);
this.clearInterval = this.#clearTimer.bind(this);
}

#createTimer(isInterval, callback, delay, ...args) {
const timerId = Symbol('kTimerId');
const timer = {
id: timerId,
callback,
time: DateNow() + delay,
interval: isInterval,
args,
};
this.timers.set(timerId, timer);
return timerId;
}

#clearTimer(timerId) {
this.timers.delete(timerId);
}

}

let realSetTimeout;
let realClearTimeout;
let realSetInterval;
let realClearInterval;

class FakeTimers {
constructor() {
this.fakeTimers = {};
this.isEnabled = false;
this.now = DateNow();
}

tick(time = 0) {

// if (!this.isEnabled) {
// throw new Error('you should enable fakeTimers first by calling the .enable function');
// }

this.now += time;
const timers = this.fakeTimers.timers;

for (const timer of timers.values()) {

if (!(this.now >= timer.time)) continue;

timer.callback(...timer.args);
if (timer.interval) {
timer.time = this.now + (timer.time - this.now) % timer.args[0];
continue;
}

timers.delete(timer.id);
}
}

enable() {
// if (this.isEnabled) {
// throw new Error('fakeTimers is already enabled!');
// }
this.now = DateNow();
this.isEnabled = true;
this.fakeTimers = new Timers();

realSetTimeout = globalThis.setTimeout;
realClearTimeout = globalThis.clearTimeout;
realSetInterval = globalThis.setInterval;
realClearInterval = globalThis.clearInterval;

globalThis.setTimeout = this.fakeTimers.setTimeout;
globalThis.clearTimeout = this.fakeTimers.clearTimeout;
globalThis.setInterval = this.fakeTimers.setInterval;
globalThis.clearInterval = this.fakeTimers.clearInterval;

}

reset() {
this.isEnabled = false;
this.fakeTimers = {};

// Restore the real timer functions
globalThis.setTimeout = realSetTimeout;
globalThis.clearTimeout = realClearTimeout;
globalThis.setInterval = realSetInterval;
globalThis.clearInterval = realClearInterval;
}

releaseAllTimers() {

}
}

module.exports = { FakeTimers };
6 changes: 6 additions & 0 deletions lib/internal/test_runner/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const {
AbortError,
} = require('internal/errors');
const { MockTracker } = require('internal/test_runner/mock');
const { FakeTimers } = require('internal/test_runner/fake_timers');
const { TestsStream } = require('internal/test_runner/tests_stream');
const {
createDeferredCallback,
Expand Down Expand Up @@ -108,6 +109,11 @@ class TestContext {
return this.#test.mock;
}

get fakeTimers() {
this.#test.fakeTimers ??= new FakeTimers();
return this.#test.fakeTimers;
}

runOnly(value) {
this.#test.runOnlySubtests = !!value;
}
Expand Down
17 changes: 17 additions & 0 deletions lib/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,20 @@ ObjectDefineProperty(module.exports, 'mock', {
return lazyMock;
},
});

let lazyFakeTimers;

ObjectDefineProperty(module.exports, 'fakeTimers', {
__proto__: null,
configurable: true,
enumerable: true,
get() {
if (lazyFakeTimers === undefined) {
const { FakeTimers } = require('internal/test_runner/fake_timers');

lazyFakeTimers = new FakeTimers();
}

return lazyFakeTimers;
},
});
46 changes: 46 additions & 0 deletions test/parallel/test-runner-fake-timers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use strict';
const common = require('../common');
process.env.NODE_TEST_KNOWN_GLOBALS = 0;

const assert = require('node:assert');
const { fakeTimers, it, mock, afterEach, describe } = require('node:test');
describe('Faketimers Test Suite', () => {

describe('setTimeout Suite', () => {
afterEach(() => fakeTimers.reset());

it('should advance in time and trigger timers when calling the .tick function', (t) => {
fakeTimers.enable();

const fn = mock.fn(() => {});

global.setTimeout(fn, 4000);

fakeTimers.tick(4000);
assert.ok(fn.mock.callCount());
});

it('should advance in time and trigger timers when calling the .tick function multiple times', (t) => {
fakeTimers.enable();
const fn = mock.fn();

global.setTimeout(fn, 2000);

fakeTimers.tick(1000);
fakeTimers.tick(1000);

assert.strictEqual(fn.mock.callCount(), 1);
});

it('should keep setTimeout working if fakeTimers are disabled', (t, done) => {
const now = Date.now();
const timeout = 2;
const expected = () => now - timeout;
global.setTimeout(common.mustCall(() => {
assert.strictEqual(now - timeout, expected());
done();
}), timeout);
});

});
});

0 comments on commit 568ad96

Please sign in to comment.