From e30e4ac3cd75bd013a7f89aee93ad02d89e974b6 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.js | 22 + 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 +- flow-typed/npm/lolex_v2.x.x.js | 50 ++ .../legacy-code-todo-rewrite/jestAdapter.js | 4 + packages/jest-environment-jsdom/src/index.js | 12 +- packages/jest-environment-node/src/index.js | 12 +- .../src/__tests__/pTimeout.test.js | 5 + packages/jest-jasmine2/src/index.js | 4 + packages/jest-runtime/src/index.js | 80 +- packages/jest-util/package.json | 1 + packages/jest-util/src/FakeTimersLolex.js | 139 ++++ .../fakeTimersLolex.test.js.snap | 3 + .../src/__tests__/fakeTimers.test.js | 10 +- .../src/__tests__/fakeTimersLolex.test.js | 722 ++++++++++++++++++ packages/jest-util/src/index.js | 2 + types/Config.js | 2 +- types/Environment.js | 15 +- types/Jest.js | 2 + yarn.lock | 5 + 29 files changed, 1143 insertions(+), 55 deletions(-) create mode 100644 e2e/__tests__/lolex.test.js 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 flow-typed/npm/lolex_v2.x.x.js create mode 100644 packages/jest-util/src/FakeTimersLolex.js create mode 100644 packages/jest-util/src/__tests__/__snapshots__/fakeTimersLolex.test.js.snap create mode 100644 packages/jest-util/src/__tests__/fakeTimersLolex.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 815f452fb361..d0637f82f8eb 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]` Add possibility to use Lolex as implementation of fake timers ([#7776](https://github.com/facebook/jest/pull/7776)) ### Fixes diff --git a/docs/Configuration.md b/docs/Configuration.md index 4a90737f9258..efe43895f5b2 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1002,6 +1002,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 3873dc92328b..2500f3add575 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -408,10 +408,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()` @@ -438,6 +440,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+** @@ -464,6 +468,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.js b/e2e/__tests__/lolex.test.js new file mode 100644 index 000000000000..f350c9fe70d9 --- /dev/null +++ b/e2e/__tests__/lolex.test.js @@ -0,0 +1,22 @@ +/** + * 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. + * + * @flow + */ + +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/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-circus/src/legacy-code-todo-rewrite/jestAdapter.js b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.js index 8cabcb8a9f76..167d51d54bf1 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.js +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.js @@ -51,6 +51,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.js b/packages/jest-environment-jsdom/src/index.js index 52eed2ff069c..960639bee8d0 100644 --- a/packages/jest-environment-jsdom/src/index.js +++ b/packages/jest-environment-jsdom/src/index.js @@ -12,13 +12,14 @@ import type {EnvironmentContext} from 'types/Environment'; import type {Global} from 'types/Global'; import type {ModuleMocker} from 'jest-mock'; -import {FakeTimers, installCommonGlobals} from 'jest-util'; +import {FakeTimers, FakeTimersLolex, installCommonGlobals} from 'jest-util'; import mock from 'jest-mock'; import {JSDOM, VirtualConsole} from 'jsdom'; class JSDOMEnvironment { dom: ?Object; fakeTimers: ?FakeTimers; + fakeTimersLolex: ?FakeTimersLolex; global: ?Global; errorEventListener: ?Function; moduleMocker: ?ModuleMocker; @@ -76,6 +77,11 @@ class JSDOMEnvironment { moduleMocker: this.moduleMocker, timerConfig, }); + + this.fakeTimersLolex = new FakeTimersLolex({ + config, + global, + }); } setup(): Promise { @@ -86,6 +92,9 @@ class JSDOMEnvironment { if (this.fakeTimers) { this.fakeTimers.dispose(); } + if (this.fakeTimersLolex) { + this.fakeTimersLolex.dispose(); + } if (this.global) { if (this.errorEventListener) { this.global.removeEventListener('error', this.errorEventListener); @@ -98,6 +107,7 @@ class JSDOMEnvironment { this.global = null; this.dom = null; this.fakeTimers = null; + this.fakeTimersLolex = null; return Promise.resolve(); } diff --git a/packages/jest-environment-node/src/index.js b/packages/jest-environment-node/src/index.js index f09b3f7c30a1..0243b755ff19 100644 --- a/packages/jest-environment-node/src/index.js +++ b/packages/jest-environment-node/src/index.js @@ -13,7 +13,7 @@ import type {Global} from 'types/Global'; import type {ModuleMocker} from 'jest-mock'; import vm from 'vm'; -import {FakeTimers, installCommonGlobals} from 'jest-util'; +import {FakeTimers, FakeTimersLolex, installCommonGlobals} from 'jest-util'; import mock from 'jest-mock'; type Timer = {| @@ -25,6 +25,7 @@ type Timer = {| class NodeEnvironment { context: ?vm$Context; fakeTimers: ?FakeTimers; + fakeTimersLolex: ?FakeTimersLolex; global: ?Global; moduleMocker: ?ModuleMocker; @@ -72,6 +73,11 @@ class NodeEnvironment { moduleMocker: this.moduleMocker, timerConfig, }); + + this.fakeTimersLolex = new FakeTimersLolex({ + config, + global, + }); } setup(): Promise { @@ -82,8 +88,12 @@ class NodeEnvironment { 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-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-jasmine2/src/index.js b/packages/jest-jasmine2/src/index.js index 17a5302af33f..ca26e55b3382 100644 --- a/packages/jest-jasmine2/src/index.js +++ b/packages/jest-jasmine2/src/index.js @@ -91,6 +91,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/src/index.js b/packages/jest-runtime/src/index.js index 91d6e6a19927..b05ddae6e24e 100644 --- a/packages/jest-runtime/src/index.js +++ b/packages/jest-runtime/src/index.js @@ -21,7 +21,12 @@ import path from 'path'; import HasteMap from 'jest-haste-map'; import {formatStackTrace, separateMessageFromStack} from 'jest-message-util'; import Resolver from 'jest-resolve'; -import {createDirectory, deepCyclicCopy} from 'jest-util'; +import { + FakeTimers, + FakeTimersLolex, + createDirectory, + deepCyclicCopy, +} from 'jest-util'; import {escapePathForRegex} from 'jest-regex-util'; import Snapshot from 'jest-snapshot'; import fs from 'graceful-fs'; @@ -92,6 +97,7 @@ class Runtime { _currentlyExecutingModulePath: string; _environment: Environment; _explicitShouldMock: BooleanObject; + _fakeTimersImplementation: FakeTimers<*> | FakeTimersLolex; _internalModuleRegistry: ModuleRegistry; _isCurrentlyExecutingManualMock: ?string; _mockFactories: {[key: string]: () => any, __proto__: null}; @@ -148,6 +154,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( @@ -934,12 +945,28 @@ class Runtime { this.restoreAllMocks(); return jestObject; }; - const useFakeTimers = () => { - this._environment.fakeTimers.useFakeTimers(); + const _getFakeTimers = () => { + 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; + } + + 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 = () => { - this._environment.fakeTimers.useRealTimers(); + // $FlowFixMe + _getFakeTimers().useRealTimers(); return jestObject; }; const resetModules = () => { @@ -967,17 +994,6 @@ class Runtime { return jestObject; }; - const _getFakeTimers = () => { - 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; - } - - return this._environment.fakeTimers; - }; - const jestObject = { addMatchers: (matchers: Object) => this._environment.global.jasmine.addMatchers(matchers), @@ -995,6 +1011,17 @@ class Runtime { fn, genMockFromModule: (moduleName: string) => this._generateMock(from, moduleName), + getRealSystemTime: () => { + const fakeTimers = _getFakeTimers(); + + if (fakeTimers instanceof FakeTimersLolex) { + return fakeTimers.getRealSystemTime(); + } else { + throw new TypeError( + 'getRealSystemTime is not available when not using Lolex', + ); + } + }, getTimerCount: () => _getFakeTimers().getTimerCount(), isMockFunction: this._moduleMocker.isMockFunction, isolateModules, @@ -1006,7 +1033,17 @@ class Runtime { resetModules, restoreAllMocks, retryTimes, - runAllImmediates: () => _getFakeTimers().runAllImmediates(), + runAllImmediates: () => { + const fakeTimers = _getFakeTimers(); + + if (fakeTimers instanceof FakeTimers) { + fakeTimers.runAllImmediates(); + } else { + throw new TypeError( + 'runAllImmediates is not available when using Lolex', + ); + } + }, runAllTicks: () => _getFakeTimers().runAllTicks(), runAllTimers: () => _getFakeTimers().runAllTimers(), runOnlyPendingTimers: () => _getFakeTimers().runOnlyPendingTimers(), @@ -1014,6 +1051,17 @@ class Runtime { _getFakeTimers().advanceTimersByTime(msToRun), setMock: (moduleName: string, mock: Object) => setMockFactory(moduleName, () => mock), + setSystemTime: (now?: number) => { + const fakeTimers = _getFakeTimers(); + + if (fakeTimers instanceof FakeTimersLolex) { + fakeTimers.setSystemTime(now); + } else { + throw new TypeError( + 'setSystemTime is not available when not using Lolex', + ); + } + }, 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/FakeTimersLolex.js b/packages/jest-util/src/FakeTimersLolex.js new file mode 100644 index 000000000000..88e1edead1c8 --- /dev/null +++ b/packages/jest-util/src/FakeTimersLolex.js @@ -0,0 +1,139 @@ +/** + * 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. + * + * @flow + */ + +import type {ProjectConfig} from 'types/Config'; +import type {Global} from 'types/Global'; +import type {lolex, Clock} from 'lolex'; + +import {withGlobal as lolexWithGlobal} from 'lolex'; +import {formatStackTrace} from 'jest-message-util'; + +export default class FakeTimers { + _clock: Clock; + _config: ProjectConfig; + _fakingTime: boolean; + _global: Global; + _lolex: lolex; + _maxLoops: number; + + constructor({ + global, + config, + maxLoops, + }: { + global: Global, + config: ProjectConfig, + 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()) { + this._clock.runMicrotasks(); + } + } + + useRealTimers() { + if (this._fakingTime) { + this._clock.uninstall(); + this._fakingTime = false; + } + } + + useFakeTimers() { + const toFake = Object.keys(this._lolex.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-util/src/__tests__/__snapshots__/fakeTimersLolex.test.js.snap b/packages/jest-util/src/__tests__/__snapshots__/fakeTimersLolex.test.js.snap new file mode 100644 index 000000000000..39cc8dd4ad64 --- /dev/null +++ b/packages/jest-util/src/__tests__/__snapshots__/fakeTimersLolex.test.js.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-util/src/__tests__/fakeTimers.test.js b/packages/jest-util/src/__tests__/fakeTimers.test.js index c39fb0c2a4e4..d74d250431f1 100644 --- a/packages/jest-util/src/__tests__/fakeTimers.test.js +++ b/packages/jest-util/src/__tests__/fakeTimers.test.js @@ -6,16 +6,14 @@ * */ -'use strict'; - -const vm = require('vm'); +import vm from 'vm'; +import mock from 'jest-mock'; +import FakeTimers from '../FakeTimers'; describe('FakeTimers', () => { - let FakeTimers, moduleMocker, timerConfig; + let moduleMocker, timerConfig; beforeEach(() => { - FakeTimers = require('../FakeTimers').default; - const mock = require('jest-mock'); const global = vm.runInNewContext('this'); moduleMocker = new mock.ModuleMocker(global); diff --git a/packages/jest-util/src/__tests__/fakeTimersLolex.test.js b/packages/jest-util/src/__tests__/fakeTimersLolex.test.js new file mode 100644 index 000000000000..51bf4d4f5ef8 --- /dev/null +++ b/packages/jest-util/src/__tests__/fakeTimersLolex.test.js @@ -0,0 +1,722 @@ +/** + * 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); + }); + }); +}); diff --git a/packages/jest-util/src/index.js b/packages/jest-util/src/index.js index 04aed91a67bb..664a9294d5e8 100644 --- a/packages/jest-util/src/index.js +++ b/packages/jest-util/src/index.js @@ -13,6 +13,7 @@ import CustomConsole from './CustomConsole'; import createDirectory from './createDirectory'; import ErrorWithStack from './ErrorWithStack'; import FakeTimers from './FakeTimers'; +import FakeTimersLolex from './FakeTimersLolex'; import formatTestResults from './formatTestResults'; import getFailedSnapshotTests from './getFailedSnapshotTests'; import getConsoleOutput from './getConsoleOutput'; @@ -31,6 +32,7 @@ module.exports = { Console: CustomConsole, ErrorWithStack, FakeTimers, + FakeTimersLolex, NullConsole, clearLine, convertDescriptorToString, diff --git a/types/Config.js b/types/Config.js index 375278326abb..cc463ca7c516 100644 --- a/types/Config.js +++ b/types/Config.js @@ -296,7 +296,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/types/Environment.js b/types/Environment.js index b11fc9658370..34a6db985ec4 100644 --- a/types/Environment.js +++ b/types/Environment.js @@ -11,6 +11,7 @@ import type {ProjectConfig} from './Config'; import type {Global} from './Global'; import type {Script} from 'vm'; import type {ModuleMocker} from 'jest-mock'; +import {FakeTimers, FakeTimersLolex} from 'jest-util'; export type EnvironmentContext = { console?: Object, @@ -21,18 +22,8 @@ declare class $JestEnvironment { constructor(config: ProjectConfig, context?: EnvironmentContext): void; runScript(script: Script): any; global: Global; - fakeTimers: { - clearAllTimers(): void, - runAllImmediates(): void, - runAllTicks(): void, - runAllTimers(): void, - advanceTimersByTime(msToRun: number): void, - runOnlyPendingTimers(): void, - runWithRealTimers(callback: any): void, - getTimerCount(): number, - useFakeTimers(): void, - useRealTimers(): void, - }; + fakeTimers: FakeTimers<*>; + fakeTimersLolex: FakeTimersLolex; testFilePath: string; moduleMocker: ModuleMocker; setup(): Promise; diff --git a/types/Jest.js b/types/Jest.js index 23f7c8e04996..fabe3fad00e1 100644 --- a/types/Jest.js +++ b/types/Jest.js @@ -24,6 +24,7 @@ export type Jest = {| enableAutomock(): Jest, fn: (implementation?: Function) => JestMockFn, genMockFromModule(moduleName: string): any, + getRealSystemTime(): number, isMockFunction(fn: Function): boolean, mock(moduleName: string, moduleFactory?: any, options?: Object): Jest, requireActual: LocalModuleRequire, @@ -37,6 +38,7 @@ export type Jest = {| runAllTicks(): void, runAllTimers(): void, runOnlyPendingTimers(): void, + 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"