Skip to content

Commit

Permalink
feat: add new 'modern' implementation of Fake Timers (#7776)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB committed May 3, 2020
1 parent d7f3427 commit 71631f6
Show file tree
Hide file tree
Showing 15 changed files with 179 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion docs/Configuration.md
Expand Up @@ -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\<string, pathToTransformer | [pathToTransformer, object]>]

Expand Down
18 changes: 17 additions & 1 deletion docs/JestObjectAPI.md
Expand Up @@ -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()`
Expand All @@ -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+**
Expand Down Expand Up @@ -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)`
Expand Down
20 changes: 20 additions & 0 deletions 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);
});
});
18 changes: 18 additions & 0 deletions 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);
});
6 changes: 6 additions & 0 deletions e2e/modern-fake-timers/from-config/package.json
@@ -0,0 +1,6 @@
{
"jest": {
"timers": "modern",
"testEnvironment": "node"
}
}
20 changes: 20 additions & 0 deletions 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);
});
5 changes: 5 additions & 0 deletions e2e/modern-fake-timers/from-jest-object/package.json
@@ -0,0 +1,5 @@
{
"jest": {
"testEnvironment": "node"
}
}
Expand Up @@ -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(() => {
Expand Down
18 changes: 17 additions & 1 deletion packages/jest-environment/src/index.ts
Expand Up @@ -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;
/**
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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;
}
6 changes: 4 additions & 2 deletions packages/jest-jasmine2/src/index.ts
Expand Up @@ -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(() => {
Expand All @@ -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();
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/jest-runtime/package.json
Expand Up @@ -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",
Expand Down
77 changes: 62 additions & 15 deletions packages/jest-runtime/src/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -134,6 +135,10 @@ class Runtime {
private _currentlyExecutingModulePath: string;
private _environment: JestEnvironment;
private _explicitShouldMock: BooleanMap;
private _fakeTimersImplementation:
| LegacyFakeTimers<unknown>
| ModernFakeTimers
| null;
private _internalModuleRegistry: ModuleRegistry;
private _isCurrentlyExecutingManualMock: string | null;
private _mockFactories: Map<string, () => unknown>;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -1445,18 +1472,6 @@ class Runtime {
return jestObject;
};

const _getFakeTimers = (): NonNullable<JestEnvironment['fakeTimers']> => {
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<string, any>) =>
this._environment.global.jasmine.addMatchers(matchers),
Expand All @@ -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,
Expand All @@ -1487,14 +1513,35 @@ 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(),
runTimersToTime: (msToRun: number) =>
_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,
Expand Down
1 change: 1 addition & 0 deletions packages/jest-runtime/tsconfig.json
Expand Up @@ -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"},
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-types/src/Config.ts
Expand Up @@ -344,7 +344,7 @@ export type ProjectConfig = {
testRegex: Array<string | RegExp>;
testRunner: string;
testURL: string;
timers: 'real' | 'fake';
timers: 'real' | 'fake' | 'modern' | 'legacy';
transform: Array<[string, Path, Record<string, unknown>]>;
transformIgnorePatterns: Array<Glob>;
watchPathIgnorePatterns: Array<string>;
Expand Down

0 comments on commit 71631f6

Please sign in to comment.