diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ece9b02be96..59b49fdcf98c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Features - `[jest-environment-jsdom]` [**BREAKING**] Upgrade `jsdom` to v16 ([#9606](https://github.com/facebook/jest/pull/9606)) +- `[@jest/fake-timers]` Add possibility to use a modern implementation of fake timers, backed by `@sinonjs/fake-timers` ([#7776](https://github.com/facebook/jest/pull/7776)) ### Fixes diff --git a/docs/Configuration.md b/docs/Configuration.md index bdfddc8c3bdd..b429dc017d68 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1124,7 +1124,9 @@ This option sets the URL for the jsdom environment. It is reflected in propertie Default: `real` -Setting this value to `fake` allows the use of fake timers for functions such as `setTimeout`. Fake timers are useful when a piece of code sets a long timeout that we don't want to wait for in a test. +Setting this value to `legacy` or `fake` allows the use of fake timers for functions such as `setTimeout`. Fake timers are useful when a piece of code sets a long timeout that we don't want to wait for in a test. + +If the value is `modern`, [`@sinonjs/fake-timers`](https://github.com/sinonjs/fake-timers) will be used as implementation instead of Jest's own legacy implementation. This will be the default fake implementation in Jest 27. ### `transform` [object\] diff --git a/docs/JestObjectAPI.md b/docs/JestObjectAPI.md index 784c5c3370f1..7a6a39a2e28c 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -577,10 +577,12 @@ Restores all mocks back to their original value. Equivalent to calling [`.mockRe ## Mock timers -### `jest.useFakeTimers()` +### `jest.useFakeTimers(implementation?: 'modern' | 'legacy')` Instructs Jest to use fake versions of the standard timer functions (`setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`, `nextTick`, `setImmediate` and `clearImmediate`). +If you pass `'modern'` as argument, [`@sinonjs/fake-timers`](https://github.com/sinonjs/fake-timers) will be used as implementation instead of Jest's own fake timers. This also mocks additional timers like `Date`. `'modern'` will be the default behavior in Jest 27. + Returns the `jest` object for chaining. ### `jest.useRealTimers()` @@ -607,6 +609,8 @@ This is often useful for synchronously executing setTimeouts during a test in or Exhausts all tasks queued by `setImmediate()`. +> Note: This function is not available when using modern fake timers implementation + ### `jest.advanceTimersByTime(msToRun)` ##### renamed in Jest **22.0.0+** @@ -639,6 +643,18 @@ This means, if any timers have been scheduled (but have not yet executed), they Returns the number of fake timers still left to run. +### `.jest.setSystemTime()` + +Set the current system time used by fake timers. Simulates a user changing the system clock while your program is running. It affects the current time but it does not in itself cause e.g. timers to fire; they will fire exactly as they would have done without the call to `jest.setSystemTime()`. + +> Note: This function is only available when using modern fake timers implementation + +### `.jest.getRealSystemTime()` + +When mocking time, `Date.now()` will also be mocked. If you for some reason need access to the real current time, you can invoke this function. + +> Note: This function is only available when using modern fake timers implementation + ## Misc ### `jest.setTimeout(timeout)` diff --git a/e2e/__tests__/modernFakeTimers.test.ts b/e2e/__tests__/modernFakeTimers.test.ts new file mode 100644 index 000000000000..e004b3b19192 --- /dev/null +++ b/e2e/__tests__/modernFakeTimers.test.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import runJest from '../runJest'; + +describe('modern implementation of fake timers', () => { + it('should be possible to use modern implementation from config', () => { + const result = runJest('modern-fake-timers/from-config'); + expect(result.exitCode).toBe(0); + }); + + it('should be possible to use modern implementation from jest-object', () => { + const result = runJest('modern-fake-timers/from-jest-object'); + expect(result.exitCode).toBe(0); + }); +}); diff --git a/e2e/modern-fake-timers/from-config/__tests__/test.js b/e2e/modern-fake-timers/from-config/__tests__/test.js new file mode 100644 index 000000000000..a32e5a5bc8e4 --- /dev/null +++ b/e2e/modern-fake-timers/from-config/__tests__/test.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +test('fake timers', () => { + jest.setSystemTime(0); + + expect(Date.now()).toBe(0); + + jest.setSystemTime(1000); + + expect(Date.now()).toBe(1000); +}); diff --git a/e2e/modern-fake-timers/from-config/package.json b/e2e/modern-fake-timers/from-config/package.json new file mode 100644 index 000000000000..48517c65a548 --- /dev/null +++ b/e2e/modern-fake-timers/from-config/package.json @@ -0,0 +1,6 @@ +{ + "jest": { + "timers": "modern", + "testEnvironment": "node" + } +} diff --git a/e2e/modern-fake-timers/from-jest-object/__tests__/test.js b/e2e/modern-fake-timers/from-jest-object/__tests__/test.js new file mode 100644 index 000000000000..acf8f56889cd --- /dev/null +++ b/e2e/modern-fake-timers/from-jest-object/__tests__/test.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +test('fake timers', () => { + jest.useFakeTimers('modern'); + + jest.setSystemTime(0); + + expect(Date.now()).toBe(0); + + jest.setSystemTime(1000); + + expect(Date.now()).toBe(1000); +}); diff --git a/e2e/modern-fake-timers/from-jest-object/package.json b/e2e/modern-fake-timers/from-jest-object/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/e2e/modern-fake-timers/from-jest-object/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts index f5653ebf5927..ff59c04981ba 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts @@ -49,9 +49,11 @@ const jestAdapter = async ( testPath, }); - if (config.timers === 'fake') { + if (config.timers === 'fake' || config.timers === 'legacy') { // during setup, this cannot be null (and it's fine to explode if it is) environment.fakeTimers!.useFakeTimers(); + } else if (config.timers === 'modern') { + environment.fakeTimersModern!.useFakeTimers(); } globals.beforeEach(() => { diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index df3e0f856bd6..d886d7128535 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -199,6 +199,8 @@ export interface Jest { retryTimes(numRetries: number): Jest; /** * Exhausts tasks queued by setImmediate(). + * + * > Note: This function is not available when using Lolex as fake timers implementation */ runAllImmediates(): void; /** @@ -269,7 +271,7 @@ export interface Jest { /** * Instructs Jest to use fake versions of the standard timer functions. */ - useFakeTimers(): Jest; + useFakeTimers(implementation?: 'modern' | 'legacy'): Jest; /** * Instructs Jest to use the real versions of the standard timer functions. */ @@ -281,4 +283,18 @@ export interface Jest { * every test so that local module state doesn't conflict between tests. */ isolateModules(fn: () => void): Jest; + + /** + * When mocking time, `Date.now()` will also be mocked. If you for some reason need access to the real current time, you can invoke this function. + * + * > Note: This function is only available when using Lolex as fake timers implementation + */ + getRealSystemTime(): number; + + /** + * Set the current system time used by fake timers. Simulates a user changing the system clock while your program is running. It affects the current time but it does not in itself cause e.g. timers to fire; they will fire exactly as they would have done without the call to `jest.setSystemTime()`. + * + * > Note: This function is only available when using Lolex as fake timers implementation + */ + setSystemTime(now?: number): void; } diff --git a/packages/jest-jasmine2/src/index.ts b/packages/jest-jasmine2/src/index.ts index c37fe26f7dbc..8d77212ba5ba 100644 --- a/packages/jest-jasmine2/src/index.ts +++ b/packages/jest-jasmine2/src/index.ts @@ -93,8 +93,10 @@ async function jasmine2( environment.global.describe.skip = environment.global.xdescribe; environment.global.describe.only = environment.global.fdescribe; - if (config.timers === 'fake') { + if (config.timers === 'fake' || config.timers === 'legacy') { environment.fakeTimers!.useFakeTimers(); + } else if (config.timers === 'modern') { + environment.fakeTimersModern!.useFakeTimers(); } env.beforeEach(() => { @@ -109,7 +111,7 @@ async function jasmine2( if (config.resetMocks) { runtime.resetAllMocks(); - if (config.timers === 'fake') { + if (config.timers === 'fake' || config.timers === 'legacy') { environment.fakeTimers!.useFakeTimers(); } } diff --git a/packages/jest-runtime/package.json b/packages/jest-runtime/package.json index 9a934f038170..a242704311c8 100644 --- a/packages/jest-runtime/package.json +++ b/packages/jest-runtime/package.json @@ -13,6 +13,7 @@ "@jest/console": "^26.0.0-alpha.0", "@jest/environment": "^26.0.0-alpha.0", "@jest/globals": "^26.0.0-alpha.0", + "@jest/fake-timers": "^26.0.0-alpha.0", "@jest/source-map": "^26.0.0-alpha.0", "@jest/test-result": "^26.0.0-alpha.0", "@jest/transform": "^26.0.0-alpha.0", diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index da46c8fc5dab..1acaa3647d3e 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -28,6 +28,7 @@ import type { } from '@jest/environment'; import type * as JestGlobals from '@jest/globals'; import type {SourceMapRegistry} from '@jest/source-map'; +import {LegacyFakeTimers, ModernFakeTimers} from '@jest/fake-timers'; import {formatStackTrace, separateMessageFromStack} from 'jest-message-util'; import {createDirectory, deepCyclicCopy} from 'jest-util'; import {escapePathForRegex} from 'jest-regex-util'; @@ -134,6 +135,10 @@ class Runtime { private _currentlyExecutingModulePath: string; private _environment: JestEnvironment; private _explicitShouldMock: BooleanMap; + private _fakeTimersImplementation: + | LegacyFakeTimers + | ModernFakeTimers + | null; private _internalModuleRegistry: ModuleRegistry; private _isCurrentlyExecutingManualMock: string | null; private _mockFactories: Map unknown>; @@ -205,6 +210,11 @@ class Runtime { this._shouldUnmockTransitiveDependenciesCache = new Map(); this._transitiveShouldMock = new Map(); + this._fakeTimersImplementation = + config.timers === 'modern' + ? this._environment.fakeTimersModern + : this._environment.fakeTimers; + this._unmockList = unmockRegExpCache.get(config); if (!this._unmockList && config.unmockedModulePathPatterns) { this._unmockList = new RegExp( @@ -1410,8 +1420,25 @@ class Runtime { this.restoreAllMocks(); return jestObject; }; - const useFakeTimers = () => { - _getFakeTimers().useFakeTimers(); + const _getFakeTimers = () => { + if ( + !(this._environment.fakeTimers || this._environment.fakeTimersModern) + ) { + this._logFormattedReferenceError( + 'You are trying to access a property or method of the Jest environment after it has been torn down.', + ); + process.exitCode = 1; + } + + return this._fakeTimersImplementation!; + }; + const useFakeTimers = (type: string = 'legacy') => { + if (type === 'modern') { + this._fakeTimersImplementation = this._environment.fakeTimersModern; + } else { + this._fakeTimersImplementation = this._environment.fakeTimers; + } + this._fakeTimersImplementation!.useFakeTimers(); return jestObject; }; const useRealTimers = () => { @@ -1445,18 +1472,6 @@ class Runtime { return jestObject; }; - const _getFakeTimers = (): NonNullable => { - if (!this._environment.fakeTimers) { - this._logFormattedReferenceError( - 'You are trying to access a property or method of the Jest environment after it has been torn down.', - ); - process.exitCode = 1; - } - - // We've logged a user message above, so it doesn't matter if we return `null` here - return this._environment.fakeTimers!; - }; - const jestObject: Jest = { addMatchers: (matchers: Record) => this._environment.global.jasmine.addMatchers(matchers), @@ -1476,6 +1491,17 @@ class Runtime { fn, genMockFromModule: (moduleName: string) => this._generateMock(from, moduleName), + getRealSystemTime: () => { + const fakeTimers = _getFakeTimers(); + + if (fakeTimers instanceof ModernFakeTimers) { + return fakeTimers.getRealSystemTime(); + } else { + throw new TypeError( + 'getRealSystemTime is not available when not using modern timers', + ); + } + }, getTimerCount: () => _getFakeTimers().getTimerCount(), isMockFunction: this._moduleMocker.isMockFunction, isolateModules, @@ -1487,7 +1513,17 @@ class Runtime { resetModules, restoreAllMocks, retryTimes, - runAllImmediates: () => _getFakeTimers().runAllImmediates(), + runAllImmediates: () => { + const fakeTimers = _getFakeTimers(); + + if (fakeTimers instanceof LegacyFakeTimers) { + fakeTimers.runAllImmediates(); + } else { + throw new TypeError( + 'runAllImmediates is not available when using modern timers', + ); + } + }, runAllTicks: () => _getFakeTimers().runAllTicks(), runAllTimers: () => _getFakeTimers().runAllTimers(), runOnlyPendingTimers: () => _getFakeTimers().runOnlyPendingTimers(), @@ -1495,6 +1531,17 @@ class Runtime { _getFakeTimers().advanceTimersByTime(msToRun), setMock: (moduleName: string, mock: unknown) => setMockFactory(moduleName, () => mock), + setSystemTime: (now?: number) => { + const fakeTimers = _getFakeTimers(); + + if (fakeTimers instanceof ModernFakeTimers) { + fakeTimers.setSystemTime(now); + } else { + throw new TypeError( + 'setSystemTime is not available when not using modern timers', + ); + } + }, setTimeout, spyOn, unmock, diff --git a/packages/jest-runtime/tsconfig.json b/packages/jest-runtime/tsconfig.json index 2955d779d6b1..fb40160ffc2b 100644 --- a/packages/jest-runtime/tsconfig.json +++ b/packages/jest-runtime/tsconfig.json @@ -9,6 +9,7 @@ {"path": "../jest-console"}, {"path": "../jest-environment"}, {"path": "../jest-environment-node"}, + {"path": "../jest-fake-timers"}, {"path": "../jest-globals"}, {"path": "../jest-haste-map"}, {"path": "../jest-message-util"}, diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index fcdb4f570856..cf895c67c1a8 100644 --- a/packages/jest-types/src/Config.ts +++ b/packages/jest-types/src/Config.ts @@ -344,7 +344,7 @@ export type ProjectConfig = { testRegex: Array; testRunner: string; testURL: string; - timers: 'real' | 'fake'; + timers: 'real' | 'fake' | 'modern' | 'legacy'; transform: Array<[string, Path, Record]>; transformIgnorePatterns: Array; watchPathIgnorePatterns: Array;