Skip to content

Commit 835635d

Browse files
committed
feat(jest-fake-timers): Add feature to enable automatically advancing timers
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. In addition, when using mock clocks, testers are always forced to write tests with intimate knowledge of when the mock clock needs to be ticked. Oftentimes, the purpose of using a mock clock is to speed up the execution time of the test when there are timeouts involved. It is not often a goal to test the exact timeout values. This can cause tests to be riddled with manual advancements of fake time. It ideal for test code to be written in a way that is independent of whether a mock clock is installed or which mock clock library is used. For example: ``` document.getElementById('submit'); // https://testing-library.com/docs/dom-testing-library/api-async/#waitfor await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1)) ``` When mock clocks are involved, the above may not be possible if there is some delay involved between the click and the request to the API. Instead, developers would need to manually tick the clock beyond the delay to trigger the API call. This commit attempts to resolve these issues by adding a feature which allows jest to advance timers automatically with the passage of time, just as clocks do without mocks installed.
1 parent bd1c6db commit 835635d

File tree

5 files changed

+139
-0
lines changed

5 files changed

+139
-0
lines changed

docs/JestObjectAPI.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,10 +1067,21 @@ This means, if any timers have been scheduled (but have not yet executed), they
10671067

10681068
Returns the number of fake timers still left to run.
10691069

1070+
1071+
### `jest.setAdvanceTimersAutomatically()`
1072+
1073+
Configures whether timers advance automatically. With automatically advancing timers enabled, tests can be written in a way that is independent from whether fake timers are installed. Tests can always be written to wait for timers to resolve, even when using fake timers.
1074+
10701075
### `jest.now()`
10711076

10721077
Returns the time in ms of the current clock. This is equivalent to `Date.now()` if real timers are in use, or if `Date` is mocked. In other cases (such as legacy timers) it may be useful for implementing custom mocks of `Date.now()`, `performance.now()`, etc.
10731078

1079+
:::info
1080+
1081+
This function is not available when using legacy fake timers implementation.
1082+
1083+
:::
1084+
10741085
### `jest.setSystemTime(now?: number | Date)`
10751086

