Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new 'modern' implementation of Fake Timers #7776

Merged
merged 2 commits into from May 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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