From c202e0d107419dabaebd531ed85bb8f023730dcb Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 24 Dec 2017 16:48:13 +0100 Subject: [PATCH] feat(jest-util): use lolex as implementation for fake timers --- CHANGELOG.md | 1 + docs/JestObjectAPI.md | 12 +- e2e/__tests__/fakePromises.test.js | 9 +- .../{asap => }/__tests__/generator.test.js | 0 e2e/fake-promises/{asap => }/fake-promises.js | 0 .../immediate/__tests__/generator.test.js | 19 - e2e/fake-promises/immediate/fake-promises.js | 10 - e2e/fake-promises/immediate/package.json | 9 - e2e/fake-promises/{asap => }/package.json | 0 .../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 +- flow-typed/npm/jest_v23.x.x.js | 8 +- flow-typed/npm/lolex_v2.x.x.js | 50 ++ packages/jest-environment-jsdom/src/index.js | 9 +- packages/jest-environment-node/src/index.js | 21 +- .../src/__tests__/pTimeout.test.js | 5 + packages/jest-runtime/src/index.js | 3 +- packages/jest-util/package.json | 1 + packages/jest-util/src/FakeTimers.js | 518 ++---------------- .../__snapshots__/fakeTimers.test.js.snap | 6 +- .../src/__tests__/fakeTimers.test.js | 481 ++++------------ types/Environment.js | 3 +- types/Jest.js | 3 +- yarn.lock | 5 + 26 files changed, 283 insertions(+), 928 deletions(-) rename e2e/fake-promises/{asap => }/__tests__/generator.test.js (100%) rename e2e/fake-promises/{asap => }/fake-promises.js (100%) delete mode 100644 e2e/fake-promises/immediate/__tests__/generator.test.js delete mode 100644 e2e/fake-promises/immediate/fake-promises.js delete mode 100644 e2e/fake-promises/immediate/package.json rename e2e/fake-promises/{asap => }/package.json (100%) create mode 100644 flow-typed/npm/lolex_v2.x.x.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 815f452fb361..2d31eb9f7aac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - `[jest-jasmine2]` Will now only execute at most 5 concurrent tests _within the same testsuite_ when using `test.concurrent` ([#7770](https://github.com/facebook/jest/pull/7770)) - `[jest-circus]` Same as `[jest-jasmine2]`, only 5 tests will run concurrently by default ([#7770](https://github.com/facebook/jest/pull/7770)) - `[jest-config]` A new `maxConcurrency` option allows to change the number of tests allowed to run concurrently ([#7770](https://github.com/facebook/jest/pull/7770)) +- `[jest-util]`[**BREAKING**] Replace Jest's fake timers implementation with Lolex ([#5171](https://github.com/facebook/jest/pull/5171)) ### Fixes diff --git a/docs/JestObjectAPI.md b/docs/JestObjectAPI.md index 3873dc92328b..39433102f8e3 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -434,10 +434,6 @@ When this API is called, all pending "macro-tasks" that have been queued via `se This is often useful for synchronously executing setTimeouts during a test in order to synchronously assert about some behavior that would only happen after the `setTimeout()` or `setInterval()` callbacks executed. See the [Timer mocks](TimerMocks.md) doc for more information. -### `jest.runAllImmediates()` - -Exhausts all tasks queued by `setImmediate()`. - ### `jest.advanceTimersByTime(msToRun)` ##### renamed in Jest **22.0.0+** @@ -464,6 +460,14 @@ 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()`. + +### `.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. + ## Misc ### `jest.setTimeout(timeout)` diff --git a/e2e/__tests__/fakePromises.test.js b/e2e/__tests__/fakePromises.test.js index 5533b87c7b90..1ee3b6153ae1 100644 --- a/e2e/__tests__/fakePromises.test.js +++ b/e2e/__tests__/fakePromises.test.js @@ -10,13 +10,8 @@ import runJest from '../runJest'; describe('Fake promises', () => { - it('should be possible to resolve with fake timers using immediates', () => { - const result = runJest('fake-promises/immediate'); - expect(result.status).toBe(0); - }); - - it('should be possible to resolve with fake timers using asap', () => { - const result = runJest('fake-promises/asap'); + it('should be possible to resolve with fake timers', () => { + const result = runJest('fake-promises'); expect(result.status).toBe(0); }); }); diff --git a/e2e/fake-promises/asap/__tests__/generator.test.js b/e2e/fake-promises/__tests__/generator.test.js similarity index 100% rename from e2e/fake-promises/asap/__tests__/generator.test.js rename to e2e/fake-promises/__tests__/generator.test.js diff --git a/e2e/fake-promises/asap/fake-promises.js b/e2e/fake-promises/fake-promises.js similarity index 100% rename from e2e/fake-promises/asap/fake-promises.js rename to e2e/fake-promises/fake-promises.js diff --git a/e2e/fake-promises/immediate/__tests__/generator.test.js b/e2e/fake-promises/immediate/__tests__/generator.test.js deleted file mode 100644 index 048163f1a7e7..000000000000 --- a/e2e/fake-promises/immediate/__tests__/generator.test.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * 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 promises', () => { - let someValue; - Promise.resolve().then(() => { - someValue = 'foobar'; - }); - - jest.runAllImmediates(); - - expect(someValue).toBe('foobar'); -}); diff --git a/e2e/fake-promises/immediate/fake-promises.js b/e2e/fake-promises/immediate/fake-promises.js deleted file mode 100644 index 080e9ccd3c8b..000000000000 --- a/e2e/fake-promises/immediate/fake-promises.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * 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'; - -global.Promise = require('promise/setimmediate'); diff --git a/e2e/fake-promises/immediate/package.json b/e2e/fake-promises/immediate/package.json deleted file mode 100644 index 0f50640514e1..000000000000 --- a/e2e/fake-promises/immediate/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "jest": { - "timers": "fake", - "setupFiles": [ - "/fake-promises" - ], - "testEnvironment": "node" - } -} diff --git a/e2e/fake-promises/asap/package.json b/e2e/fake-promises/package.json similarity index 100% rename from e2e/fake-promises/asap/package.json rename to e2e/fake-promises/package.json 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/flow-typed/npm/jest_v23.x.x.js b/flow-typed/npm/jest_v23.x.x.js index 153bfd3b6b23..42e2b44aa739 100644 --- a/flow-typed/npm/jest_v23.x.x.js +++ b/flow-typed/npm/jest_v23.x.x.js @@ -818,10 +818,6 @@ type JestObjectType = { * setInterval(), and setImmediate()). */ runAllTimers(): void, - /** - * Exhausts all tasks queued by setImmediate(). - */ - runAllImmediates(): void, /** * Executes only the macro task queue (i.e. all tasks queued by setTimeout() * or setInterval() and setImmediate()). @@ -862,6 +858,10 @@ type JestObjectType = { * Instructs Jest to use the real versions of the standard timer functions. */ useRealTimers(): JestObjectType, + /** + * Get the remaining number of fake timers scheduled + */ + getTimerCount(): number, /** * Creates a mock function similar to jest.fn but also tracks calls to * object[methodName]. diff --git a/flow-typed/npm/lolex_v2.x.x.js b/flow-typed/npm/lolex_v2.x.x.js new file mode 100644 index 000000000000..62f677d8e4d8 --- /dev/null +++ b/flow-typed/npm/lolex_v2.x.x.js @@ -0,0 +1,50 @@ +// flow-typed signature: a865cf1b7ee719c2a40b85dc8dccf56c +// flow-typed version: fc3f3a2e99/lolex_v2.x.x/flow_>=v0.64.x + +// @flow +declare module 'lolex' { + declare opaque type ImmediateID; + declare type installConfig = { + target?: Object, + now?: number | Date, + toFake?: string[], + loopLimit?: number, + shouldAdvanceTime?: boolean, + advanceTimeDelta?: number, + }; + declare type lolex = { + createClock(now?: number, loopLimit?: number): Clock, + install(config?: installConfig): Clock, + timers: Object, + withGlobal(global: Object): lolex, + }; + declare type Clock = { + setTimeout: typeof setTimeout; + clearTimeout: typeof clearTimeout; + setInterval: typeof setInterval; + clearInterval: typeof clearInterval; + setImmediate: typeof setImmediate; + clearImmediate: typeof clearImmediate; + requestAnimationFrame: typeof requestAnimationFrame; + cancelAnimationFrame: typeof cancelAnimationFrame; + hrtime: typeof process.hrtime; + nextTick: typeof process.nextTick; + now: number; + performance?: { + now: typeof performance.now, + }; + tick(time: number | string): void; + next(): void; + reset(): void; + runAll(): void; + runMicrotasks(): void; + runToFrame(): void; + runToLast(): void; + setSystemTime(now?: number | Date): void; + uninstall(): Object[]; + Date: typeof Date; + Performance: typeof Performance; + countTimers(): number; + } + declare module.exports: lolex; +} diff --git a/packages/jest-environment-jsdom/src/index.js b/packages/jest-environment-jsdom/src/index.js index 52eed2ff069c..982b80547857 100644 --- a/packages/jest-environment-jsdom/src/index.js +++ b/packages/jest-environment-jsdom/src/index.js @@ -18,7 +18,7 @@ import {JSDOM, VirtualConsole} from 'jsdom'; class JSDOMEnvironment { dom: ?Object; - fakeTimers: ?FakeTimers; + fakeTimers: ?FakeTimers; global: ?Global; errorEventListener: ?Function; moduleMocker: ?ModuleMocker; @@ -65,16 +65,9 @@ class JSDOMEnvironment { this.moduleMocker = new mock.ModuleMocker(global); - const timerConfig = { - idToRef: (id: number) => id, - refToId: (ref: number) => ref, - }; - this.fakeTimers = new FakeTimers({ config, global, - moduleMocker: this.moduleMocker, - timerConfig, }); } diff --git a/packages/jest-environment-node/src/index.js b/packages/jest-environment-node/src/index.js index f09b3f7c30a1..7c4e6f0095ab 100644 --- a/packages/jest-environment-node/src/index.js +++ b/packages/jest-environment-node/src/index.js @@ -24,7 +24,7 @@ type Timer = {| class NodeEnvironment { context: ?vm$Context; - fakeTimers: ?FakeTimers; + fakeTimers: ?FakeTimers; global: ?Global; moduleMocker: ?ModuleMocker; @@ -49,28 +49,9 @@ class NodeEnvironment { installCommonGlobals(global, config.globals); this.moduleMocker = new mock.ModuleMocker(global); - const timerIdToRef = (id: number) => ({ - id, - ref() { - return this; - }, - unref() { - return this; - }, - }); - - const timerRefToId = (timer: Timer): ?number => (timer && timer.id) || null; - - const timerConfig = { - idToRef: timerIdToRef, - refToId: timerRefToId, - }; - this.fakeTimers = new FakeTimers({ config, global, - moduleMocker: this.moduleMocker, - timerConfig, }); } diff --git a/packages/jest-jasmine2/src/__tests__/pTimeout.test.js b/packages/jest-jasmine2/src/__tests__/pTimeout.test.js index 84c8e7408a9a..2ba197992e90 100644 --- a/packages/jest-jasmine2/src/__tests__/pTimeout.test.js +++ b/packages/jest-jasmine2/src/__tests__/pTimeout.test.js @@ -13,6 +13,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-runtime/src/index.js b/packages/jest-runtime/src/index.js index 91d6e6a19927..29ab5aa7377e 100644 --- a/packages/jest-runtime/src/index.js +++ b/packages/jest-runtime/src/index.js @@ -995,6 +995,7 @@ class Runtime { fn, genMockFromModule: (moduleName: string) => this._generateMock(from, moduleName), + getRealSystemTime: () => _getFakeTimers().getRealSystemTime(), getTimerCount: () => _getFakeTimers().getTimerCount(), isMockFunction: this._moduleMocker.isMockFunction, isolateModules, @@ -1006,7 +1007,6 @@ class Runtime { resetModules, restoreAllMocks, retryTimes, - runAllImmediates: () => _getFakeTimers().runAllImmediates(), runAllTicks: () => _getFakeTimers().runAllTicks(), runAllTimers: () => _getFakeTimers().runAllTimers(), runOnlyPendingTimers: () => _getFakeTimers().runOnlyPendingTimers(), @@ -1014,6 +1014,7 @@ class Runtime { _getFakeTimers().advanceTimersByTime(msToRun), setMock: (moduleName: string, mock: Object) => setMockFactory(moduleName, () => mock), + setSystemTime: (now?: number) => _getFakeTimers().setSystemTime(now), setTimeout, spyOn, unmock, diff --git a/packages/jest-util/package.json b/packages/jest-util/package.json index db4a4a311ba5..7e7f9a35f3d9 100644 --- a/packages/jest-util/package.json +++ b/packages/jest-util/package.json @@ -14,6 +14,7 @@ "graceful-fs": "^4.1.15", "is-ci": "^2.0.0", "jest-message-util": "^24.0.0", + "lolex": "^3.0.0", "mkdirp": "^0.5.1", "slash": "^2.0.0", "source-map": "^0.6.0" diff --git a/packages/jest-util/src/FakeTimers.js b/packages/jest-util/src/FakeTimers.js index c00a3c30da25..d3209e5623e3 100644 --- a/packages/jest-util/src/FakeTimers.js +++ b/packages/jest-util/src/FakeTimers.js @@ -1,5 +1,5 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * Copyright (c) 2014-present, Facebook, Inc. 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. @@ -9,529 +9,129 @@ import type {ProjectConfig} from 'types/Config'; import type {Global} from 'types/Global'; -import type {ModuleMocker} from 'types/Mock'; +import type {lolex, Clock} from 'lolex'; +import {withGlobal as lolexWithGlobal} from 'lolex'; import {formatStackTrace} from 'jest-message-util'; -import setGlobal from './setGlobal'; -/** - * We don't know the type of arguments for a callback ahead of time which is why - * we are disabling the flowtype/no-weak-types rule here. - */ - -type Callback = (...args: any) => void; - -type TimerID = string; - -type Tick = {| - uuid: string, - callback: Callback, -|}; - -type Timer = {| - type: string, - callback: Callback, - expiry: number, - interval: ?number, -|}; - -type TimerAPI = { - clearImmediate(timeoutId?: number): void, - clearInterval(intervalId?: number): void, - clearTimeout(timeoutId?: number): void, - nextTick: (callback: Callback) => void, - - /** - * The additional arguments in the following methods are passed to the - * callback and thus we don't know their types ahead of time as they can be - * anything, which is why we are disabling the flowtype/no-weak-types rule - * here. - */ - - setImmediate(callback: Callback, ms?: number, ...args: Array): number, - setInterval(callback: Callback, ms?: number, ...args: Array): number, - setTimeout(callback: Callback, ms?: number, ...args: Array): number, -}; - -type TimerConfig = {| - idToRef: (id: number) => Ref, - refToId: (ref: Ref) => ?number, -|}; - -const MS_IN_A_YEAR = 31536000000; - -export default class FakeTimers { - _cancelledImmediates: {[key: TimerID]: boolean}; - _cancelledTicks: {[key: TimerID]: boolean}; +export default class FakeTimers { + _clock: Clock; _config: ProjectConfig; - _disposed: boolean; - _fakeTimerAPIs: TimerAPI; + _fakingTime: boolean; _global: Global; - _immediates: Array; + _lolex: lolex; _maxLoops: number; - _moduleMocker: ModuleMocker; - _now: number; - _ticks: Array; - _timerAPIs: TimerAPI; - _timers: {[key: TimerID]: Timer}; - _uuidCounter: number; - _timerConfig: TimerConfig; constructor({ global, - moduleMocker, - timerConfig, config, maxLoops, }: { global: Global, - moduleMocker: ModuleMocker, - timerConfig: TimerConfig, config: ProjectConfig, maxLoops?: number, }) { this._global = global; - this._timerConfig = timerConfig; this._config = config; this._maxLoops = maxLoops || 100000; - this._uuidCounter = 1; - this._moduleMocker = moduleMocker; - // Store original timer APIs for future reference - this._timerAPIs = { - clearImmediate: global.clearImmediate, - clearInterval: global.clearInterval, - clearTimeout: global.clearTimeout, - nextTick: global.process && global.process.nextTick, - setImmediate: global.setImmediate, - setInterval: global.setInterval, - setTimeout: global.setTimeout, - }; - - this.reset(); - this._createMocks(); + this._fakingTime = false; + this._lolex = lolexWithGlobal(global); } clearAllTimers() { - this._immediates.forEach(immediate => - this._fakeClearImmediate(immediate.uuid), - ); - for (const uuid in this._timers) { - delete this._timers[uuid]; + if (this._fakingTime) { + this._clock.reset(); } } dispose() { - this._disposed = true; - this.clearAllTimers(); - } - - reset() { - this._cancelledTicks = {}; - this._cancelledImmediates = {}; - this._now = 0; - this._ticks = []; - this._immediates = []; - this._timers = {}; + this.useRealTimers(); } - runAllTicks() { - this._checkFakeTimers(); - // Only run a generous number of ticks and then bail. - // This is just to help avoid recursive loops - let i; - for (i = 0; i < this._maxLoops; i++) { - const tick = this._ticks.shift(); - - if (tick === undefined) { - break; - } - - if (!this._cancelledTicks.hasOwnProperty(tick.uuid)) { - // Callback may throw, so update the map prior calling. - this._cancelledTicks[tick.uuid] = true; - tick.callback(); - } - } - - if (i === this._maxLoops) { - throw new Error( - 'Ran ' + - this._maxLoops + - ' ticks, and there are still more! ' + - "Assuming we've hit an infinite recursion and bailing out...", - ); + runAllTimers() { + if (this._checkFakeTimers()) { + this._clock.runAll(); } } - runAllImmediates() { - this._checkFakeTimers(); - // Only run a generous number of immediates and then bail. - let i; - for (i = 0; i < this._maxLoops; i++) { - const immediate = this._immediates.shift(); - if (immediate === undefined) { - break; - } - this._runImmediate(immediate); - } - - if (i === this._maxLoops) { - throw new Error( - 'Ran ' + - this._maxLoops + - ' immediates, and there are still more! Assuming ' + - "we've hit an infinite recursion and bailing out...", - ); + runOnlyPendingTimers() { + if (this._checkFakeTimers()) { + this._clock.runToLast(); } } - _runImmediate(immediate: Tick) { - if (!this._cancelledImmediates.hasOwnProperty(immediate.uuid)) { - // Callback may throw, so update the map prior calling. - this._cancelledImmediates[immediate.uuid] = true; - immediate.callback(); + advanceTimersByTime(msToRun: number) { + if (this._checkFakeTimers()) { + this._clock.tick(msToRun); } } - runAllTimers() { - this._checkFakeTimers(); - this.runAllTicks(); - this.runAllImmediates(); - - // Only run a generous number of timers and then bail. - // This is just to help avoid recursive loops - let i; - for (i = 0; i < this._maxLoops; i++) { - const nextTimerHandle = this._getNextTimerHandle(); - - // If there are no more timer handles, stop! - if (nextTimerHandle === null) { - break; - } - - this._runTimerHandle(nextTimerHandle); - - // Some of the immediate calls could be enqueued - // during the previous handling of the timers, we should - // run them as well. - if (this._immediates.length) { - this.runAllImmediates(); - } - - if (this._ticks.length) { - this.runAllTicks(); - } - } - - if (i === this._maxLoops) { - throw new Error( - 'Ran ' + - this._maxLoops + - ' timers, and there are still more! ' + - "Assuming we've hit an infinite recursion and bailing out...", - ); + runAllTicks() { + if (this._checkFakeTimers()) { + this._clock.runMicrotasks(); } } - runOnlyPendingTimers() { - const timers = {...this._timers}; - this._checkFakeTimers(); - this._immediates.forEach(this._runImmediate, this); - Object.keys(timers) - .sort((left, right) => timers[left].expiry - timers[right].expiry) - .forEach(this._runTimerHandle, this); - } - - advanceTimersByTime(msToRun: number) { - this._checkFakeTimers(); - // Only run a generous number of timers and then bail. - // This is just to help avoid recursive loops - let i; - for (i = 0; i < this._maxLoops; i++) { - const timerHandle = this._getNextTimerHandle(); - - // If there are no more timer handles, stop! - if (timerHandle === null) { - break; - } - - const nextTimerExpiry = this._timers[timerHandle].expiry; - if (this._now + msToRun < nextTimerExpiry) { - // There are no timers between now and the target we're running to, so - // adjust our time cursor and quit - this._now += msToRun; - break; - } else { - msToRun -= nextTimerExpiry - this._now; - this._now = nextTimerExpiry; - this._runTimerHandle(timerHandle); - } - } - - if (i === this._maxLoops) { - throw new Error( - 'Ran ' + - this._maxLoops + - ' timers, and there are still more! ' + - "Assuming we've hit an infinite recursion and bailing out...", - ); + useRealTimers() { + if (this._fakingTime) { + this._clock.uninstall(); + this._fakingTime = false; } } - runWithRealTimers(cb: Callback) { - const prevClearImmediate = this._global.clearImmediate; - const prevClearInterval = this._global.clearInterval; - const prevClearTimeout = this._global.clearTimeout; - const prevNextTick = this._global.process.nextTick; - const prevSetImmediate = this._global.setImmediate; - const prevSetInterval = this._global.setInterval; - const prevSetTimeout = this._global.setTimeout; + useFakeTimers() { + const toFake = Object.keys(this._lolex.timers); - this.useRealTimers(); + if (!this._fakingTime) { + this._clock = this._lolex.install({ + loopLimit: this._maxLoops, + now: Date.now(), + target: this._global, + toFake, + }); - let cbErr = null; - let errThrown = false; - try { - cb(); - } catch (e) { - errThrown = true; - cbErr = e; + this._fakingTime = true; } + } - this._global.clearImmediate = prevClearImmediate; - this._global.clearInterval = prevClearInterval; - this._global.clearTimeout = prevClearTimeout; - this._global.process.nextTick = prevNextTick; - this._global.setImmediate = prevSetImmediate; - this._global.setInterval = prevSetInterval; - this._global.setTimeout = prevSetTimeout; - - if (errThrown) { - throw cbErr; + reset() { + if (this._checkFakeTimers()) { + const {now} = this._clock; + this._clock.reset(); + this._clock.setSystemTime(now); } } - useRealTimers() { - const global = this._global; - setGlobal(global, 'clearImmediate', this._timerAPIs.clearImmediate); - setGlobal(global, 'clearInterval', this._timerAPIs.clearInterval); - setGlobal(global, 'clearTimeout', this._timerAPIs.clearTimeout); - setGlobal(global, 'setImmediate', this._timerAPIs.setImmediate); - setGlobal(global, 'setInterval', this._timerAPIs.setInterval); - setGlobal(global, 'setTimeout', this._timerAPIs.setTimeout); - - global.process.nextTick = this._timerAPIs.nextTick; + setSystemTime(now?: number) { + if (this._checkFakeTimers()) { + this._clock.setSystemTime(now); + } } - useFakeTimers() { - this._createMocks(); - - const global = this._global; - setGlobal(global, 'clearImmediate', this._fakeTimerAPIs.clearImmediate); - setGlobal(global, 'clearInterval', this._fakeTimerAPIs.clearInterval); - setGlobal(global, 'clearTimeout', this._fakeTimerAPIs.clearTimeout); - setGlobal(global, 'setImmediate', this._fakeTimerAPIs.setImmediate); - setGlobal(global, 'setInterval', this._fakeTimerAPIs.setInterval); - setGlobal(global, 'setTimeout', this._fakeTimerAPIs.setTimeout); - - global.process.nextTick = this._fakeTimerAPIs.nextTick; + getRealSystemTime() { + return Date.now(); } getTimerCount() { - this._checkFakeTimers(); - - return Object.keys(this._timers).length; + if (this._checkFakeTimers()) { + return this._clock.countTimers(); + } } _checkFakeTimers() { - if (this._global.setTimeout !== this._fakeTimerAPIs.setTimeout) { + 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. This warning is likely a result of a ` + - `default configuration change in Jest 15.\n\n` + - `Release Blog Post: https://jestjs.io/blog/2016/09/01/jest-15.html\n` + - `Stack Trace:\n` + + '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, }), ); } - } - - _createMocks() { - const fn = impl => this._moduleMocker.fn().mockImplementation(impl); - - this._fakeTimerAPIs = { - clearImmediate: fn(this._fakeClearImmediate.bind(this)), - clearInterval: fn(this._fakeClearTimer.bind(this)), - clearTimeout: fn(this._fakeClearTimer.bind(this)), - nextTick: fn(this._fakeNextTick.bind(this)), - setImmediate: fn(this._fakeSetImmediate.bind(this)), - setInterval: fn(this._fakeSetInterval.bind(this)), - setTimeout: fn(this._fakeSetTimeout.bind(this)), - }; - } - - _fakeClearTimer(timerRef: TimerRef) { - const uuid = this._timerConfig.refToId(timerRef); - - if (uuid && this._timers.hasOwnProperty(uuid)) { - delete this._timers[String(uuid)]; - } - } - - _fakeClearImmediate(uuid: TimerID) { - this._cancelledImmediates[uuid] = true; - } - - _fakeNextTick(callback: Callback) { - if (this._disposed) { - return; - } - - const args = []; - for (let ii = 1, ll = arguments.length; ii < ll; ii++) { - args.push(arguments[ii]); - } - - const uuid = String(this._uuidCounter++); - - this._ticks.push({ - callback: () => callback.apply(null, args), - uuid, - }); - - const cancelledTicks = this._cancelledTicks; - this._timerAPIs.nextTick(() => { - if (!cancelledTicks.hasOwnProperty(uuid)) { - // Callback may throw, so update the map prior calling. - cancelledTicks[uuid] = true; - callback.apply(null, args); - } - }); - } - - _fakeSetImmediate(callback: Callback) { - if (this._disposed) { - return null; - } - - const args = []; - for (let ii = 1, ll = arguments.length; ii < ll; ii++) { - args.push(arguments[ii]); - } - - const uuid = this._uuidCounter++; - - this._immediates.push({ - callback: () => callback.apply(null, args), - uuid: String(uuid), - }); - - const cancelledImmediates = this._cancelledImmediates; - this._timerAPIs.setImmediate(() => { - if (!cancelledImmediates.hasOwnProperty(uuid)) { - // Callback may throw, so update the map prior calling. - cancelledImmediates[String(uuid)] = true; - callback.apply(null, args); - } - }); - - return uuid; - } - _fakeSetInterval(callback: Callback, intervalDelay?: number) { - if (this._disposed) { - return null; - } - - if (intervalDelay == null) { - intervalDelay = 0; - } - - const args = []; - for (let ii = 2, ll = arguments.length; ii < ll; ii++) { - args.push(arguments[ii]); - } - - const uuid = this._uuidCounter++; - - this._timers[String(uuid)] = { - callback: () => callback.apply(null, args), - expiry: this._now + intervalDelay, - interval: intervalDelay, - type: 'interval', - }; - - return this._timerConfig.idToRef(uuid); - } - - _fakeSetTimeout(callback: Callback, delay?: number) { - if (this._disposed) { - return null; - } - - // eslint-disable-next-line no-bitwise - delay = Number(delay) | 0; - - const args = []; - for (let ii = 2, ll = arguments.length; ii < ll; ii++) { - args.push(arguments[ii]); - } - - const uuid = this._uuidCounter++; - - this._timers[String(uuid)] = { - callback: () => callback.apply(null, args), - expiry: this._now + delay, - interval: null, - type: 'timeout', - }; - - return this._timerConfig.idToRef(uuid); - } - - _getNextTimerHandle() { - let nextTimerHandle = null; - let uuid; - let soonestTime = MS_IN_A_YEAR; - let timer; - for (uuid in this._timers) { - timer = this._timers[uuid]; - if (timer.expiry < soonestTime) { - soonestTime = timer.expiry; - nextTimerHandle = uuid; - } - } - - return nextTimerHandle; - } - - _runTimerHandle(timerHandle: TimerID) { - const timer = this._timers[timerHandle]; - - if (!timer) { - return; - } - - switch (timer.type) { - case 'timeout': - const callback = timer.callback; - delete this._timers[timerHandle]; - callback(); - break; - - case 'interval': - timer.expiry = this._now + timer.interval; - timer.callback(); - break; - - default: - throw new Error('Unexpected timer type: ' + timer.type); - } + return this._fakingTime; } } diff --git a/packages/jest-util/src/__tests__/__snapshots__/fakeTimers.test.js.snap b/packages/jest-util/src/__tests__/__snapshots__/fakeTimers.test.js.snap index 5e0d7432996b..39cc8dd4ad64 100644 --- a/packages/jest-util/src/__tests__/__snapshots__/fakeTimers.test.js.snap +++ b/packages/jest-util/src/__tests__/__snapshots__/fakeTimers.test.js.snap @@ -1,7 +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. This warning is likely a result of a default configuration change in Jest 15. - -Release Blog Post: https://jestjs.io/blog/2016/09/01/jest-15.html" -`; +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-util/src/__tests__/fakeTimers.test.js b/packages/jest-util/src/__tests__/fakeTimers.test.js index c39fb0c2a4e4..9d913e6a5cf2 100644 --- a/packages/jest-util/src/__tests__/fakeTimers.test.js +++ b/packages/jest-util/src/__tests__/fakeTimers.test.js @@ -6,50 +6,34 @@ * */ -'use strict'; - -const vm = require('vm'); +import FakeTimers from '../FakeTimers'; describe('FakeTimers', () => { - let FakeTimers, moduleMocker, timerConfig; - - beforeEach(() => { - FakeTimers = require('../FakeTimers').default; - const mock = require('jest-mock'); - const global = vm.runInNewContext('this'); - moduleMocker = new mock.ModuleMocker(global); - - timerConfig = { - idToRef: (id: number) => id, - refToId: (ref: number) => ref, - }; - }); - describe('construction', () => { it('installs setTimeout mock', () => { - const global = {process}; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + 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 = {process}; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + 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 = {process}; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + 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 = {process}; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); expect(global.clearInterval).not.toBe(undefined); }); @@ -57,11 +41,14 @@ describe('FakeTimers', () => { it('mocks process.nextTick if it exists on global', () => { const origNextTick = () => {}; const global = { + Date, + clearTimeout, process: { nextTick: origNextTick, }, + setTimeout, }; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const timers = new FakeTimers({global}); timers.useFakeTimers(); expect(global.process.nextTick).not.toBe(origNextTick); }); @@ -69,10 +56,13 @@ describe('FakeTimers', () => { it('mocks setImmediate if it exists on global', () => { const origSetImmediate = () => {}; const global = { + Date, + clearTimeout, process, setImmediate: origSetImmediate, + setTimeout, }; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const timers = new FakeTimers({global}); timers.useFakeTimers(); expect(global.setImmediate).not.toBe(origSetImmediate); }); @@ -81,11 +71,14 @@ describe('FakeTimers', () => { const origSetImmediate = () => {}; const origClearImmediate = () => {}; const global = { + Date, clearImmediate: origClearImmediate, + clearTimeout, process, setImmediate: origSetImmediate, + setTimeout, }; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const timers = new FakeTimers({global}); timers.useFakeTimers(); expect(global.clearImmediate).not.toBe(origClearImmediate); }); @@ -94,12 +87,15 @@ describe('FakeTimers', () => { describe('runAllTicks', () => { it('runs all ticks, in order', () => { const global = { + Date, + clearTimeout, process: { nextTick: () => {}, }, + setTimeout, }; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const timers = new FakeTimers({global}); timers.useFakeTimers(); const runOrder = []; @@ -122,12 +118,15 @@ describe('FakeTimers', () => { 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, moduleMocker, timerConfig}); + const timers = new FakeTimers({global}); timers.useFakeTimers(); timers.runAllTicks(); @@ -136,12 +135,15 @@ describe('FakeTimers', () => { it('only runs a scheduled callback once', () => { const global = { + Date, + clearTimeout, process: { nextTick: () => {}, }, + setTimeout, }; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const timers = new FakeTimers({global}); timers.useFakeTimers(); const mock1 = jest.fn(); @@ -155,136 +157,17 @@ describe('FakeTimers', () => { expect(mock1).toHaveBeenCalledTimes(1); }); - it('cancels a callback even from native nextTick', () => { - const nativeNextTick = jest.fn(); - - const global = { - process: { - nextTick: nativeNextTick, - }, - }; - - const timers = new FakeTimers({global, moduleMocker, timerConfig}); - timers.useFakeTimers(); - - const mock1 = jest.fn(); - global.process.nextTick(mock1); - timers.runAllTicks(); - expect(mock1).toHaveBeenCalledTimes(1); - expect(nativeNextTick).toHaveBeenCalledTimes(1); - - // Now imagine we fast forward to the next real tick. We need to be sure - // that native nextTick doesn't try to run the callback again - nativeNextTick.mock.calls[0][0](); - expect(mock1).toHaveBeenCalledTimes(1); - }); - - it('cancels a callback even from native setImmediate', () => { - const nativeSetImmediate = jest.fn(); - - const global = { - process, - setImmediate: nativeSetImmediate, - }; - - const timers = new FakeTimers({global, moduleMocker, timerConfig}); - timers.useFakeTimers(); - - const mock1 = jest.fn(); - global.setImmediate(mock1); - timers.runAllImmediates(); - expect(mock1).toHaveBeenCalledTimes(1); - expect(nativeSetImmediate).toHaveBeenCalledTimes(1); - - // ensure that native setImmediate doesn't try to run the callback again - nativeSetImmediate.mock.calls[0][0](); - expect(mock1).toHaveBeenCalledTimes(1); - }); - - it('doesnt run a tick callback if native nextTick already did', () => { - const nativeNextTick = jest.fn(); - - const global = { - process: { - nextTick: nativeNextTick, - }, - }; - - const timers = new FakeTimers({global, moduleMocker, timerConfig}); - timers.useFakeTimers(); - - const mock1 = jest.fn(); - global.process.nextTick(mock1); - - // Emulate native nextTick running... - nativeNextTick.mock.calls[0][0](); - expect(mock1).toHaveBeenCalledTimes(1); - - // Ensure runAllTicks() doesn't run the callback again - timers.runAllTicks(); - expect(mock1).toHaveBeenCalledTimes(1); - }); - - it('doesnt run immediate if native setImmediate already did', () => { - const nativeSetImmediate = jest.fn(); - - const global = { - process, - setImmediate: nativeSetImmediate, - }; - - const timers = new FakeTimers({global, moduleMocker, timerConfig}); - timers.useFakeTimers(); - - const mock1 = jest.fn(); - global.setImmediate(mock1); - - // Emulate native setImmediate running... - nativeSetImmediate.mock.calls[0][0](); - expect(mock1).toHaveBeenCalledTimes(1); - - // Ensure runAllTicks() doesn't run the callback again - timers.runAllImmediates(); - expect(mock1).toHaveBeenCalledTimes(1); - }); - - it('native doesnt run immediate if fake already did', () => { - const nativeSetImmediate = jest.fn(); - - const global = { - process, - setImmediate: nativeSetImmediate, - }; - - const timers = new FakeTimers({global, moduleMocker, timerConfig}); - timers.useFakeTimers(); - - const mock1 = jest.fn(); - global.setImmediate(mock1); - - //run all immediates now - timers.runAllImmediates(); - expect(mock1).toHaveBeenCalledTimes(1); - - // Emulate native setImmediate running ensuring it doesn't re-run - nativeSetImmediate.mock.calls[0][0](); - - expect(mock1).toHaveBeenCalledTimes(1); - }); - it('throws before allowing infinite recursion', () => { const global = { + Date, + clearTimeout, process: { nextTick: () => {}, }, + setTimeout, }; - const timers = new FakeTimers({ - global, - maxLoops: 100, - moduleMocker, - timerConfig, - }); + const timers = new FakeTimers({global, maxLoops: 100}); timers.useFakeTimers(); @@ -295,18 +178,15 @@ describe('FakeTimers', () => { expect(() => { timers.runAllTicks(); }).toThrow( - new Error( - "Ran 100 ticks, and there are still more! Assuming we've hit an " + - 'infinite recursion and bailing out...', - ), + 'Aborting after running 100 timers, assuming an infinite loop!', ); }); }); describe('runAllTimers', () => { it('runs all timers in order', () => { - const global = {process}; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const runOrder = []; @@ -346,8 +226,6 @@ describe('FakeTimers', () => { rootDir: __dirname, }, global, - moduleMocker, - timerConfig, }); timers.runAllTimers(); expect( @@ -359,18 +237,20 @@ describe('FakeTimers', () => { 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, moduleMocker, timerConfig}); + const timers = new FakeTimers({global}); timers.useFakeTimers(); timers.runAllTimers(); }); it('only runs a setTimeout callback once (ever)', () => { - const global = {process}; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const fn = jest.fn(); @@ -385,8 +265,8 @@ describe('FakeTimers', () => { }); it('runs callbacks with arguments after the interval', () => { - const global = {process}; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const fn = jest.fn(); @@ -397,15 +277,20 @@ describe('FakeTimers', () => { expect(fn).toHaveBeenCalledWith('mockArg1', 'mockArg2'); }); - it('doesnt pass the callback to native setTimeout', () => { + 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, moduleMocker, timerConfig}); + 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(); @@ -417,13 +302,8 @@ describe('FakeTimers', () => { }); it('throws before allowing infinite recursion', () => { - const global = {process}; - const timers = new FakeTimers({ - global, - maxLoops: 100, - moduleMocker, - timerConfig, - }); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global, maxLoops: 100}); timers.useFakeTimers(); global.setTimeout(function infinitelyRecursingCallback() { @@ -434,15 +314,14 @@ describe('FakeTimers', () => { timers.runAllTimers(); }).toThrow( new Error( - "Ran 100 timers, and there are still more! Assuming we've hit an " + - 'infinite recursion and bailing out...', + 'Aborting after running 100 timers, assuming an infinite loop!', ), ); }); it('also clears ticks', () => { - const global = {process}; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const fn = jest.fn(); @@ -458,8 +337,8 @@ describe('FakeTimers', () => { describe('advanceTimersByTime', () => { it('runs timers in order', () => { - const global = {process}; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const runOrder = []; @@ -497,42 +376,18 @@ describe('FakeTimers', () => { }); it('does nothing when no timers have been scheduled', () => { - const global = {process}; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); timers.advanceTimersByTime(100); }); - - it('throws before allowing infinite recursion', () => { - const global = {process}; - const timers = new FakeTimers({ - global, - maxLoops: 100, - moduleMocker, - timerConfig, - }); - timers.useFakeTimers(); - - global.setTimeout(function infinitelyRecursingCallback() { - global.setTimeout(infinitelyRecursingCallback, 0); - }, 0); - - expect(() => { - timers.advanceTimersByTime(50); - }).toThrow( - new Error( - "Ran 100 timers, and there are still more! Assuming we've hit an " + - 'infinite recursion and bailing out...', - ), - ); - }); }); describe('reset', () => { it('resets all pending setTimeouts', () => { - const global = {process}; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const mock1 = jest.fn(); @@ -544,8 +399,8 @@ describe('FakeTimers', () => { }); it('resets all pending setIntervals', () => { - const global = {process}; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const mock1 = jest.fn(); @@ -556,14 +411,17 @@ describe('FakeTimers', () => { expect(mock1).toHaveBeenCalledTimes(0); }); - it('resets all pending ticks callbacks & immediates', () => { + it('resets all pending ticks callbacks', () => { const global = { + Date, + clearTimeout, process: { nextTick: () => {}, }, setImmediate: () => {}, + setTimeout, }; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const timers = new FakeTimers({global}); timers.useFakeTimers(); const mock1 = jest.fn(); @@ -572,13 +430,12 @@ describe('FakeTimers', () => { timers.reset(); timers.runAllTicks(); - timers.runAllImmediates(); expect(mock1).toHaveBeenCalledTimes(0); }); it('resets current advanceTimersByTime time cursor', () => { - const global = {process}; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const mock1 = jest.fn(); @@ -598,11 +455,14 @@ describe('FakeTimers', () => { const nativeSetImmediate = jest.fn(); const global = { + Date, + clearTimeout, process, setImmediate: nativeSetImmediate, + setTimeout, }; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const timers = new FakeTimers({global}); timers.useFakeTimers(); const runOrder = []; @@ -614,7 +474,7 @@ describe('FakeTimers', () => { global.setTimeout(function cb() { runOrder.push('mock2'); - global.setTimeout(cb, 0); + global.setTimeout(cb, 50); }, 0); global.setInterval(() => { @@ -631,26 +491,38 @@ describe('FakeTimers', () => { }); timers.runOnlyPendingTimers(); - expect(runOrder).toEqual(['mock4', 'mock5', 'mock2', 'mock1', 'mock3']); - - timers.runOnlyPendingTimers(); - expect(runOrder).toEqual([ + 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 = {process}; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const fn = jest.fn(); @@ -664,137 +536,6 @@ describe('FakeTimers', () => { }); }); - describe('runWithRealTimers', () => { - it('executes callback with native timers', () => { - const nativeClearInterval = jest.fn(); - const nativeClearTimeout = jest.fn(); - const nativeSetInterval = jest.fn(); - const nativeSetTimeout = jest.fn(); - - const global = { - clearInterval: nativeClearInterval, - clearTimeout: nativeClearTimeout, - process, - setInterval: nativeSetInterval, - setTimeout: nativeSetTimeout, - }; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); - timers.useFakeTimers(); - - // clearInterval() - timers.runWithRealTimers(() => { - global.clearInterval(); - }); - expect(nativeClearInterval).toHaveBeenCalledTimes(1); - expect(global.clearInterval).toHaveBeenCalledTimes(0); - - // clearTimeout() - timers.runWithRealTimers(() => { - global.clearTimeout(); - }); - expect(nativeClearTimeout).toHaveBeenCalledTimes(1); - expect(global.clearTimeout).toHaveBeenCalledTimes(0); - - // setInterval() - timers.runWithRealTimers(() => { - global.setInterval(); - }); - expect(nativeSetInterval).toHaveBeenCalledTimes(1); - expect(global.setInterval).toHaveBeenCalledTimes(0); - - // setTimeout() - timers.runWithRealTimers(() => { - global.setTimeout(); - }); - expect(nativeSetTimeout).toHaveBeenCalledTimes(1); - expect(global.setTimeout).toHaveBeenCalledTimes(0); - }); - - it('resets mock timers after executing callback', () => { - const nativeClearInterval = jest.fn(); - const nativeClearTimeout = jest.fn(); - const nativeSetInterval = jest.fn(); - const nativeSetTimeout = jest.fn(); - - const global = { - clearInterval: nativeClearInterval, - clearTimeout: nativeClearTimeout, - process, - setInterval: nativeSetInterval, - setTimeout: nativeSetTimeout, - }; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); - timers.useFakeTimers(); - - // clearInterval() - timers.runWithRealTimers(() => { - global.clearInterval(); - }); - expect(nativeClearInterval).toHaveBeenCalledTimes(1); - expect(global.clearInterval).toHaveBeenCalledTimes(0); - - global.clearInterval(); - expect(nativeClearInterval).toHaveBeenCalledTimes(1); - expect(global.clearInterval).toHaveBeenCalledTimes(1); - - // clearTimeout() - timers.runWithRealTimers(() => { - global.clearTimeout(); - }); - expect(nativeClearTimeout).toHaveBeenCalledTimes(1); - expect(global.clearTimeout).toHaveBeenCalledTimes(0); - - global.clearTimeout(); - expect(nativeClearTimeout).toHaveBeenCalledTimes(1); - expect(global.clearTimeout).toHaveBeenCalledTimes(1); - - // setInterval() - timers.runWithRealTimers(() => { - global.setInterval(); - }); - expect(nativeSetInterval).toHaveBeenCalledTimes(1); - expect(global.setInterval).toHaveBeenCalledTimes(0); - - global.setInterval(); - expect(nativeSetInterval).toHaveBeenCalledTimes(1); - expect(global.setInterval).toHaveBeenCalledTimes(1); - - // setTimeout() - timers.runWithRealTimers(() => { - global.setTimeout(); - }); - expect(nativeSetTimeout).toHaveBeenCalledTimes(1); - expect(global.setTimeout).toHaveBeenCalledTimes(0); - - global.setTimeout(); - expect(nativeSetTimeout).toHaveBeenCalledTimes(1); - expect(global.setTimeout).toHaveBeenCalledTimes(1); - }); - - it('resets mock timer functions even if callback throws', () => { - const nativeSetTimeout = jest.fn(); - const global = { - process, - setTimeout: nativeSetTimeout, - }; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); - timers.useFakeTimers(); - - expect(() => { - timers.runWithRealTimers(() => { - global.setTimeout(); - throw new Error('test'); - }); - }).toThrow(new Error('test')); - expect(nativeSetTimeout).toHaveBeenCalledTimes(1); - expect(global.setTimeout).toHaveBeenCalledTimes(0); - - global.setTimeout(); - expect(nativeSetTimeout).toHaveBeenCalledTimes(1); - expect(global.setTimeout).toHaveBeenCalledTimes(1); - }); - }); - describe('useRealTimers', () => { it('resets native timer APIs', () => { const nativeSetTimeout = jest.fn(); @@ -803,13 +544,14 @@ describe('FakeTimers', () => { const nativeClearInterval = jest.fn(); const global = { + Date, clearInterval: nativeClearInterval, clearTimeout: nativeClearTimeout, process, setInterval: nativeSetInterval, setTimeout: nativeSetTimeout, }; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const timers = new FakeTimers({global}); timers.useFakeTimers(); // Ensure that timers has overridden the native timer APIs @@ -831,9 +573,12 @@ describe('FakeTimers', () => { const nativeProcessNextTick = jest.fn(); const global = { + Date, + clearTimeout, process: {nextTick: nativeProcessNextTick}, + setTimeout, }; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const timers = new FakeTimers({global}); timers.useFakeTimers(); // Ensure that timers has overridden the native timer APIs @@ -850,11 +595,14 @@ describe('FakeTimers', () => { const nativeClearImmediate = jest.fn(); const global = { + Date, clearImmediate: nativeClearImmediate, + clearTimeout, process, setImmediate: nativeSetImmediate, + setTimeout, }; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const timers = new FakeTimers({global}); timers.useFakeTimers(); // Ensure that timers has overridden the native timer APIs @@ -877,13 +625,14 @@ describe('FakeTimers', () => { const nativeClearInterval = jest.fn(); const global = { + Date, clearInterval: nativeClearInterval, clearTimeout: nativeClearTimeout, process, setInterval: nativeSetInterval, setTimeout: nativeSetTimeout, }; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const timers = new FakeTimers({global}); timers.useRealTimers(); // Ensure that the real timers are installed at this point @@ -905,9 +654,12 @@ describe('FakeTimers', () => { const nativeProcessNextTick = jest.fn(); const global = { + Date, + clearTimeout, process: {nextTick: nativeProcessNextTick}, + setTimeout, }; - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const timers = new FakeTimers({global}); timers.useRealTimers(); // Ensure that the real timers are installed at this point @@ -924,11 +676,14 @@ describe('FakeTimers', () => { const nativeClearImmediate = jest.fn(); const global = { + Date, clearImmediate: nativeClearImmediate, + clearTimeout, process, setImmediate: nativeSetImmediate, + setTimeout, }; - const fakeTimers = new FakeTimers({global, moduleMocker, timerConfig}); + const fakeTimers = new FakeTimers({global}); fakeTimers.useRealTimers(); // Ensure that the real timers are installed at this point @@ -945,7 +700,7 @@ describe('FakeTimers', () => { describe('getTimerCount', () => { it('returns the correct count', () => { - const timers = new FakeTimers({global, moduleMocker, timerConfig}); + const timers = new FakeTimers({global}); timers.useFakeTimers(); diff --git a/types/Environment.js b/types/Environment.js index b11fc9658370..b5769380c71a 100644 --- a/types/Environment.js +++ b/types/Environment.js @@ -23,13 +23,14 @@ declare class $JestEnvironment { global: Global; fakeTimers: { clearAllTimers(): void, - runAllImmediates(): void, runAllTicks(): void, runAllTimers(): void, advanceTimersByTime(msToRun: number): void, runOnlyPendingTimers(): void, runWithRealTimers(callback: any): void, getTimerCount(): number, + setSystemTime(now?: number): void, + getRealSystemTime(): number, useFakeTimers(): void, useRealTimers(): void, }; diff --git a/types/Jest.js b/types/Jest.js index 23f7c8e04996..24846e54ad2e 100644 --- a/types/Jest.js +++ b/types/Jest.js @@ -33,10 +33,11 @@ export type Jest = {| resetModules(): Jest, restoreAllMocks(): Jest, retryTimes(numRetries: number): Jest, - runAllImmediates(): void, runAllTicks(): void, runAllTimers(): void, runOnlyPendingTimers(): void, + getRealSystemTime(): number, + setSystemTime(now?: number): void, advanceTimersByTime(msToRun: number): void, runTimersToTime(msToRun: number): void, getTimerCount(): number, diff --git a/yarn.lock b/yarn.lock index 9dfdd03de271..4977b6c17729 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7926,6 +7926,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"