Skip to content

Commit

Permalink
feat: add fake timers implementation backed by Lolex (#8897)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB committed Sep 6, 2019
1 parent 85e84b4 commit 9279a3a
Show file tree
Hide file tree
Showing 13 changed files with 1,049 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -8,6 +8,7 @@
- `[jest-diff]` [**BREAKING**] Export as ECMAScript module ([#8873](https://github.com/facebook/jest/pull/8873))
- `[jest-diff]` Add `includeChangeCounts` and rename `Indicator` options ([#8881](https://github.com/facebook/jest/pull/8881))
- `[jest-diff]` Add `changeColor` and `patchColor` options ([#8911](https://github.com/facebook/jest/pull/8911))
- `[@jest/fake-timers]` Add Lolex as implementation of fake timers ([#8897](https://github.com/facebook/jest/pull/8897))
- `[jest-runner]` Warn if a worker had to be force exited ([#8206](https://github.com/facebook/jest/pull/8206))
- `[@jest/test-result]` Create method to create empty `TestResult` ([#8867](https://github.com/facebook/jest/pull/8867))
- `[jest-worker]` [**BREAKING**] Return a promise from `end()`, resolving with the information whether workers exited gracefully ([#8206](https://github.com/facebook/jest/pull/8206))
Expand Down
12 changes: 6 additions & 6 deletions e2e/timer-reset-mocks/after-reset-all-mocks/timerAndMock.test.js
Expand Up @@ -9,17 +9,17 @@ describe('timers', () => {
it('should work before calling resetAllMocks', () => {
jest.useFakeTimers();
const f = jest.fn();
setImmediate(() => f());
jest.runAllImmediates();
expect(f.mock.calls.length).toBe(1);
setTimeout(f, 0);
jest.runAllTimers();
expect(f).toHaveBeenCalledTimes(1);
});

it('should not break after calling resetAllMocks', () => {
jest.resetAllMocks();
jest.useFakeTimers();
const f = jest.fn();
setImmediate(() => f());
jest.runAllImmediates();
expect(f.mock.calls.length).toBe(1);
setTimeout(f, 0);
jest.runAllTimers();
expect(f).toHaveBeenCalledTimes(1);
});
});
6 changes: 3 additions & 3 deletions e2e/timer-reset-mocks/with-reset-mocks/timerWithMock.test.js
Expand Up @@ -9,8 +9,8 @@ describe('timers', () => {
it('should work before calling resetAllMocks', () => {
const f = jest.fn();
jest.useFakeTimers();
setImmediate(() => f());
jest.runAllImmediates();
expect(f.mock.calls.length).toBe(1);
setTimeout(f, 0);
jest.runAllTimers();
expect(f).toHaveBeenCalledTimes(1);
});
});
9 changes: 5 additions & 4 deletions examples/timer/__tests__/infinite_timer_game.test.js
Expand Up @@ -5,15 +5,16 @@
jest.useFakeTimers();

it('schedules a 10-second timer after 1 second', () => {
jest.spyOn(global, 'setTimeout');
const infiniteTimerGame = require('../infiniteTimerGame');
const callback = jest.fn();

infiniteTimerGame(callback);

// At this point in time, there should have been a single call to
// setTimeout to schedule the end of the game in 1 second.
expect(setTimeout.mock.calls.length).toBe(1);
expect(setTimeout.mock.calls[0][1]).toBe(1000);
expect(setTimeout).toBeCalledTimes(1);
expect(setTimeout).toHaveBeenNthCalledWith(1, expect.any(Function), 1000);

// Fast forward and exhaust only currently pending timers
// (but not any new timers that get created during that process)
Expand All @@ -24,6 +25,6 @@ it('schedules a 10-second timer after 1 second', () => {

// And it should have created a new timer to start the game over in
// 10 seconds
expect(setTimeout.mock.calls.length).toBe(2);
expect(setTimeout.mock.calls[1][1]).toBe(10000);
expect(setTimeout).toBeCalledTimes(2);
expect(setTimeout).toHaveBeenNthCalledWith(2, expect.any(Function), 10000);
});
11 changes: 7 additions & 4 deletions examples/timer/__tests__/timer_game.test.js
Expand Up @@ -5,12 +5,15 @@
jest.useFakeTimers();

describe('timerGame', () => {
beforeEach(() => {
jest.spyOn(global, 'setTimeout');
});
it('waits 1 second before ending the game', () => {
const timerGame = require('../timerGame');
timerGame();

expect(setTimeout.mock.calls.length).toBe(1);
expect(setTimeout.mock.calls[0][1]).toBe(1000);
expect(setTimeout).toBeCalledTimes(1);
expect(setTimeout).toBeCalledWith(expect.any(Function), 1000);
});

it('calls the callback after 1 second via runAllTimers', () => {
Expand All @@ -27,7 +30,7 @@ describe('timerGame', () => {

// Now our callback should have been called!
expect(callback).toBeCalled();
expect(callback.mock.calls.length).toBe(1);
expect(callback).toBeCalledTimes(1);
});

it('calls the callback after 1 second via advanceTimersByTime', () => {
Expand All @@ -44,6 +47,6 @@ describe('timerGame', () => {

// Now our callback should have been called!
expect(callback).toBeCalled();
expect(callback.mock.calls.length).toBe(1);
expect(callback).toBeCalledTimes(1);
});
});
6 changes: 5 additions & 1 deletion packages/jest-fake-timers/package.json
Expand Up @@ -13,7 +13,11 @@
"@jest/types": "^24.9.0",
"jest-message-util": "^24.9.0",
"jest-mock": "^24.9.0",
"jest-util": "^24.9.0"
"jest-util": "^24.9.0",
"lolex": "^4.2.0"
},
"devDependencies": {
"@types/lolex": "^3.1.1"
},
"engines": {
"node": ">= 8"
Expand Down
154 changes: 154 additions & 0 deletions packages/jest-fake-timers/src/FakeTimersLolex.ts
@@ -0,0 +1,154 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {
InstalledClock,
LolexWithContext,
withGlobal as lolexWithGlobal,
} from 'lolex';
import {StackTraceConfig, formatStackTrace} from 'jest-message-util';

export default class FakeTimers {
private _clock!: InstalledClock;
private _config: StackTraceConfig;
private _fakingTime: boolean;
private _global: NodeJS.Global;
private _lolex: LolexWithContext;
private _maxLoops: number;

constructor({
global,
config,
maxLoops,
}: {
global: NodeJS.Global;
config: StackTraceConfig;
maxLoops?: number;
}) {
this._global = global;
this._config = config;
this._maxLoops = maxLoops || 100000;

this._fakingTime = false;
this._lolex = lolexWithGlobal(global);
}

clearAllTimers() {
if (this._fakingTime) {
this._clock.reset();
}
}

dispose() {
this.useRealTimers();
}

runAllTimers() {
if (this._checkFakeTimers()) {
this._clock.runAll();
}
}

runOnlyPendingTimers() {
if (this._checkFakeTimers()) {
this._clock.runToLast();
}
}

advanceTimersToNextTimer(steps = 1) {
if (this._checkFakeTimers()) {
for (let i = steps; i > 0; i--) {
this._clock.next();
// Fire all timers at this point: https://github.com/sinonjs/lolex/issues/250
this._clock.tick(0);

if (this._clock.countTimers() === 0) {
break;
}
}
}
}

advanceTimersByTime(msToRun: number) {
if (this._checkFakeTimers()) {
this._clock.tick(msToRun);
}
}

runAllTicks() {
if (this._checkFakeTimers()) {
// @ts-ignore
this._clock.runMicrotasks();
}
}

useRealTimers() {
if (this._fakingTime) {
this._clock.uninstall();
this._fakingTime = false;
}
}

useFakeTimers() {
const toFake = Object.keys(this._lolex.timers) as Array<
keyof LolexWithContext['timers']
>;

if (!this._fakingTime) {
this._clock = this._lolex.install({
loopLimit: this._maxLoops,
now: Date.now(),
target: this._global,
toFake,
});

this._fakingTime = true;
}
}

reset() {
if (this._checkFakeTimers()) {
const {now} = this._clock;
this._clock.reset();
this._clock.setSystemTime(now);
}
}

setSystemTime(now?: number) {
if (this._checkFakeTimers()) {
this._clock.setSystemTime(now);
}
}

getRealSystemTime() {
return Date.now();
}

getTimerCount() {
if (this._checkFakeTimers()) {
return this._clock.countTimers();
}

return 0;
}

private _checkFakeTimers() {
if (!this._fakingTime) {
this._global.console.warn(
'A function to advance timers was called but the timers API is not ' +
'mocked with fake timers. Call `jest.useFakeTimers()` in this test or ' +
'enable fake timers globally by setting `"timers": "fake"` in the ' +
'configuration file\nStack Trace:\n' +
formatStackTrace(new Error().stack!, this._config, {
noStackTrace: false,
}),
);
}

return this._fakingTime;
}
}
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`FakeTimers runAllTimers warns when trying to advance timers while real timers are used 1`] = `"A function to advance timers was called but the timers API is not mocked with fake timers. Call \`jest.useFakeTimers()\` in this test or enable fake timers globally by setting \`\\"timers\\": \\"fake\\"\` in the configuration file"`;

0 comments on commit 9279a3a

Please sign in to comment.