Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add fake timers implementation backed by Lolex #8897

Merged
merged 3 commits into from
Sep 6, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- `[jest-diff]` Add options for colors and symbols ([#8841](https://github.com/facebook/jest/pull/8841))
- `[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/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
Original file line number Diff line number Diff line change
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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all of these changes does not strictly have to be here, but it's needed when we actually start using Lolex. Why not land the changes now 🤷‍♂

The reason for this change is that runAllImmediates is not supported in the Lolex implementation

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
jest.useFakeTimers();

it('schedules a 10-second timer after 1 second', () => {
jest.spyOn(global, 'setTimeout');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for this change is that the fake timer functions in current implementation are mock functions, a feature I've purposefully not implemented in the Lolex version.

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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"`;
Loading