From ee23815b7f7fddab4e17f3e4d02afdf2d0061a48 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.ts | 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 +- packages/jest-environment-jsdom/src/index.ts | 14 +- packages/jest-environment-node/src/index.ts | 33 +- packages/jest-environment/src/index.ts | 9 +- packages/jest-fake-timers/package.json | 6 +- .../__snapshots__/jestFakeTimers.test.ts.snap | 6 +- .../src/__tests__/jestFakeTimers.test.ts | 774 ++++-------------- .../jest-fake-timers/src/jestFakeTimers.ts | 517 ++---------- .../src/__tests__/pTimeout.test.ts | 5 + packages/jest-runtime/src/index.ts | 3 +- yarn.lock | 10 + 23 files changed, 291 insertions(+), 1184 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%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83864ba1d072..9f2b424ee386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -186,6 +186,7 @@ We skipped 24.2.0 because a draft was accidentally published. Please use `24.3.0 - `[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 1a6cb5afe945..12166432e6c3 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -527,10 +527,6 @@ When this API is called, all pending macro-tasks and micro-tasks will be execute 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+** @@ -557,6 +553,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.ts b/e2e/__tests__/fakePromises.test.ts index 5a5b78c159f3..001afc2f43b4 100644 --- a/e2e/__tests__/fakePromises.test.ts +++ b/e2e/__tests__/fakePromises.test.ts @@ -8,13 +8,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/packages/jest-environment-jsdom/src/index.ts b/packages/jest-environment-jsdom/src/index.ts index 8d0c33c1df7c..6a34af6860df 100644 --- a/packages/jest-environment-jsdom/src/index.ts +++ b/packages/jest-environment-jsdom/src/index.ts @@ -30,7 +30,7 @@ function isWin(globals: Win | Global.Global): globals is Win { class JSDOMEnvironment implements JestEnvironment { dom: JSDOM | null; - fakeTimers: FakeTimers | null; + fakeTimers: FakeTimers | null; // @ts-ignore global: Global.Global | Win | null; errorEventListener: ((event: Event & {error: Error}) => void) | null; @@ -87,17 +87,7 @@ class JSDOMEnvironment implements JestEnvironment { this.moduleMocker = new mock.ModuleMocker(global as any); - const timerConfig = { - idToRef: (id: number) => id, - refToId: (ref: number) => ref, - }; - - this.fakeTimers = new FakeTimers({ - config, - global: global as any, - moduleMocker: this.moduleMocker, - timerConfig, - }); + this.fakeTimers = new FakeTimers({config, global: global as any}); } setup() { diff --git a/packages/jest-environment-node/src/index.ts b/packages/jest-environment-node/src/index.ts index bc82af123b5e..ef6f8b13abb8 100644 --- a/packages/jest-environment-node/src/index.ts +++ b/packages/jest-environment-node/src/index.ts @@ -12,15 +12,9 @@ import {installCommonGlobals} from 'jest-util'; import {JestFakeTimers as FakeTimers} from '@jest/fake-timers'; import {JestEnvironment} from '@jest/environment'; -type Timer = { - id: number; - ref: () => Timer; - unref: () => Timer; -}; - class NodeEnvironment implements JestEnvironment { context: Context | null; - fakeTimers: FakeTimers | null; + fakeTimers: FakeTimers | null; global: Global.Global; moduleMocker: ModuleMocker | null; @@ -54,30 +48,7 @@ class NodeEnvironment implements JestEnvironment { installCommonGlobals(global, config.globals); this.moduleMocker = new ModuleMocker(global); - const timerIdToRef = (id: number) => ({ - id, - ref() { - return this; - }, - unref() { - return this; - }, - }); - - const timerRefToId = (timer: Timer): number | undefined => - (timer && timer.id) || undefined; - - const timerConfig = { - idToRef: timerIdToRef, - refToId: timerRefToId, - }; - - this.fakeTimers = new FakeTimers({ - config, - global, - moduleMocker: this.moduleMocker, - timerConfig, - }); + this.fakeTimers = new FakeTimers({config, global}); } setup() { diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index f1b262c6ce98..e7dcab689dce 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -26,7 +26,7 @@ export declare class JestEnvironment { constructor(config: Config.ProjectConfig); constructor(config: Config.ProjectConfig, context: EnvironmentContext); global: Global.Global; - fakeTimers: FakeTimers | null; + fakeTimers: FakeTimers | null; moduleMocker: ModuleMocker | null; runScript( script: Script, @@ -162,10 +162,6 @@ export interface Jest { * retries is exhausted. This only works with `jest-circus`! */ retryTimes(numRetries: number): Jest; - /** - * Exhausts tasks queued by setImmediate(). - */ - runAllImmediates(): void; /** * Exhausts the micro-task queue (usually interfaced in node via * process.nextTick). @@ -246,4 +242,7 @@ export interface Jest { * every test so that local module state doesn't conflict between tests. */ isolateModules(fn: () => void): Jest; + + getRealSystemTime(): number; + setSystemTime(now?: number): void; } diff --git a/packages/jest-fake-timers/package.json b/packages/jest-fake-timers/package.json index d575484092d7..ec1ec0966a62 100644 --- a/packages/jest-fake-timers/package.json +++ b/packages/jest-fake-timers/package.json @@ -12,7 +12,11 @@ "dependencies": { "@jest/types": "^24.5.0", "jest-message-util": "^24.5.0", - "jest-mock": "^24.5.0" + "jest-mock": "^24.5.0", + "lolex": "^3.1.0" + }, + "devDependencies": { + "@types/lolex": "^3.1.1" }, "engines": { "node": ">= 6" diff --git a/packages/jest-fake-timers/src/__tests__/__snapshots__/jestFakeTimers.test.ts.snap b/packages/jest-fake-timers/src/__tests__/__snapshots__/jestFakeTimers.test.ts.snap index 5e0d7432996b..39cc8dd4ad64 100644 --- a/packages/jest-fake-timers/src/__tests__/__snapshots__/jestFakeTimers.test.ts.snap +++ b/packages/jest-fake-timers/src/__tests__/__snapshots__/jestFakeTimers.test.ts.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-fake-timers/src/__tests__/jestFakeTimers.test.ts b/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts index 483d2a7ca5a5..eb2198f23cab 100644 --- a/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts @@ -3,108 +3,66 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. + * */ -import vm from 'vm'; -import mock from 'jest-mock'; import FakeTimers from '../jestFakeTimers'; -const timerConfig = { - idToRef: (id: number) => id, - refToId: (ref: number) => ref, -}; - -const config = { - rootDir: '/', - testMatch: [], -}; - describe('FakeTimers', () => { - let moduleMocker: mock.ModuleMocker; - - beforeEach(() => { - const global = vm.runInNewContext('this'); - moduleMocker = new mock.ModuleMocker(global); - }); - describe('construction', () => { it('installs setTimeout mock', () => { - const global = ({process} as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - 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} as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - 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} as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - 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} as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + 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 = ({ + const global = { + Date, + clearTimeout, process: { nextTick: origNextTick, }, - } as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + 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 = ({ + const global = { + Date, + clearTimeout, process, setImmediate: origSetImmediate, - } as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + setTimeout, + }; + const timers = new FakeTimers({global}); timers.useFakeTimers(); expect(global.setImmediate).not.toBe(origSetImmediate); }); @@ -112,17 +70,15 @@ describe('FakeTimers', () => { it('mocks clearImmediate if setImmediate is on global', () => { const origSetImmediate = () => {}; const origClearImmediate = () => {}; - const global = ({ + const global = { + Date, clearImmediate: origClearImmediate, + clearTimeout, process, setImmediate: origSetImmediate, - } as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + setTimeout, + }; + const timers = new FakeTimers({global}); timers.useFakeTimers(); expect(global.clearImmediate).not.toBe(origClearImmediate); }); @@ -130,21 +86,19 @@ describe('FakeTimers', () => { describe('runAllTicks', () => { it('runs all ticks, in order', () => { - const global = ({ + const global = { + Date, + clearTimeout, process: { nextTick: () => {}, }, - } as unknown) as NodeJS.Global; + setTimeout, + }; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + const timers = new FakeTimers({global}); timers.useFakeTimers(); - const runOrder: Array = []; + const runOrder = []; const mock1 = jest.fn(() => runOrder.push('mock1')); const mock2 = jest.fn(() => runOrder.push('mock2')); @@ -163,18 +117,16 @@ describe('FakeTimers', () => { it('does nothing when no ticks have been scheduled', () => { const nextTick = jest.fn(); - const global = ({ + const global = { + Date, + clearTimeout, process: { nextTick, }, - } as unknown) as NodeJS.Global; + setTimeout, + }; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + const timers = new FakeTimers({global}); timers.useFakeTimers(); timers.runAllTicks(); @@ -182,18 +134,16 @@ describe('FakeTimers', () => { }); it('only runs a scheduled callback once', () => { - const global = ({ + const global = { + Date, + clearTimeout, process: { nextTick: () => {}, }, - } as unknown) as NodeJS.Global; + setTimeout, + }; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + const timers = new FakeTimers({global}); timers.useFakeTimers(); const mock1 = jest.fn(); @@ -207,162 +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, - }, - } as unknown) as NodeJS.Global; - - const timers = new FakeTimers({ - config, - 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, - } as unknown) as NodeJS.Global; - - const timers = new FakeTimers({ - config, - 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, - }, - } as unknown) as NodeJS.Global; - - const timers = new FakeTimers({ - config, - 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, - } as unknown) as NodeJS.Global; - - const timers = new FakeTimers({ - config, - 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, - } as unknown) as NodeJS.Global; - - const timers = new FakeTimers({ - config, - 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 = ({ + const global = { + Date, + clearTimeout, process: { nextTick: () => {}, }, - } as unknown) as NodeJS.Global; + setTimeout, + }; - const timers = new FakeTimers({ - config, - global, - maxLoops: 100, - moduleMocker, - timerConfig, - }); + const timers = new FakeTimers({global, maxLoops: 100}); timers.useFakeTimers(); @@ -373,26 +178,18 @@ 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} as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); - const runOrder: Array = []; + const runOrder = []; const mock1 = jest.fn(() => runOrder.push('mock1')); const mock2 = jest.fn(() => runOrder.push('mock2')); const mock3 = jest.fn(() => runOrder.push('mock3')); @@ -422,50 +219,38 @@ describe('FakeTimers', () => { }); it('warns when trying to advance timers while real timers are used', () => { - const consoleWarn = jest - .spyOn(console, 'warn') - .mockImplementation(() => {}); + const consoleWarn = console.warn; + console.warn = jest.fn(); const timers = new FakeTimers({ config: { rootDir: __dirname, - testMatch: [], }, global, - moduleMocker, - timerConfig, }); timers.runAllTimers(); expect( - consoleWarn.mock.calls[0][0].split('\nStack Trace')[0], + console.warn.mock.calls[0][0].split('\nStack Trace')[0], ).toMatchSnapshot(); - consoleWarn.mockRestore(); + console.warn = consoleWarn; }); it('does nothing when no timers have been scheduled', () => { const nativeSetTimeout = jest.fn(); - const global = ({ + const global = { + Date, + clearTimeout, process, setTimeout: nativeSetTimeout, - } as unknown) as NodeJS.Global; + }; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + const timers = new FakeTimers({global}); timers.useFakeTimers(); timers.runAllTimers(); }); it('only runs a setTimeout callback once (ever)', () => { - const global = ({process} as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const fn = jest.fn(); @@ -480,13 +265,8 @@ describe('FakeTimers', () => { }); it('runs callbacks with arguments after the interval', () => { - const global = ({process} as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const fn = jest.fn(); @@ -497,20 +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 = ({ + const global = { + Date, + clearTimeout, process, setTimeout: nativeSetTimeout, - } as unknown) as NodeJS.Global; + }; - const timers = new FakeTimers({ - config, - 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(); @@ -522,14 +302,8 @@ describe('FakeTimers', () => { }); it('throws before allowing infinite recursion', () => { - const global = ({process} as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - maxLoops: 100, - moduleMocker, - timerConfig, - }); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global, maxLoops: 100}); timers.useFakeTimers(); global.setTimeout(function infinitelyRecursingCallback() { @@ -540,20 +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} as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const fn = jest.fn(); @@ -569,16 +337,11 @@ describe('FakeTimers', () => { describe('advanceTimersByTime', () => { it('runs timers in order', () => { - const global = ({process} as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); - const runOrder: Array = []; + const runOrder = []; const mock1 = jest.fn(() => runOrder.push('mock1')); const mock2 = jest.fn(() => runOrder.push('mock2')); const mock3 = jest.fn(() => runOrder.push('mock3')); @@ -613,53 +376,18 @@ describe('FakeTimers', () => { }); it('does nothing when no timers have been scheduled', () => { - const global = ({process} as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - 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} as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - 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} as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const mock1 = jest.fn(); @@ -671,13 +399,8 @@ describe('FakeTimers', () => { }); it('resets all pending setIntervals', () => { - const global = ({process} as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const mock1 = jest.fn(); @@ -688,19 +411,17 @@ describe('FakeTimers', () => { expect(mock1).toHaveBeenCalledTimes(0); }); - it('resets all pending ticks callbacks & immediates', () => { - const global = ({ + it('resets all pending ticks callbacks', () => { + const global = { + Date, + clearTimeout, process: { nextTick: () => {}, }, setImmediate: () => {}, - } as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + setTimeout, + }; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const mock1 = jest.fn(); @@ -709,18 +430,12 @@ describe('FakeTimers', () => { timers.reset(); timers.runAllTicks(); - timers.runAllImmediates(); expect(mock1).toHaveBeenCalledTimes(0); }); it('resets current advanceTimersByTime time cursor', () => { - const global = ({process} as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const mock1 = jest.fn(); @@ -739,20 +454,18 @@ describe('FakeTimers', () => { it('runs all timers in order', () => { const nativeSetImmediate = jest.fn(); - const global = ({ + const global = { + Date, + clearTimeout, process, setImmediate: nativeSetImmediate, - } as unknown) as NodeJS.Global; + setTimeout, + }; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + const timers = new FakeTimers({global}); timers.useFakeTimers(); - const runOrder: Array = []; + const runOrder = []; global.setTimeout(function cb() { runOrder.push('mock1'); @@ -761,7 +474,7 @@ describe('FakeTimers', () => { global.setTimeout(function cb() { runOrder.push('mock2'); - global.setTimeout(cb, 0); + global.setTimeout(cb, 50); }, 0); global.setInterval(() => { @@ -778,31 +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} as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); timers.useFakeTimers(); const fn = jest.fn(); @@ -816,152 +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, - } as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); - timers.useFakeTimers(); - - // clearInterval() - timers.runWithRealTimers(() => { - (global as any).clearInterval(); - }); - expect(nativeClearInterval).toHaveBeenCalledTimes(1); - expect(global.clearInterval).toHaveBeenCalledTimes(0); - - // clearTimeout() - timers.runWithRealTimers(() => { - (global as any).clearTimeout(); - }); - expect(nativeClearTimeout).toHaveBeenCalledTimes(1); - expect(global.clearTimeout).toHaveBeenCalledTimes(0); - - // setInterval() - timers.runWithRealTimers(() => { - (global as any).setInterval(); - }); - expect(nativeSetInterval).toHaveBeenCalledTimes(1); - expect(global.setInterval).toHaveBeenCalledTimes(0); - - // setTimeout() - timers.runWithRealTimers(() => { - (global as any).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, - } as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); - timers.useFakeTimers(); - - // clearInterval() - timers.runWithRealTimers(() => { - (global as any).clearInterval(); - }); - expect(nativeClearInterval).toHaveBeenCalledTimes(1); - expect(global.clearInterval).toHaveBeenCalledTimes(0); - - (global as any).clearInterval(); - expect(nativeClearInterval).toHaveBeenCalledTimes(1); - expect(global.clearInterval).toHaveBeenCalledTimes(1); - - // clearTimeout() - timers.runWithRealTimers(() => { - (global as any).clearTimeout(); - }); - expect(nativeClearTimeout).toHaveBeenCalledTimes(1); - expect(global.clearTimeout).toHaveBeenCalledTimes(0); - - (global as any).clearTimeout(); - expect(nativeClearTimeout).toHaveBeenCalledTimes(1); - expect(global.clearTimeout).toHaveBeenCalledTimes(1); - - // setInterval() - timers.runWithRealTimers(() => { - (global as any).setInterval(); - }); - expect(nativeSetInterval).toHaveBeenCalledTimes(1); - expect(global.setInterval).toHaveBeenCalledTimes(0); - - (global as any).setInterval(); - expect(nativeSetInterval).toHaveBeenCalledTimes(1); - expect(global.setInterval).toHaveBeenCalledTimes(1); - - // setTimeout() - timers.runWithRealTimers(() => { - (global as any).setTimeout(); - }); - expect(nativeSetTimeout).toHaveBeenCalledTimes(1); - expect(global.setTimeout).toHaveBeenCalledTimes(0); - - (global as any).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, - } as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); - timers.useFakeTimers(); - - expect(() => { - timers.runWithRealTimers(() => { - (global as any).setTimeout(); - throw new Error('test'); - }); - }).toThrow(new Error('test')); - expect(nativeSetTimeout).toHaveBeenCalledTimes(1); - expect(global.setTimeout).toHaveBeenCalledTimes(0); - - (global as any).setTimeout(); - expect(nativeSetTimeout).toHaveBeenCalledTimes(1); - expect(global.setTimeout).toHaveBeenCalledTimes(1); - }); - }); - describe('useRealTimers', () => { it('resets native timer APIs', () => { const nativeSetTimeout = jest.fn(); @@ -969,19 +543,15 @@ describe('FakeTimers', () => { const nativeClearTimeout = jest.fn(); const nativeClearInterval = jest.fn(); - const global = ({ + const global = { + Date, clearInterval: nativeClearInterval, clearTimeout: nativeClearTimeout, process, setInterval: nativeSetInterval, setTimeout: nativeSetTimeout, - } as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + }; + const timers = new FakeTimers({global}); timers.useFakeTimers(); // Ensure that timers has overridden the native timer APIs @@ -1002,15 +572,13 @@ describe('FakeTimers', () => { it('resets native process.nextTick when present', () => { const nativeProcessNextTick = jest.fn(); - const global = ({ + const global = { + Date, + clearTimeout, process: {nextTick: nativeProcessNextTick}, - } as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + setTimeout, + }; + const timers = new FakeTimers({global}); timers.useFakeTimers(); // Ensure that timers has overridden the native timer APIs @@ -1026,17 +594,15 @@ describe('FakeTimers', () => { const nativeSetImmediate = jest.fn(); const nativeClearImmediate = jest.fn(); - const global = ({ + const global = { + Date, clearImmediate: nativeClearImmediate, + clearTimeout, process, setImmediate: nativeSetImmediate, - } as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + setTimeout, + }; + const timers = new FakeTimers({global}); timers.useFakeTimers(); // Ensure that timers has overridden the native timer APIs @@ -1058,19 +624,15 @@ describe('FakeTimers', () => { const nativeClearTimeout = jest.fn(); const nativeClearInterval = jest.fn(); - const global = ({ + const global = { + Date, clearInterval: nativeClearInterval, clearTimeout: nativeClearTimeout, process, setInterval: nativeSetInterval, setTimeout: nativeSetTimeout, - } as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + }; + const timers = new FakeTimers({global}); timers.useRealTimers(); // Ensure that the real timers are installed at this point @@ -1091,15 +653,13 @@ describe('FakeTimers', () => { it('resets mock process.nextTick when present', () => { const nativeProcessNextTick = jest.fn(); - const global = ({ + const global = { + Date, + clearTimeout, process: {nextTick: nativeProcessNextTick}, - } as unknown) as NodeJS.Global; - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + setTimeout, + }; + const timers = new FakeTimers({global}); timers.useRealTimers(); // Ensure that the real timers are installed at this point @@ -1115,17 +675,15 @@ describe('FakeTimers', () => { const nativeSetImmediate = jest.fn(); const nativeClearImmediate = jest.fn(); - const global = ({ + const global = { + Date, clearImmediate: nativeClearImmediate, + clearTimeout, process, setImmediate: nativeSetImmediate, - } as unknown) as NodeJS.Global; - const fakeTimers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + setTimeout, + }; + const fakeTimers = new FakeTimers({global}); fakeTimers.useRealTimers(); // Ensure that the real timers are installed at this point @@ -1142,12 +700,7 @@ describe('FakeTimers', () => { describe('getTimerCount', () => { it('returns the correct count', () => { - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + const timers = new FakeTimers({global}); timers.useFakeTimers(); @@ -1167,12 +720,7 @@ describe('FakeTimers', () => { }); it('includes immediates and ticks', () => { - const timers = new FakeTimers({ - config, - global, - moduleMocker, - timerConfig, - }); + const timers = new FakeTimers({global}); timers.useFakeTimers(); diff --git a/packages/jest-fake-timers/src/jestFakeTimers.ts b/packages/jest-fake-timers/src/jestFakeTimers.ts index a8ed4b4509e7..084c9d710578 100644 --- a/packages/jest-fake-timers/src/jestFakeTimers.ts +++ b/packages/jest-fake-timers/src/jestFakeTimers.ts @@ -5,519 +5,136 @@ * LICENSE file in the root directory of this source tree. */ -import {ModuleMocker} from 'jest-mock'; +import { + withGlobal as lolexWithGlobal, + LolexWithContext, + InstalledClock, +} from 'lolex'; import {formatStackTrace, StackTraceConfig} from 'jest-message-util'; -type Callback = (...args: Array) => void; - -type TimerID = string; - -type Tick = { - uuid: string; - callback: Callback; -}; - -type Timer = { - type: string; - callback: Callback; - expiry: number; - interval?: number; -}; - -type TimerAPI = { - clearImmediate: typeof global.clearImmediate; - clearInterval: typeof global.clearInterval; - clearTimeout: typeof global.clearTimeout; - nextTick: typeof process.nextTick; - - setImmediate: typeof global.setImmediate; - setInterval: typeof global.setInterval; - setTimeout: typeof global.setTimeout; -}; - -type TimerConfig = { - idToRef: (id: number) => Ref; - refToId: (ref: Ref) => number | void; -}; - -const MS_IN_A_YEAR = 31536000000; - -// TODO: Copied from `jest-util` to avoid cyclic dependency. Import from `jest-util` in the next major -const setGlobal = ( - globalToMutate: NodeJS.Global | Window, - key: string, - value: unknown, -) => { - // @ts-ignore: no index - globalToMutate[key] = value; -}; - -export default class FakeTimers { - private _cancelledImmediates!: {[key: string]: boolean}; - private _cancelledTicks!: {[key: string]: boolean}; +export default class FakeTimers { + private _clock!: InstalledClock; private _config: StackTraceConfig; - private _disposed?: boolean; - private _fakeTimerAPIs!: TimerAPI; + private _fakingTime: boolean; private _global: NodeJS.Global; - private _immediates!: Array; + private _lolex: LolexWithContext; private _maxLoops: number; - private _moduleMocker: ModuleMocker; - private _now!: number; - private _ticks!: Array; - private _timerAPIs: TimerAPI; - private _timers!: Map; - private _uuidCounter: number; - private _timerConfig: TimerConfig; constructor({ global, - moduleMocker, - timerConfig, config, maxLoops, }: { global: NodeJS.Global; - moduleMocker: ModuleMocker; - timerConfig: TimerConfig; config: StackTraceConfig; 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), - ); - this._timers.clear(); - } - - dispose() { - this._disposed = true; - this.clearAllTimers(); - } - - reset() { - this._cancelledTicks = {}; - this._cancelledImmediates = {}; - this._now = 0; - this._ticks = []; - this._immediates = []; - this._timers = new Map(); - } - - 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...", - ); - } - } - - 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...", - ); + if (this._fakingTime) { + this._clock.reset(); } } - private _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(); - } + dispose() { + this.useRealTimers(); } 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...", - ); + if (this._checkFakeTimers()) { + this._clock.runAll(); } } runOnlyPendingTimers() { - // We need to hold the current shape of `this._timers` because existing - // timers can add new ones to the map and hence would run more than necessary. - // See https://github.com/facebook/jest/pull/4608 for details - const timerEntries = Array.from(this._timers.entries()); - this._checkFakeTimers(); - this._immediates.forEach(this._runImmediate, this); - - timerEntries - .sort(([, left], [, right]) => left.expiry - right.expiry) - .forEach(([timerHandle]) => this._runTimerHandle(timerHandle)); + if (this._checkFakeTimers()) { + this._clock.runToLast(); + } } 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 timerValue = this._timers.get(timerHandle); - if (timerValue === undefined) { - break; - } - const nextTimerExpiry = timerValue.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...", - ); + if (this._checkFakeTimers()) { + this._clock.tick(msToRun); } } - 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; - - this.useRealTimers(); - - let cbErr = null; - let errThrown = false; - try { - cb(); - } catch (e) { - errThrown = true; - cbErr = e; - } - - 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; + runAllTicks() { + if (this._checkFakeTimers()) { + // @ts-ignore + this._clock.runMicrotasks(); } } 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; + if (this._fakingTime) { + this._clock.uninstall(); + this._fakingTime = false; + } } 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; - } - - getTimerCount() { - this._checkFakeTimers(); + const toFake = Object.keys(this._lolex.timers) as Array< + keyof LolexWithContext['timers'] + >; - return this._timers.size + this._immediates.length + this._ticks.length; - } + if (!this._fakingTime) { + this._clock = this._lolex.install({ + loopLimit: this._maxLoops, + now: Date.now(), + target: this._global, + toFake, + }); - private _checkFakeTimers() { - if (this._global.setTimeout !== this._fakeTimerAPIs.setTimeout) { - 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` + - formatStackTrace(new Error().stack!, this._config, { - noStackTrace: false, - }), - ); + this._fakingTime = true; } } - private _createMocks() { - const fn = (impl: Function) => - // @ts-ignore TODO: figure out better typings here - this._moduleMocker.fn().mockImplementation(impl); - - // TODO: add better typings; these are mocks, but typed as regular timers - 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)), - }; - } - - private _fakeClearTimer(timerRef: TimerRef) { - const uuid = this._timerConfig.refToId(timerRef); - - if (uuid) { - this._timers.delete(String(uuid)); + reset() { + if (this._checkFakeTimers()) { + const {now} = this._clock; + this._clock.reset(); + this._clock.setSystemTime(now); } } - private _fakeClearImmediate(uuid: TimerID) { - this._cancelledImmediates[uuid] = true; - } - - private _fakeNextTick(callback: Callback, ...args: Array) { - if (this._disposed) { - return; + setSystemTime(now?: number) { + if (this._checkFakeTimers()) { + this._clock.setSystemTime(now); } - - 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); - } - }); } - private _fakeSetImmediate(callback: Callback, ...args: Array) { - if (this._disposed) { - return null; - } - - 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; + getRealSystemTime() { + return Date.now(); } - private _fakeSetInterval( - callback: Callback, - intervalDelay?: number, - ...args: Array - ) { - if (this._disposed) { - return null; - } - - if (intervalDelay == null) { - intervalDelay = 0; - } - - const uuid = this._uuidCounter++; - - this._timers.set(String(uuid), { - callback: () => callback.apply(null, args), - expiry: this._now + intervalDelay, - interval: intervalDelay, - type: 'interval', - }); - - return this._timerConfig.idToRef(uuid); - } - - private _fakeSetTimeout( - callback: Callback, - delay?: number, - ...args: Array - ) { - if (this._disposed) { - return null; + getTimerCount() { + if (this._checkFakeTimers()) { + return this._clock.countTimers(); } - // eslint-disable-next-line no-bitwise - delay = Number(delay) | 0; - - const uuid = this._uuidCounter++; - - this._timers.set(String(uuid), { - callback: () => callback.apply(null, args), - expiry: this._now + delay, - interval: undefined, - type: 'timeout', - }); - - return this._timerConfig.idToRef(uuid); + return 0; } - private _getNextTimerHandle() { - let nextTimerHandle = null; - let soonestTime = MS_IN_A_YEAR; - - this._timers.forEach((timer, uuid) => { - if (timer.expiry < soonestTime) { - soonestTime = timer.expiry; - nextTimerHandle = uuid; - } - }); - - return nextTimerHandle; - } - - private _runTimerHandle(timerHandle: TimerID) { - const timer = this._timers.get(timerHandle); - - if (!timer) { - return; + _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, + }), + ); } - switch (timer.type) { - case 'timeout': - const callback = timer.callback; - this._timers.delete(timerHandle); - callback(); - break; - - case 'interval': - timer.expiry = this._now + (timer.interval || 0); - timer.callback(); - break; - - default: - throw new Error('Unexpected timer type: ' + timer.type); - } + return this._fakingTime; } } diff --git a/packages/jest-jasmine2/src/__tests__/pTimeout.test.ts b/packages/jest-jasmine2/src/__tests__/pTimeout.test.ts index fef80a2bd541..9907d1f5945f 100644 --- a/packages/jest-jasmine2/src/__tests__/pTimeout.test.ts +++ b/packages/jest-jasmine2/src/__tests__/pTimeout.test.ts @@ -11,6 +11,11 @@ jest.useFakeTimers(); import pTimeout from '../pTimeout'; describe('pTimeout', () => { + beforeEach(() => { + jest.spyOn(global, 'setTimeout'); + jest.spyOn(global, 'clearTimeout'); + }); + it('calls `clearTimeout` and resolves when `promise` resolves.', async () => { const onTimeout = jest.fn(); const promise = Promise.resolve(); diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 067c247fdaf9..60301bd2de47 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -1028,6 +1028,7 @@ class Runtime { fn, genMockFromModule: (moduleName: string) => this._generateMock(from, moduleName), + getRealSystemTime: () => _getFakeTimers().getRealSystemTime(), getTimerCount: () => _getFakeTimers().getTimerCount(), isMockFunction: this._moduleMocker.isMockFunction, isolateModules, @@ -1039,7 +1040,6 @@ class Runtime { resetModules, restoreAllMocks, retryTimes, - runAllImmediates: () => _getFakeTimers().runAllImmediates(), runAllTicks: () => _getFakeTimers().runAllTicks(), runAllTimers: () => _getFakeTimers().runAllTimers(), runOnlyPendingTimers: () => _getFakeTimers().runOnlyPendingTimers(), @@ -1047,6 +1047,7 @@ class Runtime { _getFakeTimers().advanceTimersByTime(msToRun), setMock: (moduleName: string, mock: unknown) => setMockFactory(moduleName, () => mock), + setSystemTime: (now?: number) => _getFakeTimers().setSystemTime(now), setTimeout, spyOn, unmock, diff --git a/yarn.lock b/yarn.lock index 93b6c8ef0001..9d9ee4e80ff5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1770,6 +1770,11 @@ resolved "https://registry.yarnpkg.com/@types/leven/-/leven-2.1.1.tgz#7cdc02ec636f80dc0bb0a53d8ee7eff2d8e8e1d8" integrity sha512-f74SsCQnQzm244o5LHZgSLijrwG5e9BgkMHGbDlQThfh42q5RG4c+RNzUvZ347wAlQYD9kwu64qSNylxZdKs6w== +"@types/lolex@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/lolex/-/lolex-3.1.1.tgz#d40895223e5c8f8aa64f5500c6ca4eeab067d432" + integrity sha512-NU2qVtKxbt4IBvjEOW1QeUnV6KGUF6hpgJyvwZt3JrXe2qmwQF0+BiazQw+iFy9qL5ie+QHOxTzXkcvJUEh76g== + "@types/merge-stream@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@types/merge-stream/-/merge-stream-1.1.2.tgz#a880ff66b1fbbb5eef4958d015c5947a9334dbb1" @@ -8333,6 +8338,11 @@ logalot@^2.0.0: figures "^1.3.5" squeak "^1.0.0" +lolex@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-3.1.0.tgz#1a7feb2fefd75b3e3a7f79f0e110d9476e294434" + integrity sha512-zFo5MgCJ0rZ7gQg69S4pqBsLURbFw11X68C18OcJjJQbqaXm2NoTrGl1IMM3TIz0/BnN1tIs2tzmmqvCsOMMjw== + longest@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"