From f70c255fc6e9086ce998b202633c481b89e8a902 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Tue, 26 Feb 2019 00:07:20 +0100 Subject: [PATCH 1/3] chore: extract FakeTimers into a separate package --- CHANGELOG.md | 1 + packages/jest-environment-jsdom/package.json | 1 + packages/jest-environment-jsdom/src/index.js | 3 +- packages/jest-environment-node/package.json | 1 + packages/jest-environment-node/src/index.js | 3 +- packages/jest-environment/package.json | 1 + packages/jest-environment/src/index.ts | 29 +++++++------------ packages/jest-environment/tsconfig.json | 1 + packages/jest-fake-timers/.npmignore | 3 ++ packages/jest-fake-timers/package.json | 22 ++++++++++++++ .../src/__tests__/index.test.ts} | 6 ++-- .../src/index.ts} | 22 +++++++------- packages/jest-fake-timers/tsconfig.json | 11 +++++++ packages/jest-util/package.json | 3 +- packages/jest-util/src/index.ts | 3 +- packages/jest-util/tsconfig.json | 5 ++-- 16 files changed, 75 insertions(+), 40 deletions(-) create mode 100644 packages/jest-fake-timers/.npmignore create mode 100644 packages/jest-fake-timers/package.json rename packages/{jest-util/src/__tests__/fakeTimers.test.ts => jest-fake-timers/src/__tests__/index.test.ts} (99%) rename packages/{jest-util/src/FakeTimers.ts => jest-fake-timers/src/index.ts} (97%) create mode 100644 packages/jest-fake-timers/tsconfig.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ff03985fa81..08835071522c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ - `[jest-phabricator]`: Migrate to TypeScript ([#7965](https://github.com/facebook/jest/pull/7965)) - `[jest-runner]`: Migrate to TypeScript ([#7968](https://github.com/facebook/jest/pull/7968)) - `[jest-runtime]`: Migrate to TypeScript ([#7964](https://github.com/facebook/jest/pull/7964)) +- `[@jest/fake-timers]`: Extract FakeTimers class from `jest-util` into a new separate package ([#7987](https://github.com/facebook/jest/pull/7987)) ### Performance diff --git a/packages/jest-environment-jsdom/package.json b/packages/jest-environment-jsdom/package.json index 0fb34d1b04f9..65799a5f7909 100644 --- a/packages/jest-environment-jsdom/package.json +++ b/packages/jest-environment-jsdom/package.json @@ -9,6 +9,7 @@ "license": "MIT", "main": "build/index.js", "dependencies": { + "@jest/fake-timers": "^24.1.0", "jest-mock": "^24.0.0", "jest-util": "^24.0.0", "jsdom": "^11.5.1" diff --git a/packages/jest-environment-jsdom/src/index.js b/packages/jest-environment-jsdom/src/index.js index 52eed2ff069c..e5a25208f50a 100644 --- a/packages/jest-environment-jsdom/src/index.js +++ b/packages/jest-environment-jsdom/src/index.js @@ -12,7 +12,8 @@ import type {EnvironmentContext} from 'types/Environment'; import type {Global} from 'types/Global'; import type {ModuleMocker} from 'jest-mock'; -import {FakeTimers, installCommonGlobals} from 'jest-util'; +import FakeTimers from '@jest/fake-timers'; +import {installCommonGlobals} from 'jest-util'; import mock from 'jest-mock'; import {JSDOM, VirtualConsole} from 'jsdom'; diff --git a/packages/jest-environment-node/package.json b/packages/jest-environment-node/package.json index 0914ca74f5e8..374695a24a69 100644 --- a/packages/jest-environment-node/package.json +++ b/packages/jest-environment-node/package.json @@ -9,6 +9,7 @@ "license": "MIT", "main": "build/index.js", "dependencies": { + "@jest/fake-timers": "^24.1.0", "jest-mock": "^24.0.0", "jest-util": "^24.0.0" }, diff --git a/packages/jest-environment-node/src/index.js b/packages/jest-environment-node/src/index.js index f09b3f7c30a1..f70718f9ff29 100644 --- a/packages/jest-environment-node/src/index.js +++ b/packages/jest-environment-node/src/index.js @@ -13,7 +13,8 @@ import type {Global} from 'types/Global'; import type {ModuleMocker} from 'jest-mock'; import vm from 'vm'; -import {FakeTimers, installCommonGlobals} from 'jest-util'; +import FakeTimers from '@jest/fake-timers'; +import {installCommonGlobals} from 'jest-util'; import mock from 'jest-mock'; type Timer = {| diff --git a/packages/jest-environment/package.json b/packages/jest-environment/package.json index d1888425032d..d9595ec1d00b 100644 --- a/packages/jest-environment/package.json +++ b/packages/jest-environment/package.json @@ -10,6 +10,7 @@ "main": "build/index.js", "types": "build/index.d.ts", "dependencies": { + "@jest/fake-timers": "^24.1.0", "@jest/transform": "^24.1.0", "@jest/types": "^24.1.0", "@types/node": "*", diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index 8f9d1e08502e..31a908de1d0a 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -7,8 +7,12 @@ import {Script} from 'vm'; import {Config, Global} from '@jest/types'; -import moduleMocker from 'jest-mock'; +import jestMock, {ModuleMocker} from 'jest-mock'; import {ScriptTransformer} from '@jest/transform'; +import FakeTimers from '@jest/fake-timers'; + +type JestMockFn = typeof jestMock.fn; +type JestMockSpyOn = typeof jestMock.spyOn; export type EnvironmentContext = { console?: Console; @@ -27,21 +31,10 @@ export interface JestEnvironment { script: Script, ): {[ScriptTransformer.EVAL_RESULT_VARIABLE]: ModuleWrapper} | null; global: Global.Global; - // TODO: When `jest-util` is ESM, this can just be `fakeTimers: import('jest-util').FakeTimers` - fakeTimers: { - clearAllTimers(): void; - runAllImmediates(): void; - runAllTicks(): void; - runAllTimers(): void; - advanceTimersByTime(msToRun: number): void; - runOnlyPendingTimers(): void; - runWithRealTimers(callback: () => void): void; - getTimerCount(): number; - useFakeTimers(): void; - useRealTimers(): void; - }; + // TODO: This is nullable, and TS doesn't understand we deal with it in `jest-runtime`. Should be fixed + fakeTimers: FakeTimers; testFilePath: Config.Path; - moduleMocker: typeof moduleMocker; + moduleMocker: ModuleMocker; setup(): Promise; teardown(): Promise; } @@ -112,7 +105,7 @@ export interface Jest { /** * Creates a mock function. Optionally takes a mock implementation. */ - fn: typeof moduleMocker.fn; + fn: JestMockFn; /** * Given the name of a module, use the automatic mocking system to generate a * mocked version of the module for you. @@ -124,7 +117,7 @@ export interface Jest { /** * Determines if the given function is a mocked function. */ - isMockFunction(fn: Function): fn is ReturnType; + isMockFunction(fn: Function): fn is ReturnType; /** * Mocks a module with an auto-mocked version when it is being required. */ @@ -235,7 +228,7 @@ export interface Jest { * Note: By default, jest.spyOn also calls the spied method. This is * different behavior from most other test libraries. */ - spyOn: typeof moduleMocker.spyOn; + spyOn: JestMockSpyOn; /** * Indicates that the module system should never return a mocked version of * the specified module from require() (e.g. that it should always return the diff --git a/packages/jest-environment/tsconfig.json b/packages/jest-environment/tsconfig.json index 8824863bd93b..cfce4b39485b 100644 --- a/packages/jest-environment/tsconfig.json +++ b/packages/jest-environment/tsconfig.json @@ -5,6 +5,7 @@ "outDir": "build" }, "references": [ + {"path": "../jest-fake-timers"}, {"path": "../jest-transform"}, {"path": "../jest-types"}, {"path": "../jest-util"} diff --git a/packages/jest-fake-timers/.npmignore b/packages/jest-fake-timers/.npmignore new file mode 100644 index 000000000000..85e48fe7b0a4 --- /dev/null +++ b/packages/jest-fake-timers/.npmignore @@ -0,0 +1,3 @@ +**/__mocks__/** +**/__tests__/** +src diff --git a/packages/jest-fake-timers/package.json b/packages/jest-fake-timers/package.json new file mode 100644 index 000000000000..988be66bb72f --- /dev/null +++ b/packages/jest-fake-timers/package.json @@ -0,0 +1,22 @@ +{ + "name": "@jest/fake-timers", + "version": "24.1.0", + "repository": { + "type": "git", + "url": "https://github.com/facebook/jest.git", + "directory": "packages/jest-fake-timers" + }, + "license": "MIT", + "main": "build/index.js", + "types": "build/index.d.ts", + "dependencies": { + "@jest/types": "^24.1.0", + "@types/node": "*", + "jest-message-util": "^24.0.0", + "jest-mock": "^24.0.0" + }, + "engines": { + "node": ">= 6" + }, + "gitHead": "b16789230fd45056a7f2fa199bae06c7a1780deb" +} diff --git a/packages/jest-util/src/__tests__/fakeTimers.test.ts b/packages/jest-fake-timers/src/__tests__/index.test.ts similarity index 99% rename from packages/jest-util/src/__tests__/fakeTimers.test.ts rename to packages/jest-fake-timers/src/__tests__/index.test.ts index 7d53b0c91e51..1e591f729264 100644 --- a/packages/jest-util/src/__tests__/fakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/index.test.ts @@ -7,9 +7,7 @@ import vm from 'vm'; import mock from 'jest-mock'; -import FakeTimers from '../FakeTimers'; -// TODO: import this type directly from jest-mock once TS migration is done -type ModuleMocker = typeof mock; +import FakeTimers from '../'; const timerConfig = { idToRef: (id: number) => id, @@ -22,7 +20,7 @@ const config = { }; describe('FakeTimers', () => { - let moduleMocker: ModuleMocker; + let moduleMocker: mock.ModuleMocker; beforeEach(() => { const global = vm.runInNewContext('this'); diff --git a/packages/jest-util/src/FakeTimers.ts b/packages/jest-fake-timers/src/index.ts similarity index 97% rename from packages/jest-util/src/FakeTimers.ts rename to packages/jest-fake-timers/src/index.ts index aea4916981b5..9dca4a0ac1d2 100644 --- a/packages/jest-util/src/FakeTimers.ts +++ b/packages/jest-fake-timers/src/index.ts @@ -5,18 +5,10 @@ * LICENSE file in the root directory of this source tree. */ -import mock from 'jest-mock'; +import {ModuleMocker} from 'jest-mock'; import {formatStackTrace, StackTraceConfig} from 'jest-message-util'; -import setGlobal from './setGlobal'; -type ModuleMocker = typeof mock; - -/** - * We don't know the type of arguments for a callback ahead of time which is why - * we are disabling the flowtype/no-weak-types rule here. - */ - -type Callback = (...args: any) => void; +type Callback = (...args: Array) => void; type TimerID = string; @@ -50,6 +42,16 @@ type TimerConfig = { 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}; diff --git a/packages/jest-fake-timers/tsconfig.json b/packages/jest-fake-timers/tsconfig.json new file mode 100644 index 000000000000..335b1e9c373a --- /dev/null +++ b/packages/jest-fake-timers/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build" + }, + "references": [ + {"path": "../jest-message-util"}, + {"path": "../jest-mock"} + ] +} diff --git a/packages/jest-util/package.json b/packages/jest-util/package.json index 13e9e9f6c706..4fceefa7a5da 100644 --- a/packages/jest-util/package.json +++ b/packages/jest-util/package.json @@ -10,14 +10,13 @@ "main": "build/index.js", "types": "build/index.d.ts", "dependencies": { + "@jest/fake-timers": "^24.1.0", "@jest/types": "^24.1.0", "@types/node": "*", "callsites": "^3.0.0", "chalk": "^2.0.1", "graceful-fs": "^4.1.15", "is-ci": "^2.0.0", - "jest-message-util": "^24.0.0", - "jest-mock": "^24.0.0", "mkdirp": "^0.5.1", "readable-stream": "^3.1.1", "slash": "^2.0.0", diff --git a/packages/jest-util/src/index.ts b/packages/jest-util/src/index.ts index 66c38c2b63e0..1db987e58574 100644 --- a/packages/jest-util/src/index.ts +++ b/packages/jest-util/src/index.ts @@ -5,12 +5,13 @@ * LICENSE file in the root directory of this source tree. */ +// TODO: Remove this export in the next major +import FakeTimers from '@jest/fake-timers'; import BufferedConsole from './BufferedConsole'; import clearLine from './clearLine'; import CustomConsole from './CustomConsole'; import createDirectory from './createDirectory'; import ErrorWithStack from './ErrorWithStack'; -import FakeTimers from './FakeTimers'; import formatTestResults from './formatTestResults'; import getFailedSnapshotTests from './getFailedSnapshotTests'; import getConsoleOutput from './getConsoleOutput'; diff --git a/packages/jest-util/tsconfig.json b/packages/jest-util/tsconfig.json index c8a0f5ac5aa8..876baa6cf5c8 100644 --- a/packages/jest-util/tsconfig.json +++ b/packages/jest-util/tsconfig.json @@ -5,8 +5,7 @@ "outDir": "build" }, "references": [ - {"path": "../jest-types"}, - {"path": "../jest-message-util"}, - {"path": "../jest-mock"} + {"path": "../jest-fake-timers"}, + {"path": "../jest-types"} ] } From e47bd89fbfcadc5ba2915946327062b0d73623a2 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Tue, 26 Feb 2019 00:16:04 +0100 Subject: [PATCH 2/3] make fake timer export named --- packages/jest-environment-jsdom/src/index.js | 2 +- packages/jest-environment-node/src/index.js | 2 +- packages/jest-environment/src/index.ts | 2 +- .../{index.test.ts => jestFakeTimers.test.ts} | 2 +- packages/jest-fake-timers/src/index.ts | 514 +---------------- .../jest-fake-timers/src/jestFakeTimers.ts | 520 ++++++++++++++++++ packages/jest-util/src/index.ts | 2 +- 7 files changed, 526 insertions(+), 518 deletions(-) rename packages/jest-fake-timers/src/__tests__/{index.test.ts => jestFakeTimers.test.ts} (99%) create mode 100644 packages/jest-fake-timers/src/jestFakeTimers.ts diff --git a/packages/jest-environment-jsdom/src/index.js b/packages/jest-environment-jsdom/src/index.js index e5a25208f50a..56f417ba8188 100644 --- a/packages/jest-environment-jsdom/src/index.js +++ b/packages/jest-environment-jsdom/src/index.js @@ -12,7 +12,7 @@ import type {EnvironmentContext} from 'types/Environment'; import type {Global} from 'types/Global'; import type {ModuleMocker} from 'jest-mock'; -import FakeTimers from '@jest/fake-timers'; +import {JestFakeTimers as FakeTimers} from '@jest/fake-timers'; import {installCommonGlobals} from 'jest-util'; import mock from 'jest-mock'; import {JSDOM, VirtualConsole} from 'jsdom'; diff --git a/packages/jest-environment-node/src/index.js b/packages/jest-environment-node/src/index.js index f70718f9ff29..1ba6d72de0af 100644 --- a/packages/jest-environment-node/src/index.js +++ b/packages/jest-environment-node/src/index.js @@ -13,7 +13,7 @@ import type {Global} from 'types/Global'; import type {ModuleMocker} from 'jest-mock'; import vm from 'vm'; -import FakeTimers from '@jest/fake-timers'; +import {JestFakeTimers as FakeTimers} from '@jest/fake-timers'; import {installCommonGlobals} from 'jest-util'; import mock from 'jest-mock'; diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index 31a908de1d0a..d8a6aed05f4e 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -9,7 +9,7 @@ import {Script} from 'vm'; import {Config, Global} from '@jest/types'; import jestMock, {ModuleMocker} from 'jest-mock'; import {ScriptTransformer} from '@jest/transform'; -import FakeTimers from '@jest/fake-timers'; +import {JestFakeTimers as FakeTimers} from '@jest/fake-timers'; type JestMockFn = typeof jestMock.fn; type JestMockSpyOn = typeof jestMock.spyOn; diff --git a/packages/jest-fake-timers/src/__tests__/index.test.ts b/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts similarity index 99% rename from packages/jest-fake-timers/src/__tests__/index.test.ts rename to packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts index 1e591f729264..ac0d7ad149d1 100644 --- a/packages/jest-fake-timers/src/__tests__/index.test.ts +++ b/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts @@ -7,7 +7,7 @@ import vm from 'vm'; import mock from 'jest-mock'; -import FakeTimers from '../'; +import FakeTimers from '../jestFakeTimers'; const timerConfig = { idToRef: (id: number) => id, diff --git a/packages/jest-fake-timers/src/index.ts b/packages/jest-fake-timers/src/index.ts index 9dca4a0ac1d2..fe0d39d5b62a 100644 --- a/packages/jest-fake-timers/src/index.ts +++ b/packages/jest-fake-timers/src/index.ts @@ -5,516 +5,4 @@ * LICENSE file in the root directory of this source tree. */ -import {ModuleMocker} from 'jest-mock'; -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}; - private _config: StackTraceConfig; - private _disposed?: boolean; - private _fakeTimerAPIs!: TimerAPI; - private _global: NodeJS.Global; - private _immediates!: Array; - private _maxLoops: number; - private _moduleMocker: ModuleMocker; - private _now!: number; - private _ticks!: Array; - private _timerAPIs: TimerAPI; - private _timers!: {[key: string]: Timer}; - 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(); - } - - clearAllTimers() { - this._immediates.forEach(immediate => - this._fakeClearImmediate(immediate.uuid), - ); - for (const uuid in this._timers) { - delete this._timers[uuid]; - } - } - - dispose() { - this._disposed = true; - this.clearAllTimers(); - } - - reset() { - this._cancelledTicks = {}; - this._cancelledImmediates = {}; - this._now = 0; - this._ticks = []; - this._immediates = []; - this._timers = {}; - } - - 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...", - ); - } - } - - 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(); - } - } - - 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...", - ); - } - } - - runOnlyPendingTimers() { - const timers = {...this._timers}; - this._checkFakeTimers(); - this._immediates.forEach(this._runImmediate, this); - Object.keys(timers) - .sort((left, right) => timers[left].expiry - timers[right].expiry) - .forEach(this._runTimerHandle, this); - } - - advanceTimersByTime(msToRun: number) { - this._checkFakeTimers(); - // Only run a generous number of timers and then bail. - // This is just to help avoid recursive loops - let i; - for (i = 0; i < this._maxLoops; i++) { - const timerHandle = this._getNextTimerHandle(); - - // If there are no more timer handles, stop! - if (timerHandle === null) { - break; - } - - const nextTimerExpiry = this._timers[timerHandle].expiry; - if (this._now + msToRun < nextTimerExpiry) { - // There are no timers between now and the target we're running to, so - // adjust our time cursor and quit - this._now += msToRun; - break; - } else { - msToRun -= nextTimerExpiry - this._now; - this._now = nextTimerExpiry; - this._runTimerHandle(timerHandle); - } - } - - if (i === this._maxLoops) { - throw new Error( - 'Ran ' + - this._maxLoops + - ' timers, and there are still more! ' + - "Assuming we've hit an infinite recursion and bailing out...", - ); - } - } - - 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; - } - } - - 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; - } - - 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(); - - return Object.keys(this._timers).length; - } - - 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, - }), - ); - } - } - - 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.hasOwnProperty(uuid)) { - delete this._timers[String(uuid)]; - } - } - - private _fakeClearImmediate(uuid: TimerID) { - this._cancelledImmediates[uuid] = true; - } - - private _fakeNextTick(callback: Callback, ...args: Array) { - if (this._disposed) { - return; - } - - 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; - } - - private _fakeSetInterval( - callback: Callback, - intervalDelay?: number, - ...args: Array - ) { - if (this._disposed) { - return null; - } - - if (intervalDelay == null) { - intervalDelay = 0; - } - - const uuid = this._uuidCounter++; - - this._timers[String(uuid)] = { - callback: () => callback.apply(null, args), - expiry: this._now + intervalDelay, - interval: intervalDelay, - type: 'interval', - }; - - return this._timerConfig.idToRef(uuid); - } - - private _fakeSetTimeout( - callback: Callback, - delay?: number, - ...args: Array - ) { - if (this._disposed) { - return null; - } - - // eslint-disable-next-line no-bitwise - delay = Number(delay) | 0; - - const uuid = this._uuidCounter++; - - this._timers[String(uuid)] = { - callback: () => callback.apply(null, args), - expiry: this._now + delay, - interval: undefined, - type: 'timeout', - }; - - return this._timerConfig.idToRef(uuid); - } - - private _getNextTimerHandle() { - let nextTimerHandle = null; - let uuid; - let soonestTime = MS_IN_A_YEAR; - let timer; - for (uuid in this._timers) { - timer = this._timers[uuid]; - if (timer.expiry < soonestTime) { - soonestTime = timer.expiry; - nextTimerHandle = uuid; - } - } - - return nextTimerHandle; - } - - private _runTimerHandle(timerHandle: TimerID) { - const timer = this._timers[timerHandle]; - - if (!timer) { - return; - } - - switch (timer.type) { - case 'timeout': - const callback = timer.callback; - delete this._timers[timerHandle]; - callback(); - break; - - case 'interval': - timer.expiry = this._now + (timer.interval || 0); - timer.callback(); - break; - - default: - throw new Error('Unexpected timer type: ' + timer.type); - } - } -} +export {default as JestFakeTimers} from './jestFakeTimers'; diff --git a/packages/jest-fake-timers/src/jestFakeTimers.ts b/packages/jest-fake-timers/src/jestFakeTimers.ts new file mode 100644 index 000000000000..9dca4a0ac1d2 --- /dev/null +++ b/packages/jest-fake-timers/src/jestFakeTimers.ts @@ -0,0 +1,520 @@ +/** + * 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 {ModuleMocker} from 'jest-mock'; +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}; + private _config: StackTraceConfig; + private _disposed?: boolean; + private _fakeTimerAPIs!: TimerAPI; + private _global: NodeJS.Global; + private _immediates!: Array; + private _maxLoops: number; + private _moduleMocker: ModuleMocker; + private _now!: number; + private _ticks!: Array; + private _timerAPIs: TimerAPI; + private _timers!: {[key: string]: Timer}; + 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(); + } + + clearAllTimers() { + this._immediates.forEach(immediate => + this._fakeClearImmediate(immediate.uuid), + ); + for (const uuid in this._timers) { + delete this._timers[uuid]; + } + } + + dispose() { + this._disposed = true; + this.clearAllTimers(); + } + + reset() { + this._cancelledTicks = {}; + this._cancelledImmediates = {}; + this._now = 0; + this._ticks = []; + this._immediates = []; + this._timers = {}; + } + + 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...", + ); + } + } + + 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(); + } + } + + 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...", + ); + } + } + + runOnlyPendingTimers() { + const timers = {...this._timers}; + this._checkFakeTimers(); + this._immediates.forEach(this._runImmediate, this); + Object.keys(timers) + .sort((left, right) => timers[left].expiry - timers[right].expiry) + .forEach(this._runTimerHandle, this); + } + + advanceTimersByTime(msToRun: number) { + this._checkFakeTimers(); + // Only run a generous number of timers and then bail. + // This is just to help avoid recursive loops + let i; + for (i = 0; i < this._maxLoops; i++) { + const timerHandle = this._getNextTimerHandle(); + + // If there are no more timer handles, stop! + if (timerHandle === null) { + break; + } + + const nextTimerExpiry = this._timers[timerHandle].expiry; + if (this._now + msToRun < nextTimerExpiry) { + // There are no timers between now and the target we're running to, so + // adjust our time cursor and quit + this._now += msToRun; + break; + } else { + msToRun -= nextTimerExpiry - this._now; + this._now = nextTimerExpiry; + this._runTimerHandle(timerHandle); + } + } + + if (i === this._maxLoops) { + throw new Error( + 'Ran ' + + this._maxLoops + + ' timers, and there are still more! ' + + "Assuming we've hit an infinite recursion and bailing out...", + ); + } + } + + 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; + } + } + + 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; + } + + 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(); + + return Object.keys(this._timers).length; + } + + 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, + }), + ); + } + } + + 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.hasOwnProperty(uuid)) { + delete this._timers[String(uuid)]; + } + } + + private _fakeClearImmediate(uuid: TimerID) { + this._cancelledImmediates[uuid] = true; + } + + private _fakeNextTick(callback: Callback, ...args: Array) { + if (this._disposed) { + return; + } + + 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; + } + + private _fakeSetInterval( + callback: Callback, + intervalDelay?: number, + ...args: Array + ) { + if (this._disposed) { + return null; + } + + if (intervalDelay == null) { + intervalDelay = 0; + } + + const uuid = this._uuidCounter++; + + this._timers[String(uuid)] = { + callback: () => callback.apply(null, args), + expiry: this._now + intervalDelay, + interval: intervalDelay, + type: 'interval', + }; + + return this._timerConfig.idToRef(uuid); + } + + private _fakeSetTimeout( + callback: Callback, + delay?: number, + ...args: Array + ) { + if (this._disposed) { + return null; + } + + // eslint-disable-next-line no-bitwise + delay = Number(delay) | 0; + + const uuid = this._uuidCounter++; + + this._timers[String(uuid)] = { + callback: () => callback.apply(null, args), + expiry: this._now + delay, + interval: undefined, + type: 'timeout', + }; + + return this._timerConfig.idToRef(uuid); + } + + private _getNextTimerHandle() { + let nextTimerHandle = null; + let uuid; + let soonestTime = MS_IN_A_YEAR; + let timer; + for (uuid in this._timers) { + timer = this._timers[uuid]; + if (timer.expiry < soonestTime) { + soonestTime = timer.expiry; + nextTimerHandle = uuid; + } + } + + return nextTimerHandle; + } + + private _runTimerHandle(timerHandle: TimerID) { + const timer = this._timers[timerHandle]; + + if (!timer) { + return; + } + + switch (timer.type) { + case 'timeout': + const callback = timer.callback; + delete this._timers[timerHandle]; + callback(); + break; + + case 'interval': + timer.expiry = this._now + (timer.interval || 0); + timer.callback(); + break; + + default: + throw new Error('Unexpected timer type: ' + timer.type); + } + } +} diff --git a/packages/jest-util/src/index.ts b/packages/jest-util/src/index.ts index 1db987e58574..a7291e0ae7e1 100644 --- a/packages/jest-util/src/index.ts +++ b/packages/jest-util/src/index.ts @@ -6,7 +6,7 @@ */ // TODO: Remove this export in the next major -import FakeTimers from '@jest/fake-timers'; +import {JestFakeTimers as FakeTimers} from '@jest/fake-timers'; import BufferedConsole from './BufferedConsole'; import clearLine from './clearLine'; import CustomConsole from './CustomConsole'; From 1d2d505daa4d88bdd037d81ab2861ccd9ecfd8cd Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Tue, 26 Feb 2019 00:42:45 +0100 Subject: [PATCH 3/3] update snapshots --- .../src/__tests__/__snapshots__/jestFakeTimers.test.ts.snap} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/{jest-util/src/__tests__/__snapshots__/fakeTimers.test.ts.snap => jest-fake-timers/src/__tests__/__snapshots__/jestFakeTimers.test.ts.snap} (100%) diff --git a/packages/jest-util/src/__tests__/__snapshots__/fakeTimers.test.ts.snap b/packages/jest-fake-timers/src/__tests__/__snapshots__/jestFakeTimers.test.ts.snap similarity index 100% rename from packages/jest-util/src/__tests__/__snapshots__/fakeTimers.test.ts.snap rename to packages/jest-fake-timers/src/__tests__/__snapshots__/jestFakeTimers.test.ts.snap