Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### Features

- `[jest-config]` Add `defineConfig` and `mergeConfig` helpers for type-safe Jest config ([#15844](https://github.com/jestjs/jest/pull/15844))
- `[jest-fake-timers]` Add `setTimerTickMode` to configure how timers advance

### Fixes

Expand All @@ -12,6 +13,7 @@
### Chore & Maintenance

- `[docs]` Update V30 migration guide to notify users on `jest.mock()` work with case-sensitive path ([#15849](https://github.com/jestjs/jest/pull/15849))
- `[deps]` Update to sinon/fake-timers v15

## 30.2.0

Expand Down
23 changes: 23 additions & 0 deletions docs/JestObjectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,29 @@ This function is not available when using legacy fake timers implementation.

:::

### `jest.setTimerTickMode(mode)`

Allows configuring how fake timers advance time.

Configuration options:

```ts
type TimerTickMode =
| {mode: 'manual'}
| {mode: 'nextAsync'}
| {mode: 'interval'; delta?: number};
```

- `manual`: Timers do not advance without explicit, manual calls to the tick APIs (`jest.advanceTimersByTime(ms)`, `jest.runAllTimers()`, etc).
- `nextAsync`: The clock will continuously break the event loop, then run the next timer until the mode changes.
- `interval`: This is the same as specifying `advanceTimers: true` with an `advanceTimeDelta`. If the delta is not specified, 20 will be used by default.

:::info

This function is not available when using legacy 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

const realSetTimeout = setTimeout;

describe('jest.setTimerTickMode', () => {
it('should not advance the clock with manual', async () => {
jest.useFakeTimers();
jest.setTimerTickMode({mode: 'manual'});
const spy = jest.fn();
setTimeout(spy, 5);
await new Promise(resolve => realSetTimeout(resolve, 20));
expect(spy).not.toHaveBeenCalled();
});

it('should advance the clock to next timer with nextAsync', async () => {
jest.useFakeTimers();
jest.setTimerTickMode({mode: 'nextAsync'});
await new Promise(resolve => setTimeout(resolve, 5000));
await new Promise(resolve => setTimeout(resolve, 5000));
await new Promise(resolve => setTimeout(resolve, 5000));
await new Promise(resolve => setTimeout(resolve, 5000));
// test should not time out
});

it('should advance the clock in real time with delta', async () => {
jest.useFakeTimers();
jest.setTimerTickMode({delta: 10, mode: 'interval'});
const spy = jest.fn();
setTimeout(spy, 10);
await new Promise(resolve => realSetTimeout(resolve, 5));
expect(spy).not.toHaveBeenCalled();
await new Promise(resolve => realSetTimeout(resolve, 5));
expect(spy).toHaveBeenCalled();
});
});
5 changes: 5 additions & 0 deletions e2e/fake-timers/set-timer-tick-mode/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "set-timer-tick-mode-e2e",
"private": true,
"version": "1.0.0"
}
21 changes: 21 additions & 0 deletions packages/jest-environment/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,4 +425,25 @@ export interface Jest {
* performance, time and timer APIs.
*/
useRealTimers(): Jest;
/**
* Updates the mode of advancing timers when using fake timers.
*
* @param config The configuration to use for advancing timers
*
* When the mode is 'interval', timers will be advanced automatically by the [delta]
* milliseconds every [delta] milliseconds of real time. The default delta is 20 milliseconds.
*
* When mode is 'nextAsync', configures whether timers advance automatically to the next timer in the queue after each macrotask.
* This mode differs from 'interval' in that it advances all the way to the next timer, regardless
* of how far in the future that timer is scheduled (e.g. advanceTimersToNextTimerAsync).
*
* When mode is 'manual' (the default), timers will not advance automatically. Instead,
* timers must be advanced using APIs such as `advanceTimersToNextTimer`, `advanceTimersByTime`, etc.
*
* @remarks
* Not available when using legacy fake timers implementation.
*/
setTimerTickMode(
config: {mode: 'manual' | 'nextAsync'} | {mode: 'interval'; delta?: number},
): Jest;
}
4 changes: 2 additions & 2 deletions packages/jest-fake-timers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@
},
"dependencies": {
"@jest/types": "workspace:*",
"@sinonjs/fake-timers": "^13.0.0",
"@sinonjs/fake-timers": "^15.0.0",
"@types/node": "*",
"jest-message-util": "workspace:*",
"jest-mock": "workspace:*",
"jest-util": "workspace:*"
},
"devDependencies": {
"@jest/test-utils": "workspace:*",
"@types/sinonjs__fake-timers": "^8.1.5"
"@types/sinonjs__fake-timers": "15.0.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
Expand Down
66 changes: 48 additions & 18 deletions packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,7 @@ describe('FakeTimers', () => {
Date,
clearInterval,
clearTimeout,
process: {
nextTick: origNextTick,
},
process: {nextTick: origNextTick},
setInterval,
setTimeout,
} as unknown as typeof globalThis;
Expand Down Expand Up @@ -153,9 +151,7 @@ describe('FakeTimers', () => {
Date,
clearInterval,
clearTimeout,
process: {
nextTick: () => {},
},
process: {nextTick: () => {}},
setInterval,
setTimeout,
} as unknown as typeof globalThis;
Expand Down Expand Up @@ -186,9 +182,7 @@ describe('FakeTimers', () => {
Date,
clearInterval,
clearTimeout,
process: {
nextTick,
},
process: {nextTick},
setInterval,
setTimeout,
} as unknown as typeof globalThis;
Expand All @@ -205,9 +199,7 @@ describe('FakeTimers', () => {
Date,
clearInterval,
clearTimeout,
process: {
nextTick: () => {},
},
process: {nextTick: () => {}},
setInterval,
setTimeout,
} as unknown as typeof globalThis;
Expand All @@ -231,9 +223,7 @@ describe('FakeTimers', () => {
Date,
clearInterval,
clearTimeout,
process: {
nextTick: () => {},
},
process: {nextTick: () => {}},
setInterval,
setTimeout,
} as unknown as typeof globalThis;
Expand Down Expand Up @@ -902,9 +892,7 @@ describe('FakeTimers', () => {
Date,
clearInterval,
clearTimeout,
process: {
nextTick: () => {},
},
process: {nextTick: () => {}},
setImmediate: () => {},
setInterval,
setTimeout,
Expand Down Expand Up @@ -1430,6 +1418,48 @@ describe('FakeTimers', () => {
});
});

describe('setTimerTickMode', () => {
let global: typeof globalThis;
let timers: FakeTimers;
const realSetTimeout = setTimeout;
beforeEach(() => {
global = {
Date,
Promise,
clearInterval,
clearTimeout,
process,
setInterval,
setTimeout,
} as unknown as typeof globalThis;
timers = new FakeTimers({config: makeProjectConfig(), global});
timers.useFakeTimers();
});

afterEach(() => {
timers.useRealTimers();
});

it('should advance the clock to next timer with nextAsync', async () => {
timers.setTimerTickMode({mode: 'nextAsync'});
await new Promise(resolve => global.setTimeout(resolve, 5000));
await new Promise(resolve => global.setTimeout(resolve, 5000));
await new Promise(resolve => global.setTimeout(resolve, 5000));
await new Promise(resolve => global.setTimeout(resolve, 5000));
// test should not time out
});

it('should advance the clock in real time with delta', async () => {
timers.setTimerTickMode({delta: 10, mode: 'interval'});
const spy = jest.fn();
global.setTimeout(spy, 10);
await new Promise(resolve => realSetTimeout(resolve, 5));
expect(spy).not.toHaveBeenCalled();
await new Promise(resolve => realSetTimeout(resolve, 5));
expect(spy).toHaveBeenCalled();
});
});

describe('now', () => {
let timers: FakeTimers;
let fakedGlobal: typeof globalThis;
Expand Down
11 changes: 10 additions & 1 deletion packages/jest-fake-timers/src/modernFakeTimers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export default class FakeTimers {

runAllTicks(): void {
if (this._checkFakeTimers()) {
// @ts-expect-error - doesn't exist?
// @ts-expect-error needs an upstream fix: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/73943
this._clock.runMicrotasks();
}
}
Expand Down Expand Up @@ -156,6 +156,15 @@ export default class FakeTimers {
}
}

setTimerTickMode(tickModeConfig: {
mode: 'interval' | 'manual' | 'nextAsync';
delta?: number;
}): void {
if (this._checkFakeTimers()) {
this._clock.setTickMode(tickModeConfig);
}
}

getRealSystemTime(): number {
return Date.now();
}
Expand Down
34 changes: 19 additions & 15 deletions packages/jest-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1161,10 +1161,7 @@ export default class Runtime {
private _getFullTransformationOptions(
options: InternalModuleOptions = defaultTransformOptions,
): TransformationOptions {
return {
...options,
...this._coverageOptions,
};
return {...options, ...this._coverageOptions};
}

requireModuleOrMock<T = unknown>(from: string, moduleName: string): T {
Expand Down Expand Up @@ -1337,10 +1334,7 @@ export default class Runtime {
.map(result => {
const transformedFile = this._v8CoverageSources!.get(result.url);

return {
codeTransformResult: transformedFile,
result,
};
return {codeTransformResult: transformedFile, result};
});
}

Expand Down Expand Up @@ -1444,9 +1438,7 @@ export default class Runtime {

private _resolveCjsModule(from: string, to: string | undefined) {
return to
? this._resolver.resolveModule(from, to, {
conditions: this.cjsConditions,
})
? this._resolver.resolveModule(from, to, {conditions: this.cjsConditions})
: from;
}

Expand Down Expand Up @@ -2120,10 +2112,7 @@ export default class Runtime {
get: (_target, key) =>
typeof key === 'string' ? this._moduleRegistry.get(key) : undefined,
getOwnPropertyDescriptor() {
return {
configurable: true,
enumerable: true,
};
return {configurable: true, enumerable: true};
},
has: (_target, key) =>
typeof key === 'string' && this._moduleRegistry.has(key),
Expand Down Expand Up @@ -2428,6 +2417,21 @@ export default class Runtime {
}
},
setTimeout,
setTimerTickMode: (
mode:
| {mode: 'manual' | 'nextAsync'}
| {mode: 'interval'; delta?: number},
) => {
const fakeTimers = _getFakeTimers();
if (fakeTimers === this._environment.fakeTimersModern) {
fakeTimers.setTimerTickMode(mode);
} else {
throw new TypeError(
'`jest.setTimerTickMode()` is not available when using legacy fake timers.',
);
}
return jestObject;
},
spyOn,
unmock,
unstable_mockModule: mockModule,
Expand Down
5 changes: 5 additions & 0 deletions packages/jest-types/__typetests__/jest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,11 @@ expect(jest.useFakeTimers('modern')).type.toRaiseError();
expect(jest.useRealTimers()).type.toBe<typeof jest>();
expect(jest.useRealTimers(true)).type.toRaiseError();

expect(jest.setTimerTickMode('manual')).type.toRaiseError();
expect(jest.setTimerTickMode({mode: 'interval'})).type.toBe<typeof jest>();
expect(jest.setTimerTickMode({mode: 'manual'})).type.toBe<typeof jest>();
expect(jest.setTimerTickMode({mode: 'nextAsync'})).type.toBe<typeof jest>();

// Misc

expect(jest.retryTimes(3)).type.toBe<typeof jest>();
Expand Down
Loading
Loading