From 9279a3a97a39cc67f289635e4911a009ecfec60e Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 6 Sep 2019 10:42:12 +0200 Subject: [PATCH] feat: add fake timers implementation backed by Lolex (#8897) --- CHANGELOG.md | 1 + .../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-fake-timers/package.json | 6 +- .../jest-fake-timers/src/FakeTimersLolex.ts | 154 ++++ .../fakeTimersLolex.test.ts.snap | 3 + .../src/__tests__/fakeTimersLolex.test.ts | 848 ++++++++++++++++++ .../src/__tests__/jestFakeTimers.test.ts | 2 +- packages/jest-fake-timers/src/index.ts | 1 + .../src/__tests__/pTimeout.test.ts | 5 + yarn.lock | 10 + 13 files changed, 1049 insertions(+), 19 deletions(-) create mode 100644 packages/jest-fake-timers/src/FakeTimersLolex.ts create mode 100644 packages/jest-fake-timers/src/__tests__/__snapshots__/fakeTimersLolex.test.ts.snap create mode 100644 packages/jest-fake-timers/src/__tests__/fakeTimersLolex.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c8bd846baeb4..f83f547771a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - `[jest-diff]` [**BREAKING**] Export as ECMAScript module ([#8873](https://github.com/facebook/jest/pull/8873)) - `[jest-diff]` Add `includeChangeCounts` and rename `Indicator` options ([#8881](https://github.com/facebook/jest/pull/8881)) - `[jest-diff]` Add `changeColor` and `patchColor` options ([#8911](https://github.com/facebook/jest/pull/8911)) +- `[@jest/fake-timers]` Add Lolex as implementation of fake timers ([#8897](https://github.com/facebook/jest/pull/8897)) - `[jest-runner]` Warn if a worker had to be force exited ([#8206](https://github.com/facebook/jest/pull/8206)) - `[@jest/test-result]` Create method to create empty `TestResult` ([#8867](https://github.com/facebook/jest/pull/8867)) - `[jest-worker]` [**BREAKING**] Return a promise from `end()`, resolving with the information whether workers exited gracefully ([#8206](https://github.com/facebook/jest/pull/8206)) 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 ab26793f5589..67c095a6f57f 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 @@ -9,17 +9,17 @@ describe('timers', () => { it('should work before calling resetAllMocks', () => { jest.useFakeTimers(); const f = jest.fn(); - setImmediate(() => f()); - jest.runAllImmediates(); - expect(f.mock.calls.length).toBe(1); + setTimeout(f, 0); + 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 7d7cc8b75a58..d45bb7b54ed5 100644 --- a/e2e/timer-reset-mocks/with-reset-mocks/timerWithMock.test.js +++ b/e2e/timer-reset-mocks/with-reset-mocks/timerWithMock.test.js @@ -9,8 +9,8 @@ describe('timers', () => { it('should work before calling resetAllMocks', () => { const f = jest.fn(); jest.useFakeTimers(); - setImmediate(() => f()); - jest.runAllImmediates(); - expect(f.mock.calls.length).toBe(1); + setTimeout(f, 0); + jest.runAllTimers(); + expect(f).toHaveBeenCalledTimes(1); }); }); diff --git a/examples/timer/__tests__/infinite_timer_game.test.js b/examples/timer/__tests__/infinite_timer_game.test.js index 031e5aa0e049..8205ccc68a9c 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-fake-timers/package.json b/packages/jest-fake-timers/package.json index 52c246bbf7c0..3085e2d14b62 100644 --- a/packages/jest-fake-timers/package.json +++ b/packages/jest-fake-timers/package.json @@ -13,7 +13,11 @@ "@jest/types": "^24.9.0", "jest-message-util": "^24.9.0", "jest-mock": "^24.9.0", - "jest-util": "^24.9.0" + "jest-util": "^24.9.0", + "lolex": "^4.2.0" + }, + "devDependencies": { + "@types/lolex": "^3.1.1" }, "engines": { "node": ">= 8" diff --git a/packages/jest-fake-timers/src/FakeTimersLolex.ts b/packages/jest-fake-timers/src/FakeTimersLolex.ts new file mode 100644 index 000000000000..7964e0ccf8cd --- /dev/null +++ b/packages/jest-fake-timers/src/FakeTimersLolex.ts @@ -0,0 +1,154 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + InstalledClock, + LolexWithContext, + withGlobal as lolexWithGlobal, +} from 'lolex'; +import {StackTraceConfig, formatStackTrace} from 'jest-message-util'; + +export default class FakeTimers { + private _clock!: InstalledClock; + private _config: StackTraceConfig; + private _fakingTime: boolean; + private _global: NodeJS.Global; + private _lolex: LolexWithContext; + private _maxLoops: number; + + constructor({ + global, + config, + maxLoops, + }: { + global: NodeJS.Global; + config: StackTraceConfig; + maxLoops?: number; + }) { + this._global = global; + this._config = config; + this._maxLoops = maxLoops || 100000; + + this._fakingTime = false; + this._lolex = lolexWithGlobal(global); + } + + clearAllTimers() { + if (this._fakingTime) { + this._clock.reset(); + } + } + + dispose() { + this.useRealTimers(); + } + + runAllTimers() { + if (this._checkFakeTimers()) { + this._clock.runAll(); + } + } + + runOnlyPendingTimers() { + if (this._checkFakeTimers()) { + this._clock.runToLast(); + } + } + + advanceTimersToNextTimer(steps = 1) { + if (this._checkFakeTimers()) { + for (let i = steps; i > 0; i--) { + this._clock.next(); + // Fire all timers at this point: https://github.com/sinonjs/lolex/issues/250 + this._clock.tick(0); + + if (this._clock.countTimers() === 0) { + break; + } + } + } + } + + advanceTimersByTime(msToRun: number) { + if (this._checkFakeTimers()) { + this._clock.tick(msToRun); + } + } + + runAllTicks() { + if (this._checkFakeTimers()) { + // @ts-ignore + this._clock.runMicrotasks(); + } + } + + useRealTimers() { + if (this._fakingTime) { + this._clock.uninstall(); + this._fakingTime = false; + } + } + + useFakeTimers() { + const toFake = Object.keys(this._lolex.timers) as Array< + keyof LolexWithContext['timers'] + >; + + if (!this._fakingTime) { + this._clock = this._lolex.install({ + loopLimit: this._maxLoops, + now: Date.now(), + target: this._global, + toFake, + }); + + this._fakingTime = true; + } + } + + reset() { + if (this._checkFakeTimers()) { + const {now} = this._clock; + this._clock.reset(); + this._clock.setSystemTime(now); + } + } + + setSystemTime(now?: number) { + if (this._checkFakeTimers()) { + this._clock.setSystemTime(now); + } + } + + getRealSystemTime() { + return Date.now(); + } + + getTimerCount() { + if (this._checkFakeTimers()) { + return this._clock.countTimers(); + } + + return 0; + } + + private _checkFakeTimers() { + if (!this._fakingTime) { + this._global.console.warn( + 'A function to advance timers was called but the timers API is not ' + + 'mocked with fake timers. Call `jest.useFakeTimers()` in this test or ' + + 'enable fake timers globally by setting `"timers": "fake"` in the ' + + 'configuration file\nStack Trace:\n' + + formatStackTrace(new Error().stack!, this._config, { + noStackTrace: false, + }), + ); + } + + return this._fakingTime; + } +} diff --git a/packages/jest-fake-timers/src/__tests__/__snapshots__/fakeTimersLolex.test.ts.snap b/packages/jest-fake-timers/src/__tests__/__snapshots__/fakeTimersLolex.test.ts.snap new file mode 100644 index 000000000000..39cc8dd4ad64 --- /dev/null +++ b/packages/jest-fake-timers/src/__tests__/__snapshots__/fakeTimersLolex.test.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FakeTimers runAllTimers warns when trying to advance timers while real timers are used 1`] = `"A function to advance timers was called but the timers API is not mocked with fake timers. Call \`jest.useFakeTimers()\` in this test or enable fake timers globally by setting \`\\"timers\\": \\"fake\\"\` in the configuration file"`; diff --git a/packages/jest-fake-timers/src/__tests__/fakeTimersLolex.test.ts b/packages/jest-fake-timers/src/__tests__/fakeTimersLolex.test.ts new file mode 100644 index 000000000000..bba8df289496 --- /dev/null +++ b/packages/jest-fake-timers/src/__tests__/fakeTimersLolex.test.ts @@ -0,0 +1,848 @@ +/** + * 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('advanceTimersToNextTimer', () => { + it('runs timers in order', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + const runOrder: Array = []; + 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); + + timers.advanceTimersToNextTimer(); + // Move forward to t=0 + expect(runOrder).toEqual(['mock2', 'mock3']); + + timers.advanceTimersToNextTimer(); + // Move forward to t=100 + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1']); + + timers.advanceTimersToNextTimer(); + // Move forward to t=200 + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4']); + + timers.advanceTimersToNextTimer(); + // Move forward to t=400 + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4', 'mock4']); + }); + + it('run correct amount of steps', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + const runOrder: Array = []; + 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=100 + timers.advanceTimersToNextTimer(2); + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1']); + + // Move forward to t=600 + timers.advanceTimersToNextTimer(3); + expect(runOrder).toEqual([ + 'mock2', + 'mock3', + 'mock1', + 'mock4', + 'mock4', + 'mock4', + ]); + }); + + it('setTimeout inside setTimeout', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + const runOrder: Array = []; + 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, 0); + global.setTimeout(() => { + mock2(); + global.setTimeout(mock3, 50); + }, 25); + global.setTimeout(mock4, 100); + + // Move forward to t=75 + timers.advanceTimersToNextTimer(3); + expect(runOrder).toEqual(['mock1', 'mock2', 'mock3']); + }); + + it('does nothing when no timers have been scheduled', () => { + const global = {Date, clearTimeout, process, setTimeout}; + const timers = new FakeTimers({global}); + timers.useFakeTimers(); + + timers.advanceTimersToNextTimer(); + }); + }); + + 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); + }); + + it('includes immediates and ticks', () => { + const timers = new FakeTimers({global}); + + timers.useFakeTimers(); + + global.setTimeout(() => {}, 0); + global.setImmediate(() => {}); + process.nextTick(() => {}); + + expect(timers.getTimerCount()).toEqual(3); + }); + + it('not includes cancelled immediates', () => { + const timers = new FakeTimers({global}); + + timers.useFakeTimers(); + + global.setImmediate(() => {}); + expect(timers.getTimerCount()).toEqual(1); + timers.clearAllTimers(); + + expect(timers.getTimerCount()).toEqual(0); + }); + }); +}); diff --git a/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts b/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts index 5bedb0047726..434315c93343 100644 --- a/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts @@ -6,7 +6,7 @@ */ import {runInNewContext} from 'vm'; -import mock from 'jest-mock'; +import mock = require('jest-mock'); import FakeTimers from '../jestFakeTimers'; const timerConfig = { diff --git a/packages/jest-fake-timers/src/index.ts b/packages/jest-fake-timers/src/index.ts index fe0d39d5b62a..295edc2bee29 100644 --- a/packages/jest-fake-timers/src/index.ts +++ b/packages/jest-fake-timers/src/index.ts @@ -6,3 +6,4 @@ */ export {default as JestFakeTimers} from './jestFakeTimers'; +export {default as LolexFakeTimers} from './FakeTimersLolex'; diff --git a/packages/jest-jasmine2/src/__tests__/pTimeout.test.ts b/packages/jest-jasmine2/src/__tests__/pTimeout.test.ts index fef80a2bd541..9907d1f5945f 100644 --- a/packages/jest-jasmine2/src/__tests__/pTimeout.test.ts +++ b/packages/jest-jasmine2/src/__tests__/pTimeout.test.ts @@ -11,6 +11,11 @@ jest.useFakeTimers(); import pTimeout from '../pTimeout'; describe('pTimeout', () => { + beforeEach(() => { + jest.spyOn(global, 'setTimeout'); + jest.spyOn(global, 'clearTimeout'); + }); + it('calls `clearTimeout` and resolves when `promise` resolves.', async () => { const onTimeout = jest.fn(); const promise = Promise.resolve(); diff --git a/yarn.lock b/yarn.lock index 16a4d4cff02e..4f1a04a38960 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1940,6 +1940,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== +"@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" @@ -8749,6 +8754,11 @@ logalot@^2.0.0: figures "^1.3.5" squeak "^1.0.0" +lolex@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7" + integrity sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg== + longest@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"