10761087
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()`.

packages/jest-environment/src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ export interface Jest {
9090
* Not available when using legacy fake timers implementation.
9191
*/
9292
advanceTimersToNextTimerAsync(steps?: number): Promise<void>;
93+
/**
94+
* Configures whether timers advance automatically. With automatically advancing
95+
* timers enabled, tests can be written in a way that is independent from whether
96+
* fake timers are installed. Tests can always be written to wait for timers to
97+
* resolve, even when using fake timers.
98+
*
99+
* @remarks
100+
* Not available when using legacy fake timers implementation.
101+
*/
102+
setAdvanceTimersAutomatically(autoAdvance: boolean): void;
93103
/**
94104
* Disables automatic mocking in the module loader.
95105
*/

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1330,6 +1330,74 @@ describe('FakeTimers', () => {
13301330
});
13311331
});
13321332

1333+
describe('setAdvanceTimersAutomatically', () => {
1334+
let global: typeof globalThis;
1335+
let timers: FakeTimers;
1336+
beforeEach(() => {
1337+
global = {
1338+
Date,
1339+
Promise,
1340+
clearTimeout,
1341+
process,
1342+
setTimeout,
1343+
} as unknown as typeof globalThis;
1344+
1345+
timers = new FakeTimers({config: makeProjectConfig(), global});
1346+
1347+
timers.useFakeTimers();
1348+
timers.setAdvanceTimersAutomatically(true);
1349+
});
1350+
1351+
it('can always wait for a timer to execute', async () => {
1352+
const p = new Promise(resolve => {
1353+
global.setTimeout(resolve, 100);
1354+
});
1355+
await expect(p).resolves.toBeUndefined();
1356+
});
1357+
1358+
it('can mix promises inside timers', async () => {
1359+
const p = new Promise(resolve =>
1360+
global.setTimeout(async () => {
1361+
await Promise.resolve();
1362+
global.setTimeout(resolve, 100);
1363+
}, 100),
1364+
);
1365+
await expect(p).resolves.toBeUndefined();
1366+
});
1367+
1368+
it('automatically advances all timers', async () => {
1369+
const p1 = new Promise(resolve => global.setTimeout(resolve, 50));
1370+
const p2 = new Promise(resolve => global.setTimeout(resolve, 50));
1371+
const p3 = new Promise(resolve => global.setTimeout(resolve, 100));
1372+
await expect(Promise.all([p1, p2, p3])).resolves.toEqual([
1373+
undefined,
1374+
undefined,
1375+
undefined,
1376+
]);
1377+
});
1378+
1379+
it('can turn off and on auto advancing of time', async () => {
1380+
let p2Resolved = false;
1381+
const p1 = new Promise(resolve => global.setTimeout(resolve, 50));
1382+
const p2 = new Promise(resolve => global.setTimeout(resolve, 51)).then(
1383+
() => (p2Resolved = true),
1384+
);
1385+
const p3 = new Promise(resolve => global.setTimeout(resolve, 52));
1386+
1387+
await expect(p1).resolves.toBeUndefined();
1388+
1389+
timers.setAdvanceTimersAutomatically(false);
1390+
await new Promise(resolve => setTimeout(resolve, 5));
1391+
expect(p2Resolved).toBe(false);
1392+
1393+
timers.setAdvanceTimersAutomatically(true);
1394+
await new Promise(resolve => setTimeout(resolve, 5));
1395+
await expect(p2).resolves.toBe(true);
1396+
await expect(p3).resolves.toBeUndefined();
1397+
expect(p2Resolved).toBe(true);
1398+
});
1399+
});
1400+
13331401
describe('now', () => {
13341402
let timers: FakeTimers;
13351403
let fakedGlobal: typeof globalThis;

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export default class FakeTimers {
2121
private _fakingTime: boolean;
2222
private readonly _global: typeof globalThis;
2323
private readonly _fakeTimers: FakeTimerWithContext;
24+
private autoTickMode: {counter: number; mode: 'manual' | 'auto'} = {
25+
counter: 0,
26+
mode: 'manual',
27+
};
2428

2529
constructor({
2630
global,
@@ -142,6 +146,22 @@ export default class FakeTimers {
142146
this._fakingTime = true;
143147
}
144148

149+
setAdvanceTimersAutomatically(autoAdvance: boolean): void {
150+
if (!this._checkFakeTimers()) {
151+
return;
152+
}
153+
154+
const newMode = autoAdvance ? 'auto' : 'manual';
155+
if (newMode === this.autoTickMode.mode) {
156+
return;
157+
}
158+
159+
this.autoTickMode = {counter: this.autoTickMode.counter + 1, mode: newMode};
160+
if (autoAdvance) {
161+
this._advanceUntilModeChanges();
162+
}
163+
}
164+
145165
reset(): void {
146166
if (this._checkFakeTimers()) {
147167
const {now} = this._clock;
@@ -224,4 +244,23 @@ export default class FakeTimers {
224244
toFake: [...toFake],
225245
};
226246
}
247+
248+
/**
249+
* Advances the Clock's time until the mode changes.
250+
*
251+
* The time is advanced asynchronously, giving microtasks and events a chance
252+
* to run before each timer runs.
253+
*/
254+
private async _advanceUntilModeChanges() {
255+
if (!this._checkFakeTimers()) {
256+
return;
257+
}
258+
const {counter} = this.autoTickMode;
259+
260+
while (this.autoTickMode.counter === counter && this._fakingTime) {
261+
// nextAsync always resolves in a setTimeout, even when there are no timers.
262+
// https://github.com/sinonjs/fake-timers/blob/710cafad25abe9465c807efd8ed9cf3a15985fb1/src/fake-timers-src.js#L1517-L1546
263+
await this._clock.nextAsync();
264+
}
265+
}
227266
}

packages/jest-runtime/src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2407,6 +2407,17 @@ export default class Runtime {
24072407
);
24082408
}
24092409
},
2410+
setAdvanceTimersAutomatically: (autoAdvance: boolean) => {
2411+
const fakeTimers = _getFakeTimers();
2412+
2413+
if (fakeTimers === this._environment.fakeTimersModern) {
2414+
fakeTimers.setAdvanceTimersAutomatically(autoAdvance);
2415+
} else {
2416+
throw new TypeError(
2417+
'`jest.setAdvanceTimersAutomatically()` is not available when using legacy fake timers.',
2418+
);
2419+
}
2420+
},
24102421
setMock: (moduleName, mock) => setMockFactory(moduleName, () => mock),
24112422
setSystemTime: now => {
24122423
const fakeTimers = _getFakeTimers();

0 commit comments

Comments
 (0)