Skip to content

Commit 8320a7f

Browse files
authored
Merge branch 'main' into fix/resolve-correct-test-deps-in-pnpm-project
2 parents 7cc5d31 + 6813dfa commit 8320a7f

File tree

11 files changed

+187
-46
lines changed

11 files changed

+187
-46
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

@@ -13,6 +14,7 @@
1314
### Chore & Maintenance
1415

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

1719
## 30.2.0
1820

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.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
'use strict';
9+
10+
const realSetTimeout = setTimeout;
11+
12+
describe('jest.setTimerTickMode', () => {
13+
it('should not advance the clock with manual', async () => {
14+
jest.useFakeTimers();
15+
jest.setTimerTickMode({mode: 'manual'});
16+
const spy = jest.fn();
17+
setTimeout(spy, 5);
18+
await new Promise(resolve => realSetTimeout(resolve, 20));
19+
expect(spy).not.toHaveBeenCalled();
20+
});
21+
22+
it('should advance the clock to next timer with nextAsync', async () => {
23+
jest.useFakeTimers();
24+
jest.setTimerTickMode({mode: 'nextAsync'});
25+
await new Promise(resolve => setTimeout(resolve, 5000));
26+
await new Promise(resolve => setTimeout(resolve, 5000));
27+
await new Promise(resolve => setTimeout(resolve, 5000));
28+
await new Promise(resolve => setTimeout(resolve, 5000));
29+
// test should not time out
30+
});
31+
32+
it('should advance the clock in real time with delta', async () => {
33+
jest.useFakeTimers();
34+
jest.setTimerTickMode({delta: 10, mode: 'interval'});
35+
const spy = jest.fn();
36+
setTimeout(spy, 10);
37+
await new Promise(resolve => realSetTimeout(resolve, 5));
38+
expect(spy).not.toHaveBeenCalled();
39+
await new Promise(resolve => realSetTimeout(resolve, 5));
40+
expect(spy).toHaveBeenCalled();
41+
});
42+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "set-timer-tick-mode-e2e",
3+
"private": true,
4+
"version": "1.0.0"
5+
}

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/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@
2020
},
2121
"dependencies": {
2222
"@jest/types": "workspace:*",
23-
"@sinonjs/fake-timers": "^13.0.0",
23+
"@sinonjs/fake-timers": "^15.0.0",
2424
"@types/node": "*",
2525
"jest-message-util": "workspace:*",
2626
"jest-mock": "workspace:*",
2727
"jest-util": "workspace:*"
2828
},
2929
"devDependencies": {
3030
"@jest/test-utils": "workspace:*",
31-
"@types/sinonjs__fake-timers": "^8.1.5"
31+
"@types/sinonjs__fake-timers": "15.0.0"
3232
},
3333
"engines": {
3434
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"

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-fake-timers/src/modernFakeTimers.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export default class FakeTimers {
118118

119119
runAllTicks(): void {
120120
if (this._checkFakeTimers()) {
121-
// @ts-expect-error - doesn't exist?
121+
// @ts-expect-error needs an upstream fix: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/73943
122122
this._clock.runMicrotasks();
123123
}
124124
}
@@ -156,6 +156,15 @@ export default class FakeTimers {
156156
}
157157
}
158158

159+
setTimerTickMode(tickModeConfig: {
160+
mode: 'interval' | 'manual' | 'nextAsync';
161+
delta?: number;
162+
}): void {
163+
if (this._checkFakeTimers()) {
164+
this._clock.setTickMode(tickModeConfig);
165+
}
166+
}
167+
159168
getRealSystemTime(): number {
160169
return Date.now();
161170
}

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)