Skip to content

Commit 702cc55

Browse files
committed
feat(jest-fake-timers): Add feature to configure how timers advance
This change exposes the `setTimerTickMode` function from the underlying `fake-timers` library. This is to align with the new feature introduced in `fake-timers`. The new `setTimerTickMode` method allows developers to configure whether time should auto advance and what the delta should be after the clock has been created. Prior to this, it could only be done at creation time with `shouldAdvanceTime: true`. This also adds a new mode for automatically advancing time that moves more quickly than the existing shouldAdvanceTime, which uses real time. Testing with mock clocks can often turn into a real struggle when dealing with situations where some work in the test is truly async and other work is captured by the mock clock. When using mock clocks, testers are always forced to write tests with intimate knowledge of when the mock clock needs to be ticked. It is ideal for test code to be written in a way that is independent of whether a mock clock is installed. `shouldAdvanceTime` is essentially `setInterval(() => clock.tick(ms), ms)` while this feature is `const loop = () => setTimeout(() => clock.advancetimerstoNextTimerAsync().then(() => loop()), 0);` There are two key differences: 1. `shouldAdvanceTime` uses `clock.tick(ms)` so it synchronously runs all timers inside the "ms" of the clock queue. This doesn't allow the microtask queue to empty between the macrotask timers in the clock. 2. `shouldAdvanceTime` uses real time to advance the same amount of real time in the mock clock. `setTimerTickMode({mode: "nextAsync"})` advances time as quickly possible and as far as necessary. See: sinonjs/fake-timers@108efae
1 parent 689f340 commit 702cc55

File tree

6 files changed

+118
-33
lines changed

6 files changed

+118
-33
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### Features
44

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

78
### Fixes
89

@@ -12,6 +13,7 @@
1213
### Chore & Maintenance
1314

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

1618
## 30.2.0
1719

docs/JestObjectAPI.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,6 +1126,29 @@ This function is not available when using legacy fake timers implementation.
11261126

11271127
:::
11281128

1129+
### `jest.setTimerTickMode(mode)`
1130+
1131+
Allows configuring how fake timers advance time.
1132+
1133+
Configuration options:
1134+
1135+
```ts
1136+
type TimerTickMode =
1137+
| {mode: 'manual'}
1138+
| {mode: 'nextAsync'}
1139+
| {mode: 'interval'; delta?: number};
1140+
```
1141+
1142+
- `manual`: Timers do not advance without explicit, manual calls to the tick APIs (`jest.advanceTimersByTime(ms)`, `jest.runAllTimers()`, etc).
1143+
- `nextAsync`: The clock will continuously break the event loop, then run the next timer until the mode changes.
1144+
- `interval`: This is the same as specifying `advanceTimers: true` with an `advanceTimeDelta`. If the delta is not specified, 20 will be used by default.
1145+
1146+
:::info
1147+
1148+
This function is not available when using legacy fake timers implementation.
1149+
1150+
:::
1151+
11291152
### `jest.getRealSystemTime()`
11301153

11311154
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.

packages/jest-environment/src/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,4 +425,25 @@ export interface Jest {
425425
* performance, time and timer APIs.
426426
*/
427427
useRealTimers(): Jest;
428+
/**
429+
* Updates the mode of advancing timers when using fake timers.
430+
*
431+
* @param config The configuration to use for advancing timers
432+
*
433+
* When the mode is 'interval', timers will be advanced automatically by the [delta]
434+
* milliseconds every [delta] milliseconds of real time. The default delta is 20 milliseconds.
435+
*
436+
* When mode is 'nextAsync', configures whether timers advance automatically to the next timer in the queue after each macrotask.
437+
* This mode differs from 'interval' in that it advances all the way to the next timer, regardless
438+
* of how far in the future that timer is scheduled (e.g. advanceTimersToNextTimerAsync).
439+
*
440+
* When mode is 'manual' (the default), timers will not advance automatically. Instead,
441+
* timers must be advanced using APIs such as `advanceTimersToNextTimer`, `advanceTimersByTime`, etc.
442+
*
443+
* @remarks
444+
* Not available when using legacy fake timers implementation.
445+
*/
446+
setTimerTickMode(
447+
config: {mode: 'manual' | 'nextAsync'} | {mode: 'interval'; delta?: number},
448+
): Jest;
428449
}

packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,7 @@ describe('FakeTimers', () => {
7373
Date,
7474
clearInterval,
7575
clearTimeout,
76-
process: {
77-
nextTick: origNextTick,
78-
},
76+
process: {nextTick: origNextTick},
7977
setInterval,
8078
setTimeout,
8179
} as unknown as typeof globalThis;
@@ -153,9 +151,7 @@ describe('FakeTimers', () => {
153151
Date,
154152
clearInterval,
155153
clearTimeout,
156-
process: {
157-
nextTick: () => {},
158-
},
154+
process: {nextTick: () => {}},
159155
setInterval,
160156
setTimeout,
161157
} as unknown as typeof globalThis;
@@ -186,9 +182,7 @@ describe('FakeTimers', () => {
186182
Date,
187183
clearInterval,
188184
clearTimeout,
189-
process: {
190-
nextTick,
191-
},
185+
process: {nextTick},
192186
setInterval,
193187
setTimeout,
194188
} as unknown as typeof globalThis;
@@ -205,9 +199,7 @@ describe('FakeTimers', () => {
205199
Date,
206200
clearInterval,
207201
clearTimeout,
208-
process: {
209-
nextTick: () => {},
210-
},
202+
process: {nextTick: () => {}},
211203
setInterval,
212204
setTimeout,
213205
} as unknown as typeof globalThis;
@@ -231,9 +223,7 @@ describe('FakeTimers', () => {
231223
Date,
232224
clearInterval,
233225
clearTimeout,
234-
process: {
235-
nextTick: () => {},
236-
},
226+
process: {nextTick: () => {}},
237227
setInterval,
238228
setTimeout,
239229
} as unknown as typeof globalThis;
@@ -902,9 +892,7 @@ describe('FakeTimers', () => {
902892
Date,
903893
clearInterval,
904894
clearTimeout,
905-
process: {
906-
nextTick: () => {},
907-
},
895+
process: {nextTick: () => {}},
908896
setImmediate: () => {},
909897
setInterval,
910898
setTimeout,
@@ -1430,6 +1418,48 @@ describe('FakeTimers', () => {
14301418
});
14311419
});
14321420

1421+
describe('setTimerTickMode', () => {
1422+
let global: typeof globalThis;
1423+
let timers: FakeTimers;
1424+
const realSetTimeout = setTimeout;
1425+
beforeEach(() => {
1426+
global = {
1427+
Date,
1428+
Promise,
1429+
clearInterval,
1430+
clearTimeout,
1431+
process,
1432+
setInterval,
1433+
setTimeout,
1434+
} as unknown as typeof globalThis;
1435+
timers = new FakeTimers({config: makeProjectConfig(), global});
1436+
timers.useFakeTimers();
1437+
});
1438+
1439+
afterEach(() => {
1440+
timers.useRealTimers();
1441+
});
1442+
1443+
it('should advance the clock to next timer with nextAsync', async () => {
1444+
timers.setTimerTickMode({mode: 'nextAsync'});
1445+
await new Promise(resolve => global.setTimeout(resolve, 5000));
1446+
await new Promise(resolve => global.setTimeout(resolve, 5000));
1447+
await new Promise(resolve => global.setTimeout(resolve, 5000));
1448+
await new Promise(resolve => global.setTimeout(resolve, 5000));
1449+
// test should not time out
1450+
});
1451+
1452+
it('should advance the clock in real time with delta', async () => {
1453+
timers.setTimerTickMode({delta: 10, mode: 'interval'});
1454+
const spy = jest.fn();
1455+
global.setTimeout(spy, 10);
1456+
await new Promise(resolve => realSetTimeout(resolve, 5));
1457+
expect(spy).not.toHaveBeenCalled();
1458+
await new Promise(resolve => realSetTimeout(resolve, 5));
1459+
expect(spy).toHaveBeenCalled();
1460+
});
1461+
});
1462+
14331463
describe('now', () => {
14341464
let timers: FakeTimers;
14351465
let fakedGlobal: typeof globalThis;

packages/jest-runtime/src/index.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,10 +1161,7 @@ export default class Runtime {
11611161
private _getFullTransformationOptions(
11621162
options: InternalModuleOptions = defaultTransformOptions,
11631163
): TransformationOptions {
1164-
return {
1165-
...options,
1166-
...this._coverageOptions,
1167-
};
1164+
return {...options, ...this._coverageOptions};
11681165
}
11691166

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

1340-
return {
1341-
codeTransformResult: transformedFile,
1342-
result,
1343-
};
1337+
return {codeTransformResult: transformedFile, result};
13441338
});
13451339
}
13461340

@@ -1444,9 +1438,7 @@ export default class Runtime {
14441438

14451439
private _resolveCjsModule(from: string, to: string | undefined) {
14461440
return to
1447-
? this._resolver.resolveModule(from, to, {
1448-
conditions: this.cjsConditions,
1449-
})
1441+
? this._resolver.resolveModule(from, to, {conditions: this.cjsConditions})
14501442
: from;
14511443
}
14521444

@@ -2120,10 +2112,7 @@ export default class Runtime {
21202112
get: (_target, key) =>
21212113
typeof key === 'string' ? this._moduleRegistry.get(key) : undefined,
21222114
getOwnPropertyDescriptor() {
2123-
return {
2124-
configurable: true,
2125-
enumerable: true,
2126-
};
2115+
return {configurable: true, enumerable: true};
21272116
},
21282117
has: (_target, key) =>
21292118
typeof key === 'string' && this._moduleRegistry.has(key),
@@ -2428,6 +2417,21 @@ export default class Runtime {
24282417
}
24292418
},
24302419
setTimeout,
2420+
setTimerTickMode: (
2421+
mode:
2422+
| {mode: 'manual' | 'nextAsync'}
2423+
| {mode: 'interval'; delta?: number},
2424+
) => {
2425+
const fakeTimers = _getFakeTimers();
2426+
if (fakeTimers === this._environment.fakeTimersModern) {
2427+
fakeTimers.setTimerTickMode(mode);
2428+
} else {
2429+
throw new TypeError(
2430+
'`jest.setTimerTickMode()` is not available when using legacy fake timers.',
2431+
);
2432+
}
2433+
return jestObject;
2434+
},
24312435
spyOn,
24322436
unmock,
24332437
unstable_mockModule: mockModule,

packages/jest-types/__typetests__/jest.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,11 @@ expect(jest.useFakeTimers('modern')).type.toRaiseError();
651651
expect(jest.useRealTimers()).type.toBe<typeof jest>();
652652
expect(jest.useRealTimers(true)).type.toRaiseError();
653653

654+
expect(jest.setTimerTickMode('manual')).type.toRaiseError();
655+
expect(jest.setTimerTickMode({mode: 'interval'})).type.toBe<typeof jest>();
656+
expect(jest.setTimerTickMode({mode: 'manual'})).type.toBe<typeof jest>();
657+
expect(jest.setTimerTickMode({mode: 'nextAsync'})).type.toBe<typeof jest>();
658+
654659
// Misc
655660

656661
expect(jest.retryTimes(3)).type.toBe<typeof jest>();

0 commit comments

Comments
 (0)