From b34d066268a726aa8655faa454136dbaaaf1967f Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 1 Feb 2019 16:41:06 +0100 Subject: [PATCH] add Lolex as alternate implementation of Fake Timers --- CHANGELOG.md | 1 + docs/Configuration.md | 2 + docs/JestObjectAPI.md | 18 +- e2e/__tests__/lolex.test.ts | 20 + e2e/lolex/from-config/__tests__/test.js | 18 + e2e/lolex/from-config/package.json | 6 + e2e/lolex/from-jest-object/__tests__/test.js | 20 + e2e/lolex/from-jest-object/package.json | 5 + .../timerAndMock.test.js | 12 +- .../with-reset-mocks/timerWithMock.test.js | 6 +- .../__tests__/infinite_timer_game.test.js | 9 +- examples/timer/__tests__/timer_game.test.js | 11 +- .../legacy-code-todo-rewrite/jestAdapter.ts | 4 + packages/jest-environment-jsdom/src/index.ts | 12 +- packages/jest-environment-node/src/index.ts | 9 +- packages/jest-environment/src/index.ts | 19 +- packages/jest-fake-timers/package.json | 6 +- .../jest-fake-timers/src/FakeTimersLolex.ts | 140 ++++ .../fakeTimersLolex.test.ts.snap | 3 + .../src/__tests__/fakeTimersLolex.test.ts | 735 ++++++++++++++++++ packages/jest-fake-timers/src/index.ts | 1 + .../src/__tests__/pTimeout.test.ts | 5 + packages/jest-jasmine2/src/index.ts | 4 + packages/jest-runtime/package.json | 1 + packages/jest-runtime/src/index.ts | 77 +- packages/jest-runtime/tsconfig.json | 1 + packages/jest-types/src/Config.ts | 2 +- yarn.lock | 10 + 28 files changed, 1119 insertions(+), 38 deletions(-) create mode 100644 e2e/__tests__/lolex.test.ts create mode 100644 e2e/lolex/from-config/__tests__/test.js create mode 100644 e2e/lolex/from-config/package.json create mode 100644 e2e/lolex/from-jest-object/__tests__/test.js create mode 100644 e2e/lolex/from-jest-object/package.json create mode 100644 packages/jest-fake-timers/src/FakeTimersLolex.ts create mode 100644 packages/jest-fake-timers/src/__tests__/__snapshots__/fakeTimersLolex.test.ts.snap create mode 100644 packages/jest-fake-timers/src/__tests__/fakeTimersLolex.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 83864ba1d072..399fbeeca872 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - `[expect]`: Improve report when matcher fails, part 13 ([#8077](https://github.com/facebook/jest/pull/8077)) - `[@jest/core]` Filter API pre-filter setup hook ([#8142](https://github.com/facebook/jest/pull/8142)) +- `[@jest/fake-timers]` Add possibility to use Lolex as implementation of fake timers ([#7776](https://github.com/facebook/jest/pull/7776)) - `[jest-snapshot]` Improve report when matcher fails, part 14 ([#8132](https://github.com/facebook/jest/pull/8132)) - `[@jest/reporter]` Display todo and skip test descriptions when verbose is true ([#8038](https://github.com/facebook/jest/pull/8038)) - `[jest-runner]` Support default exports for test environments ([#8163](https://github.com/facebook/jest/pull/8163)) diff --git a/docs/Configuration.md b/docs/Configuration.md index 894e840f4b83..8bb289c5d2c6 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1005,6 +1005,8 @@ Default: `real` Setting this value to `fake` allows the use of fake timers for functions such as `setTimeout`. Fake timers are useful when a piece of code sets a long timeout that we don't want to wait for in a test. +If the value is `lolex`, Lolex will be used as implementation instead of Jest's own. This will be the default fake implementation in a future major version of Jest. + ### `transform` [object] Default: `undefined` diff --git a/docs/JestObjectAPI.md b/docs/JestObjectAPI.md index 1a6cb5afe945..c6d944456e4d 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -501,10 +501,12 @@ Restores all mocks back to their original value. Equivalent to calling [`.mockRe ## Mock timers -### `jest.useFakeTimers()` +### `jest.useFakeTimers(string?)` Instructs Jest to use fake versions of the standard timer functions (`setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`, `nextTick`, `setImmediate` and `clearImmediate`). +If you pass `'lolex'` as argument, Lolex will be used as implementation instead of Jest's own fake timers. + Returns the `jest` object for chaining. ### `jest.useRealTimers()` @@ -531,6 +533,8 @@ This is often useful for synchronously executing setTimeouts during a test in or Exhausts all tasks queued by `setImmediate()`. +> Note: This function is not available when using Lolex as fake timers implementation + ### `jest.advanceTimersByTime(msToRun)` ##### renamed in Jest **22.0.0+** @@ -557,6 +561,18 @@ This means, if any timers have been scheduled (but have not yet executed), they Returns the number of fake timers still left to run. +### `.jest.setSystemTime()` + +Set the current system time used by fake timers. Simulates a user changing the system clock while your program is running. It affects the current time but it does not in itself cause e.g. timers to fire; they will fire exactly as they would have done without the call to `jest.setSystemTime()`. + +> Note: This function is only available when using Lolex as fake timers implementation + +### `.jest.getRealSystemTime()` + +When mocking time, `Date.now()` will also be mocked. If you for some reason need access to the real current time, you can invoke this function. + +> Note: This function is only available when using Lolex as fake timers implementation + ## Misc ### `jest.setTimeout(timeout)` diff --git a/e2e/__tests__/lolex.test.ts b/e2e/__tests__/lolex.test.ts new file mode 100644 index 000000000000..6a181bdf2f69 --- /dev/null +++ b/e2e/__tests__/lolex.test.ts @@ -0,0 +1,20 @@ +/** + * 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 runJest from '../runJest'; + +describe('Lolex as implementation fo fake timers', () => { + it('should be possible to use Lolex from config', () => { + const result = runJest('lolex/from-config'); + expect(result.status).toBe(0); + }); + + it('should be possible to use Lolex from jest-object', () => { + const result = runJest('lolex/from-jest-object'); + expect(result.status).toBe(0); + }); +}); diff --git a/e2e/lolex/from-config/__tests__/test.js b/e2e/lolex/from-config/__tests__/test.js new file mode 100644 index 000000000000..a32e5a5bc8e4 --- /dev/null +++ b/e2e/lolex/from-config/__tests__/test.js @@ -0,0 +1,18 @@ +/** + * 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. + */ + +'use strict'; + +test('fake timers', () => { + jest.setSystemTime(0); + + expect(Date.now()).toBe(0); + + jest.setSystemTime(1000); + + expect(Date.now()).toBe(1000); +}); diff --git a/e2e/lolex/from-config/package.json b/e2e/lolex/from-config/package.json new file mode 100644 index 000000000000..a08fb98aaf7c --- /dev/null +++ b/e2e/lolex/from-config/package.json @@ -0,0 +1,6 @@ +{ + "jest": { + "timers": "lolex", + "testEnvironment": "node" + } +} diff --git a/e2e/lolex/from-jest-object/__tests__/test.js b/e2e/lolex/from-jest-object/__tests__/test.js new file mode 100644 index 000000000000..d30756fe350d --- /dev/null +++ b/e2e/lolex/from-jest-object/__tests__/test.js @@ -0,0 +1,20 @@ +/** + * 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. + */ + +'use strict'; + +test('fake timers', () => { + jest.useFakeTimers('lolex'); + + jest.setSystemTime(0); + + expect(Date.now()).toBe(0); + + jest.setSystemTime(1000); + + expect(Date.now()).toBe(1000); +}); diff --git a/e2e/lolex/from-jest-object/package.json b/e2e/lolex/from-jest-object/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/e2e/lolex/from-jest-object/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} diff --git a/e2e/timer-reset-mocks/after-reset-all-mocks/timerAndMock.test.js b/e2e/timer-reset-mocks/after-reset-all-mocks/timerAndMock.test.js index c87e267c8e2e..143b0d77438f 100644 --- a/e2e/timer-reset-mocks/after-reset-all-mocks/timerAndMock.test.js +++ b/e2e/timer-reset-mocks/after-reset-all-mocks/timerAndMock.test.js @@ -4,17 +4,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); }); }); diff --git a/e2e/timer-reset-mocks/with-reset-mocks/timerWithMock.test.js b/e2e/timer-reset-mocks/with-reset-mocks/timerWithMock.test.js index e2f349869304..b9025af5d54d 100644 --- a/e2e/timer-reset-mocks/with-reset-mocks/timerWithMock.test.js +++ b/e2e/timer-reset-mocks/with-reset-mocks/timerWithMock.test.js @@ -4,8 +4,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); }); }); diff --git a/examples/timer/__tests__/infinite_timer_game.test.js b/examples/timer/__tests__/infinite_timer_game.test.js index e4e09d90fa54..d33d9b62eded 100644 --- a/examples/timer/__tests__/infinite_timer_game.test.js +++ b/examples/timer/__tests__/infinite_timer_game.test.js @@ -5,6 +5,7 @@ jest.useFakeTimers(); it('schedules a 10-second timer after 1 second', () => { + jest.spyOn(global, 'setTimeout'); const infiniteTimerGame = require('../infiniteTimerGame'); const callback = jest.fn(); @@ -12,8 +13,8 @@ it('schedules a 10-second timer after 1 second', () => { // 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) @@ -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); }); diff --git a/examples/timer/__tests__/timer_game.test.js b/examples/timer/__tests__/timer_game.test.js index 599d083c6fbc..c2f55ea24ecf 100644 --- a/examples/timer/__tests__/timer_game.test.js +++ b/examples/timer/__tests__/timer_game.test.js @@ -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', () => { @@ -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', () => { @@ -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); }); }); diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts index d255c9b4661c..b7b2c69c7f21 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts @@ -52,6 +52,10 @@ const jestAdapter = async ( environment.fakeTimers!.useFakeTimers(); } + if (config.timers === 'lolex') { + environment.fakeTimersLolex!.useFakeTimers(); + } + globals.beforeEach(() => { if (config.resetModules) { runtime.resetModules(); diff --git a/packages/jest-environment-jsdom/src/index.ts b/packages/jest-environment-jsdom/src/index.ts index 8d0c33c1df7c..387a1083029a 100644 --- a/packages/jest-environment-jsdom/src/index.ts +++ b/packages/jest-environment-jsdom/src/index.ts @@ -9,7 +9,7 @@ import {Script} from 'vm'; import {Global, Config} from '@jest/types'; import {installCommonGlobals} from 'jest-util'; import mock, {ModuleMocker} from 'jest-mock'; -import {JestFakeTimers as FakeTimers} from '@jest/fake-timers'; +import {JestFakeTimers as FakeTimers, LolexFakeTimers} from '@jest/fake-timers'; import {JestEnvironment, EnvironmentContext} from '@jest/environment'; import {JSDOM, VirtualConsole} from 'jsdom'; @@ -31,6 +31,7 @@ function isWin(globals: Win | Global.Global): globals is Win { class JSDOMEnvironment implements JestEnvironment { dom: JSDOM | null; fakeTimers: FakeTimers | null; + fakeTimersLolex: LolexFakeTimers | null; // @ts-ignore global: Global.Global | Win | null; errorEventListener: ((event: Event & {error: Error}) => void) | null; @@ -98,6 +99,11 @@ class JSDOMEnvironment implements JestEnvironment { moduleMocker: this.moduleMocker, timerConfig, }); + + this.fakeTimersLolex = new LolexFakeTimers({ + config, + global: (global as unknown) as NodeJS.Global, + }); } setup() { @@ -108,6 +114,9 @@ class JSDOMEnvironment implements JestEnvironment { if (this.fakeTimers) { this.fakeTimers.dispose(); } + if (this.fakeTimersLolex) { + this.fakeTimersLolex.dispose(); + } if (this.global) { if (this.errorEventListener && isWin(this.global)) { this.global.removeEventListener('error', this.errorEventListener); @@ -122,6 +131,7 @@ class JSDOMEnvironment implements JestEnvironment { this.global = null; this.dom = null; this.fakeTimers = null; + this.fakeTimersLolex = null; return Promise.resolve(); } diff --git a/packages/jest-environment-node/src/index.ts b/packages/jest-environment-node/src/index.ts index bc82af123b5e..e404343b19da 100644 --- a/packages/jest-environment-node/src/index.ts +++ b/packages/jest-environment-node/src/index.ts @@ -9,7 +9,7 @@ import vm, {Script, Context} from 'vm'; import {Global, Config} from '@jest/types'; import {ModuleMocker} from 'jest-mock'; import {installCommonGlobals} from 'jest-util'; -import {JestFakeTimers as FakeTimers} from '@jest/fake-timers'; +import {JestFakeTimers as FakeTimers, LolexFakeTimers} from '@jest/fake-timers'; import {JestEnvironment} from '@jest/environment'; type Timer = { @@ -21,6 +21,7 @@ type Timer = { class NodeEnvironment implements JestEnvironment { context: Context | null; fakeTimers: FakeTimers | null; + fakeTimersLolex: LolexFakeTimers | null; global: Global.Global; moduleMocker: ModuleMocker | null; @@ -78,6 +79,8 @@ class NodeEnvironment implements JestEnvironment { moduleMocker: this.moduleMocker, timerConfig, }); + + this.fakeTimersLolex = new LolexFakeTimers({config, global}); } setup() { @@ -88,8 +91,12 @@ class NodeEnvironment implements JestEnvironment { if (this.fakeTimers) { this.fakeTimers.dispose(); } + if (this.fakeTimersLolex) { + this.fakeTimersLolex.dispose(); + } this.context = null; this.fakeTimers = null; + this.fakeTimersLolex = null; return Promise.resolve(); } diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index f1b262c6ce98..6d12421c3ac0 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -9,7 +9,7 @@ import {Script} from 'vm'; import {Config, Global} from '@jest/types'; import jestMock, {ModuleMocker} from 'jest-mock'; import {ScriptTransformer} from '@jest/transform'; -import {JestFakeTimers as FakeTimers} from '@jest/fake-timers'; +import {JestFakeTimers as FakeTimers, LolexFakeTimers} from '@jest/fake-timers'; type JestMockFn = typeof jestMock.fn; type JestMockSpyOn = typeof jestMock.spyOn; @@ -27,6 +27,7 @@ export declare class JestEnvironment { constructor(config: Config.ProjectConfig, context: EnvironmentContext); global: Global.Global; fakeTimers: FakeTimers | null; + fakeTimersLolex: LolexFakeTimers | null; moduleMocker: ModuleMocker | null; runScript( script: Script, @@ -164,6 +165,8 @@ export interface Jest { retryTimes(numRetries: number): Jest; /** * Exhausts tasks queued by setImmediate(). + * + * > Note: This function is not available when using Lolex as fake timers implementation */ runAllImmediates(): void; /** @@ -246,4 +249,18 @@ export interface Jest { * every test so that local module state doesn't conflict between tests. */ isolateModules(fn: () => void): Jest; + + /** + * When mocking time, `Date.now()` will also be mocked. If you for some reason need access to the real current time, you can invoke this function. + * + * > Note: This function is only available when using Lolex as fake timers implementation + */ + getRealSystemTime(): number; + + /** + * Set the current system time used by fake timers. Simulates a user changing the system clock while your program is running. It affects the current time but it does not in itself cause e.g. timers to fire; they will fire exactly as they would have done without the call to `jest.setSystemTime()`. + * + * > Note: This function is only available when using Lolex as fake timers implementation + */ + setSystemTime(now?: number): void; } diff --git a/packages/jest-fake-timers/package.json b/packages/jest-fake-timers/package.json index d575484092d7..485196c3216d 100644 --- a/packages/jest-fake-timers/package.json +++ b/packages/jest-fake-timers/package.json @@ -12,7 +12,11 @@ "dependencies": { "@jest/types": "^24.5.0", "jest-message-util": "^24.5.0", - "jest-mock": "^24.5.0" + "jest-mock": "^24.5.0", + "lolex": "3.0.0" + }, + "devDependencies": { + "@types/lolex": "^3.1.1" }, "engines": { "node": ">= 6" diff --git a/packages/jest-fake-timers/src/FakeTimersLolex.ts b/packages/jest-fake-timers/src/FakeTimersLolex.ts new file mode 100644 index 000000000000..084c9d710578 --- /dev/null +++ b/packages/jest-fake-timers/src/FakeTimersLolex.ts @@ -0,0 +1,140 @@ +/** + * 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 { + withGlobal as lolexWithGlobal, + LolexWithContext, + InstalledClock, +} from 'lolex'; +import {formatStackTrace, StackTraceConfig} 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(); + } + } + + 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; + } + + _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; + } +} diff --git a/packages/jest-fake-timers/src/__tests__/__snapshots__/fakeTimersLolex.test.ts.snap b/packages/jest-fake-timers/src/__tests__/__snapshots__/fakeTimersLolex.test.ts.snap new file mode 100644 index 000000000000..39cc8dd4ad64 --- /dev/null +++ b/packages/jest-fake-timers/src/__tests__/__snapshots__/fakeTimersLolex.test.ts.snap @@ -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"`; diff --git a/packages/jest-fake-timers/src/__tests__/fakeTimersLolex.test.ts b/packages/jest-fake-timers/src/__tests__/fakeTimersLolex.test.ts new file mode 100644 index 000000000000..2f01748979f8 --- /dev/null +++ b/packages/jest-fake-timers/src/__tests__/fakeTimersLolex.test.ts @@ -0,0 +1,735 @@ +/** + * 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 FakeTimers from '../FakeTimersLolex'; + +describe('FakeTimers', () => { + describe('construction', () => { + it('installs setTimeout mock', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + expect(global.setTimeout).not.toBe(undefined); + }); + + it('installs clearTimeout mock', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + expect(global.clearTimeout).not.toBe(undefined); + }); + + it('installs setInterval mock', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + expect(global.setInterval).not.toBe(undefined); + }); + + it('installs clearInterval mock', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + expect(global.clearInterval).not.toBe(undefined); + }); + + it('mocks process.nextTick if it exists on global', () => { + const origNextTick = () => {}; + const global = { + Date, + clearTimeout, + process: { + nextTick: origNextTick, + }, + setTimeout, + }; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + expect(global.process.nextTick).not.toBe(origNextTick); + }); + + it('mocks setImmediate if it exists on global', () => { + const origSetImmediate = () => {}; + const global = { + Date, + clearTimeout, + process, + setImmediate: origSetImmediate, + setTimeout, + }; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + expect(global.setImmediate).not.toBe(origSetImmediate); + }); + + it('mocks clearImmediate if setImmediate is on global', () => { + const origSetImmediate = () => {}; + const origClearImmediate = () => {}; + const global = { + Date, + clearImmediate: origClearImmediate, + clearTimeout, + process, + setImmediate: origSetImmediate, + setTimeout, + }; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + expect(global.clearImmediate).not.toBe(origClearImmediate); + }); + }); + + describe('runAllTicks', () => { + it('runs all ticks, in order', () => { + const global = { + Date, + clearTimeout, + process: { + nextTick: () => {}, + }, + setTimeout, + }; + + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + const runOrder = []; + const mock1 = jest.fn(() => runOrder.push('mock1')); + const mock2 = jest.fn(() => runOrder.push('mock2')); + + global.process.nextTick(mock1); + global.process.nextTick(mock2); + + expect(mock1).toHaveBeenCalledTimes(0); + expect(mock2).toHaveBeenCalledTimes(0); + + timers.runAllTicks(); + + expect(mock1).toHaveBeenCalledTimes(1); + expect(mock2).toHaveBeenCalledTimes(1); + expect(runOrder).toEqual(['mock1', 'mock2']); + }); + + it('does nothing when no ticks have been scheduled', () => { + const nextTick = jest.fn(); + const global = { + Date, + clearTimeout, + process: { + nextTick, + }, + setTimeout, + }; + + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + timers.runAllTicks(); + + expect(nextTick).toHaveBeenCalledTimes(0); + }); + + it('only runs a scheduled callback once', () => { + const global = { + Date, + clearTimeout, + process: { + nextTick: () => {}, + }, + setTimeout, + }; + + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + const mock1 = jest.fn(); + global.process.nextTick(mock1); + expect(mock1).toHaveBeenCalledTimes(0); + + timers.runAllTicks(); + expect(mock1).toHaveBeenCalledTimes(1); + + timers.runAllTicks(); + expect(mock1).toHaveBeenCalledTimes(1); + }); + + it('throws before allowing infinite recursion', () => { + const global = { + Date, + clearTimeout, + process: { + nextTick: () => {}, + }, + setTimeout, + }; + + const timers = new FakeTimers({global, maxLoops: 100}); + + timers.useFakeTimers(); + + global.process.nextTick(function infinitelyRecursingCallback() { + global.process.nextTick(infinitelyRecursingCallback); + }); + + expect(() => { + timers.runAllTicks(); + }).toThrow( + 'Aborting after running 100 timers, assuming an infinite loop!', + ); + }); + }); + + describe('runAllTimers', () => { + it('runs all timers in order', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + const runOrder = []; + const mock1 = jest.fn(() => runOrder.push('mock1')); + const mock2 = jest.fn(() => runOrder.push('mock2')); + const mock3 = jest.fn(() => runOrder.push('mock3')); + const mock4 = jest.fn(() => runOrder.push('mock4')); + const mock5 = jest.fn(() => runOrder.push('mock5')); + const mock6 = jest.fn(() => runOrder.push('mock6')); + + global.setTimeout(mock1, 100); + global.setTimeout(mock2, NaN); + global.setTimeout(mock3, 0); + const intervalHandler = global.setInterval(() => { + mock4(); + global.clearInterval(intervalHandler); + }, 200); + global.setTimeout(mock5, Infinity); + global.setTimeout(mock6, -Infinity); + + timers.runAllTimers(); + expect(runOrder).toEqual([ + 'mock2', + 'mock3', + 'mock5', + 'mock6', + 'mock1', + 'mock4', + ]); + }); + + it('warns when trying to advance timers while real timers are used', () => { + const consoleWarn = console.warn; + console.warn = jest.fn(); + const timers = new FakeTimers({ + config: { + rootDir: __dirname, + }, + global, + }); + timers.runAllTimers(); + expect( + console.warn.mock.calls[0][0].split('\nStack Trace')[0], + ).toMatchSnapshot(); + console.warn = consoleWarn; + }); + + it('does nothing when no timers have been scheduled', () => { + const nativeSetTimeout = jest.fn(); + const global = { + Date, + clearTimeout, + process, + setTimeout: nativeSetTimeout, + }; + + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + timers.runAllTimers(); + }); + + it('only runs a setTimeout callback once (ever)', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + const fn = jest.fn(); + global.setTimeout(fn, 0); + expect(fn).toHaveBeenCalledTimes(0); + + timers.runAllTimers(); + expect(fn).toHaveBeenCalledTimes(1); + + timers.runAllTimers(); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('runs callbacks with arguments after the interval', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + const fn = jest.fn(); + global.setTimeout(fn, 0, 'mockArg1', 'mockArg2'); + + timers.runAllTimers(); + expect(fn).toHaveBeenCalledTimes(1); + expect(fn).toHaveBeenCalledWith('mockArg1', 'mockArg2'); + }); + + it("doesn't pass the callback to native setTimeout", () => { + const nativeSetTimeout = jest.fn(); + + const global = { + Date, + clearTimeout, + process, + setTimeout: nativeSetTimeout, + }; + + const timers = new FakeTimers({global}); + // Lolex uses `setTimeout` during init to figure out if it's in Node or + // browser env. So clear its calls before we install them into the env + nativeSetTimeout.mockClear(); + timers.useFakeTimers(); + + const mock1 = jest.fn(); + global.setTimeout(mock1, 0); + + timers.runAllTimers(); + expect(mock1).toHaveBeenCalledTimes(1); + expect(nativeSetTimeout).toHaveBeenCalledTimes(0); + }); + + it('throws before allowing infinite recursion', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global, maxLoops: 100}); + timers.useFakeTimers(); + + global.setTimeout(function infinitelyRecursingCallback() { + global.setTimeout(infinitelyRecursingCallback, 0); + }, 0); + + expect(() => { + timers.runAllTimers(); + }).toThrow( + new Error( + 'Aborting after running 100 timers, assuming an infinite loop!', + ), + ); + }); + + it('also clears ticks', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + const fn = jest.fn(); + global.setTimeout(() => { + process.nextTick(fn); + }, 0); + expect(fn).toHaveBeenCalledTimes(0); + + timers.runAllTimers(); + expect(fn).toHaveBeenCalledTimes(1); + }); + }); + + describe('advanceTimersByTime', () => { + it('runs timers in order', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + const runOrder = []; + const mock1 = jest.fn(() => runOrder.push('mock1')); + const mock2 = jest.fn(() => runOrder.push('mock2')); + const mock3 = jest.fn(() => runOrder.push('mock3')); + const mock4 = jest.fn(() => runOrder.push('mock4')); + + global.setTimeout(mock1, 100); + global.setTimeout(mock2, 0); + global.setTimeout(mock3, 0); + global.setInterval(() => { + mock4(); + }, 200); + + // Move forward to t=50 + timers.advanceTimersByTime(50); + expect(runOrder).toEqual(['mock2', 'mock3']); + + // Move forward to t=60 + timers.advanceTimersByTime(10); + expect(runOrder).toEqual(['mock2', 'mock3']); + + // Move forward to t=100 + timers.advanceTimersByTime(40); + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1']); + + // Move forward to t=200 + timers.advanceTimersByTime(100); + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4']); + + // Move forward to t=400 + timers.advanceTimersByTime(200); + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4', 'mock4']); + }); + + it('does nothing when no timers have been scheduled', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + timers.advanceTimersByTime(100); + }); + }); + + describe('reset', () => { + it('resets all pending setTimeouts', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + const mock1 = jest.fn(); + global.setTimeout(mock1, 100); + + timers.reset(); + timers.runAllTimers(); + expect(mock1).toHaveBeenCalledTimes(0); + }); + + it('resets all pending setIntervals', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + const mock1 = jest.fn(); + global.setInterval(mock1, 200); + + timers.reset(); + timers.runAllTimers(); + expect(mock1).toHaveBeenCalledTimes(0); + }); + + it('resets all pending ticks callbacks', () => { + const global = { + Date, + clearTimeout, + process: { + nextTick: () => {}, + }, + setImmediate: () => {}, + setTimeout, + }; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + const mock1 = jest.fn(); + global.process.nextTick(mock1); + global.setImmediate(mock1); + + timers.reset(); + timers.runAllTicks(); + expect(mock1).toHaveBeenCalledTimes(0); + }); + + it('resets current advanceTimersByTime time cursor', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + const mock1 = jest.fn(); + global.setTimeout(mock1, 100); + timers.advanceTimersByTime(50); + + timers.reset(); + global.setTimeout(mock1, 100); + + timers.advanceTimersByTime(50); + expect(mock1).toHaveBeenCalledTimes(0); + }); + }); + + describe('runOnlyPendingTimers', () => { + it('runs all timers in order', () => { + const nativeSetImmediate = jest.fn(); + + const global = { + Date, + clearTimeout, + process, + setImmediate: nativeSetImmediate, + setTimeout, + }; + + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + const runOrder = []; + + global.setTimeout(function cb() { + runOrder.push('mock1'); + global.setTimeout(cb, 100); + }, 100); + + global.setTimeout(function cb() { + runOrder.push('mock2'); + global.setTimeout(cb, 50); + }, 0); + + global.setInterval(() => { + runOrder.push('mock3'); + }, 200); + + global.setImmediate(() => { + runOrder.push('mock4'); + }); + + global.setImmediate(function cb() { + runOrder.push('mock5'); + global.setTimeout(cb, 400); + }); + + timers.runOnlyPendingTimers(); + const firsRunOrder = [ + 'mock4', + 'mock5', + 'mock2', + 'mock2', + 'mock1', + 'mock2', + 'mock2', + 'mock3', + 'mock1', + 'mock2', + ]; + + expect(runOrder).toEqual(firsRunOrder); + + timers.runOnlyPendingTimers(); + expect(runOrder).toEqual([ + ...firsRunOrder, + 'mock2', + 'mock1', + 'mock2', + 'mock2', + 'mock3', + 'mock5', + 'mock1', + 'mock2', + ]); + }); + + it('does not run timers that were cleared in another timer', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + const fn = jest.fn(); + const timer = global.setTimeout(fn, 10); + global.setTimeout(() => { + global.clearTimeout(timer); + }, 0); + + timers.runOnlyPendingTimers(); + expect(fn).not.toBeCalled(); + }); + }); + + describe('useRealTimers', () => { + it('resets native timer APIs', () => { + const nativeSetTimeout = jest.fn(); + const nativeSetInterval = jest.fn(); + const nativeClearTimeout = jest.fn(); + const nativeClearInterval = jest.fn(); + + const global = { + Date, + clearInterval: nativeClearInterval, + clearTimeout: nativeClearTimeout, + process, + setInterval: nativeSetInterval, + setTimeout: nativeSetTimeout, + }; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + // Ensure that timers has overridden the native timer APIs + // (because if it didn't, this test might pass when it shouldn't) + expect(global.setTimeout).not.toBe(nativeSetTimeout); + expect(global.setInterval).not.toBe(nativeSetInterval); + expect(global.clearTimeout).not.toBe(nativeClearTimeout); + expect(global.clearInterval).not.toBe(nativeClearInterval); + + timers.useRealTimers(); + + expect(global.setTimeout).toBe(nativeSetTimeout); + expect(global.setInterval).toBe(nativeSetInterval); + expect(global.clearTimeout).toBe(nativeClearTimeout); + expect(global.clearInterval).toBe(nativeClearInterval); + }); + + it('resets native process.nextTick when present', () => { + const nativeProcessNextTick = jest.fn(); + + const global = { + Date, + clearTimeout, + process: {nextTick: nativeProcessNextTick}, + setTimeout, + }; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + // Ensure that timers has overridden the native timer APIs + // (because if it didn't, this test might pass when it shouldn't) + expect(global.process.nextTick).not.toBe(nativeProcessNextTick); + + timers.useRealTimers(); + + expect(global.process.nextTick).toBe(nativeProcessNextTick); + }); + + it('resets native setImmediate when present', () => { + const nativeSetImmediate = jest.fn(); + const nativeClearImmediate = jest.fn(); + + const global = { + Date, + clearImmediate: nativeClearImmediate, + clearTimeout, + process, + setImmediate: nativeSetImmediate, + setTimeout, + }; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + // Ensure that timers has overridden the native timer APIs + // (because if it didn't, this test might pass when it shouldn't) + expect(global.setImmediate).not.toBe(nativeSetImmediate); + expect(global.clearImmediate).not.toBe(nativeClearImmediate); + + timers.useRealTimers(); + + expect(global.setImmediate).toBe(nativeSetImmediate); + expect(global.clearImmediate).toBe(nativeClearImmediate); + }); + }); + + describe('useFakeTimers', () => { + it('resets mock timer APIs', () => { + const nativeSetTimeout = jest.fn(); + const nativeSetInterval = jest.fn(); + const nativeClearTimeout = jest.fn(); + const nativeClearInterval = jest.fn(); + + const global = { + Date, + clearInterval: nativeClearInterval, + clearTimeout: nativeClearTimeout, + process, + setInterval: nativeSetInterval, + setTimeout: nativeSetTimeout, + }; + const timers = new FakeTimers({global}); + timers.useRealTimers(); + + // Ensure that the real timers are installed at this point + // (because if they aren't, this test might pass when it shouldn't) + expect(global.setTimeout).toBe(nativeSetTimeout); + expect(global.setInterval).toBe(nativeSetInterval); + expect(global.clearTimeout).toBe(nativeClearTimeout); + expect(global.clearInterval).toBe(nativeClearInterval); + + timers.useFakeTimers(); + + expect(global.setTimeout).not.toBe(nativeSetTimeout); + expect(global.setInterval).not.toBe(nativeSetInterval); + expect(global.clearTimeout).not.toBe(nativeClearTimeout); + expect(global.clearInterval).not.toBe(nativeClearInterval); + }); + + it('resets mock process.nextTick when present', () => { + const nativeProcessNextTick = jest.fn(); + + const global = { + Date, + clearTimeout, + process: {nextTick: nativeProcessNextTick}, + setTimeout, + }; + const timers = new FakeTimers({global}); + timers.useRealTimers(); + + // Ensure that the real timers are installed at this point + // (because if they aren't, this test might pass when it shouldn't) + expect(global.process.nextTick).toBe(nativeProcessNextTick); + + timers.useFakeTimers(); + + expect(global.process.nextTick).not.toBe(nativeProcessNextTick); + }); + + it('resets mock setImmediate when present', () => { + const nativeSetImmediate = jest.fn(); + const nativeClearImmediate = jest.fn(); + + const global = { + Date, + clearImmediate: nativeClearImmediate, + clearTimeout, + process, + setImmediate: nativeSetImmediate, + setTimeout, + }; + const fakeTimers = new FakeTimers({global}); + fakeTimers.useRealTimers(); + + // Ensure that the real timers are installed at this point + // (because if they aren't, this test might pass when it shouldn't) + expect(global.setImmediate).toBe(nativeSetImmediate); + expect(global.clearImmediate).toBe(nativeClearImmediate); + + fakeTimers.useFakeTimers(); + + expect(global.setImmediate).not.toBe(nativeSetImmediate); + expect(global.clearImmediate).not.toBe(nativeClearImmediate); + }); + }); + + describe('getTimerCount', () => { + it('returns the correct count', () => { + const timers = new FakeTimers({global}); + + timers.useFakeTimers(); + + global.setTimeout(() => {}, 0); + global.setTimeout(() => {}, 0); + global.setTimeout(() => {}, 10); + + expect(timers.getTimerCount()).toEqual(3); + + timers.advanceTimersByTime(5); + + expect(timers.getTimerCount()).toEqual(1); + + timers.advanceTimersByTime(5); + + expect(timers.getTimerCount()).toEqual(0); + }); + + // Lolex does not consider microticks timers. That should be fixed + it.skip('includes immediates and ticks', () => { + const timers = new FakeTimers({global}); + + timers.useFakeTimers(); + + global.setTimeout(() => {}, 0); + global.setImmediate(() => {}); + process.nextTick(() => {}); + + expect(timers.getTimerCount()).toEqual(3); + }); + }); +}); diff --git a/packages/jest-fake-timers/src/index.ts b/packages/jest-fake-timers/src/index.ts index fe0d39d5b62a..295edc2bee29 100644 --- a/packages/jest-fake-timers/src/index.ts +++ b/packages/jest-fake-timers/src/index.ts @@ -6,3 +6,4 @@ */ export {default as JestFakeTimers} from './jestFakeTimers'; +export {default as LolexFakeTimers} from './FakeTimersLolex'; diff --git a/packages/jest-jasmine2/src/__tests__/pTimeout.test.ts b/packages/jest-jasmine2/src/__tests__/pTimeout.test.ts index fef80a2bd541..9907d1f5945f 100644 --- a/packages/jest-jasmine2/src/__tests__/pTimeout.test.ts +++ b/packages/jest-jasmine2/src/__tests__/pTimeout.test.ts @@ -11,6 +11,11 @@ jest.useFakeTimers(); import pTimeout from '../pTimeout'; describe('pTimeout', () => { + beforeEach(() => { + jest.spyOn(global, 'setTimeout'); + jest.spyOn(global, 'clearTimeout'); + }); + it('calls `clearTimeout` and resolves when `promise` resolves.', async () => { const onTimeout = jest.fn(); const promise = Promise.resolve(); diff --git a/packages/jest-jasmine2/src/index.ts b/packages/jest-jasmine2/src/index.ts index d1bce2deaac0..17b53a83aa4d 100644 --- a/packages/jest-jasmine2/src/index.ts +++ b/packages/jest-jasmine2/src/index.ts @@ -94,6 +94,10 @@ async function jasmine2( environment.fakeTimers!.useFakeTimers(); } + if (config.timers === 'lolex') { + environment.fakeTimersLolex!.useFakeTimers(); + } + env.beforeEach(() => { if (config.resetModules) { runtime.resetModules(); diff --git a/packages/jest-runtime/package.json b/packages/jest-runtime/package.json index 46cade009588..b7b9ce70662b 100644 --- a/packages/jest-runtime/package.json +++ b/packages/jest-runtime/package.json @@ -12,6 +12,7 @@ "dependencies": { "@jest/console": "^24.3.0", "@jest/environment": "^24.5.0", + "@jest/fake-timers": "^24.5.0", "@jest/source-map": "^24.3.0", "@jest/transform": "^24.5.0", "@jest/types": "^24.5.0", diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 067c247fdaf9..24088244e6e5 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -14,6 +14,7 @@ import { Module, } from '@jest/environment'; import {SourceMapRegistry} from '@jest/source-map'; +import {JestFakeTimers, LolexFakeTimers} from '@jest/fake-timers'; import jestMock, {MockFunctionMetadata} from 'jest-mock'; import HasteMap, {ModuleMap} from 'jest-haste-map'; import {formatStackTrace, separateMessageFromStack} from 'jest-message-util'; @@ -87,6 +88,10 @@ class Runtime { private _currentlyExecutingModulePath: string; private _environment: JestEnvironment; private _explicitShouldMock: BooleanObject; + private _fakeTimersImplementation: + | JestFakeTimers + | LolexFakeTimers + | null; private _internalModuleRegistry: ModuleRegistry; private _isCurrentlyExecutingManualMock: string | null; private _mockFactories: {[key: string]: () => unknown}; @@ -148,6 +153,11 @@ class Runtime { this._shouldUnmockTransitiveDependenciesCache = Object.create(null); this._transitiveShouldMock = Object.create(null); + this._fakeTimersImplementation = + config.timers === 'lolex' + ? this._environment.fakeTimersLolex + : this._environment.fakeTimers; + this._unmockList = unmockRegExpCache.get(config); if (!this._unmockList && config.unmockedModulePathPatterns) { this._unmockList = new RegExp( @@ -964,8 +974,25 @@ class Runtime { this.restoreAllMocks(); return jestObject; }; - const useFakeTimers = () => { - _getFakeTimers().useFakeTimers(); + const _getFakeTimers = () => { + if ( + !(this._environment.fakeTimers || this._environment.fakeTimersLolex) + ) { + this._logFormattedReferenceError( + 'You are trying to access a property or method of the Jest environment after it has been torn down.', + ); + process.exitCode = 1; + } + + return this._fakeTimersImplementation!; + }; + const useFakeTimers = (type: string = 'default') => { + if (type === 'lolex') { + this._fakeTimersImplementation = this._environment.fakeTimersLolex; + } else { + this._fakeTimersImplementation = this._environment.fakeTimers; + } + this._fakeTimersImplementation!.useFakeTimers(); return jestObject; }; const useRealTimers = () => { @@ -999,18 +1026,6 @@ class Runtime { return jestObject; }; - const _getFakeTimers = (): NonNullable => { - if (!this._environment.fakeTimers) { - this._logFormattedReferenceError( - 'You are trying to access a property or method of the Jest environment after it has been torn down.', - ); - process.exitCode = 1; - } - - // We've logged a user message above, so it doesn't matter if we return `null` here - return this._environment.fakeTimers!; - }; - const jestObject: Jest = { addMatchers: (matchers: Record) => this._environment.global.jasmine.addMatchers(matchers), @@ -1028,6 +1043,17 @@ class Runtime { fn, genMockFromModule: (moduleName: string) => this._generateMock(from, moduleName), + getRealSystemTime: () => { + const fakeTimers = _getFakeTimers(); + + if (fakeTimers instanceof LolexFakeTimers) { + return fakeTimers.getRealSystemTime(); + } else { + throw new TypeError( + 'getRealSystemTime is not available when not using Lolex', + ); + } + }, getTimerCount: () => _getFakeTimers().getTimerCount(), isMockFunction: this._moduleMocker.isMockFunction, isolateModules, @@ -1039,7 +1065,17 @@ class Runtime { resetModules, restoreAllMocks, retryTimes, - runAllImmediates: () => _getFakeTimers().runAllImmediates(), + runAllImmediates: () => { + const fakeTimers = _getFakeTimers(); + + if (fakeTimers instanceof JestFakeTimers) { + fakeTimers.runAllImmediates(); + } else { + throw new TypeError( + 'runAllImmediates is not available when using Lolex', + ); + } + }, runAllTicks: () => _getFakeTimers().runAllTicks(), runAllTimers: () => _getFakeTimers().runAllTimers(), runOnlyPendingTimers: () => _getFakeTimers().runOnlyPendingTimers(), @@ -1047,6 +1083,17 @@ class Runtime { _getFakeTimers().advanceTimersByTime(msToRun), setMock: (moduleName: string, mock: unknown) => setMockFactory(moduleName, () => mock), + setSystemTime: (now?: number) => { + const fakeTimers = _getFakeTimers(); + + if (fakeTimers instanceof LolexFakeTimers) { + fakeTimers.setSystemTime(now); + } else { + throw new TypeError( + 'setSystemTime is not available when not using Lolex', + ); + } + }, setTimeout, spyOn, unmock, diff --git a/packages/jest-runtime/tsconfig.json b/packages/jest-runtime/tsconfig.json index 2892d4eb078d..cddd8ed9144b 100644 --- a/packages/jest-runtime/tsconfig.json +++ b/packages/jest-runtime/tsconfig.json @@ -9,6 +9,7 @@ {"path": "../jest-console"}, {"path": "../jest-environment"}, {"path": "../jest-environment-node"}, + {"path": "../jest-fake-timers"}, {"path": "../jest-haste-map"}, {"path": "../jest-message-util"}, {"path": "../jest-mock"}, diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index 76e4d06ef343..2d6be8b463c0 100644 --- a/packages/jest-types/src/Config.ts +++ b/packages/jest-types/src/Config.ts @@ -357,7 +357,7 @@ export type ProjectConfig = { testRegex: Array; testRunner: string; testURL: string; - timers: 'real' | 'fake'; + timers: 'real' | 'fake' | 'lolex'; transform: Array<[string, Path]>; transformIgnorePatterns: Array; watchPathIgnorePatterns: Array; diff --git a/yarn.lock b/yarn.lock index 93b6c8ef0001..2c01c2446881 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1770,6 +1770,11 @@ resolved "https://registry.yarnpkg.com/@types/leven/-/leven-2.1.1.tgz#7cdc02ec636f80dc0bb0a53d8ee7eff2d8e8e1d8" integrity sha512-f74SsCQnQzm244o5LHZgSLijrwG5e9BgkMHGbDlQThfh42q5RG4c+RNzUvZ347wAlQYD9kwu64qSNylxZdKs6w== +"@types/lolex@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/lolex/-/lolex-3.1.1.tgz#d40895223e5c8f8aa64f5500c6ca4eeab067d432" + integrity sha512-NU2qVtKxbt4IBvjEOW1QeUnV6KGUF6hpgJyvwZt3JrXe2qmwQF0+BiazQw+iFy9qL5ie+QHOxTzXkcvJUEh76g== + "@types/merge-stream@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@types/merge-stream/-/merge-stream-1.1.2.tgz#a880ff66b1fbbb5eef4958d015c5947a9334dbb1" @@ -8333,6 +8338,11 @@ logalot@^2.0.0: figures "^1.3.5" squeak "^1.0.0" +lolex@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-3.0.0.tgz#f04ee1a8aa13f60f1abd7b0e8f4213ec72ec193e" + integrity sha512-hcnW80h3j2lbUfFdMArd5UPA/vxZJ+G8vobd+wg3nVEQA0EigStbYcrG030FJxL6xiDDPEkoMatV9xIh5OecQQ== + longest@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"