diff --git a/docs/JestObjectAPI.md b/docs/JestObjectAPI.md index 8b0e6ece428f..810cf9772522 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -1067,10 +1067,25 @@ 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.setAdvanceTimersAutomatically()` + +Configures whether timers advance automatically. When enabled, jest will advance the clock to the next timer in the queue after a macrotask. 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. + +This feature differs from the `advanceTimers` in two key ways: + +1. The microtask queue is allowed to empty between each timer execution, as would be the case without fake timers installed. +1. It advances as quickly and as far as necessary. If the next timer in the queue is at 1000ms, it will advance 1000ms immediately whereas `advanceTimers`, without manually advancing time in the test, would take `1000 / advanceTimersMs` real time to reach and execute the timer. + ### `jest.now()` 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. +:::info + +This function is not available when using legacy fake timers implementation. + +::: + ### `jest.setSystemTime(now?: number | Date)` 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()`. diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index 9f1eccc3c378..d9bb2b844a3c 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -90,6 +90,16 @@ export interface Jest { * Not available when using legacy fake timers implementation. */ advanceTimersToNextTimerAsync(steps?: number): Promise; + /** + * 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. + * + * @remarks + * Not available when using legacy fake timers implementation. + */ + setAdvanceTimersAutomatically(autoAdvance: boolean): void; /** * Disables automatic mocking in the module loader. */ diff --git a/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts b/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts index 71a542c607da..12b677042c14 100644 --- a/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts @@ -1330,6 +1330,74 @@ describe('FakeTimers', () => { }); }); + describe('setAdvanceTimersAutomatically', () => { + let global: typeof globalThis; + let timers: FakeTimers; + beforeEach(() => { + global = { + Date, + Promise, + clearTimeout, + process, + setTimeout, + } as unknown as typeof globalThis; + + timers = new FakeTimers({config: makeProjectConfig(), global}); + + timers.useFakeTimers(); + timers.setAdvanceTimersAutomatically(true); + }); + + it('can always wait for a timer to execute', async () => { + const p = new Promise(resolve => { + global.setTimeout(resolve, 100); + }); + await expect(p).resolves.toBeUndefined(); + }); + + it('can mix promises inside timers', async () => { + const p = new Promise(resolve => + global.setTimeout(async () => { + await Promise.resolve(); + global.setTimeout(resolve, 100); + }, 100), + ); + await expect(p).resolves.toBeUndefined(); + }); + + it('automatically advances all timers', async () => { + const p1 = new Promise(resolve => global.setTimeout(resolve, 50)); + const p2 = new Promise(resolve => global.setTimeout(resolve, 50)); + const p3 = new Promise(resolve => global.setTimeout(resolve, 100)); + await expect(Promise.all([p1, p2, p3])).resolves.toEqual([ + undefined, + undefined, + undefined, + ]); + }); + + it('can turn off and on auto advancing of time', async () => { + let p2Resolved = false; + const p1 = new Promise(resolve => global.setTimeout(resolve, 50)); + const p2 = new Promise(resolve => global.setTimeout(resolve, 51)).then( + () => (p2Resolved = true), + ); + const p3 = new Promise(resolve => global.setTimeout(resolve, 52)); + + await expect(p1).resolves.toBeUndefined(); + + timers.setAdvanceTimersAutomatically(false); + await new Promise(resolve => setTimeout(resolve, 5)); + expect(p2Resolved).toBe(false); + + timers.setAdvanceTimersAutomatically(true); + await new Promise(resolve => setTimeout(resolve, 5)); + await expect(p2).resolves.toBe(true); + await expect(p3).resolves.toBeUndefined(); + expect(p2Resolved).toBe(true); + }); + }); + describe('now', () => { let timers: FakeTimers; let fakedGlobal: typeof globalThis; diff --git a/packages/jest-fake-timers/src/modernFakeTimers.ts b/packages/jest-fake-timers/src/modernFakeTimers.ts index 310cf4be6b85..2454ee90a7bc 100644 --- a/packages/jest-fake-timers/src/modernFakeTimers.ts +++ b/packages/jest-fake-timers/src/modernFakeTimers.ts @@ -21,6 +21,10 @@ export default class FakeTimers { private _fakingTime: boolean; private readonly _global: typeof globalThis; private readonly _fakeTimers: FakeTimerWithContext; + private autoTickMode: {counter: number; mode: 'manual' | 'auto'} = { + counter: 0, + mode: 'manual', + }; constructor({ global, @@ -142,6 +146,22 @@ export default class FakeTimers { this._fakingTime = true; } + setAdvanceTimersAutomatically(autoAdvance: boolean): void { + if (!this._checkFakeTimers()) { + return; + } + + const newMode = autoAdvance ? 'auto' : 'manual'; + if (newMode === this.autoTickMode.mode) { + return; + } + + this.autoTickMode = {counter: this.autoTickMode.counter + 1, mode: newMode}; + if (autoAdvance) { + this._advanceUntilModeChanges(); + } + } + reset(): void { if (this._checkFakeTimers()) { const {now} = this._clock; @@ -224,4 +244,23 @@ export default class FakeTimers { toFake: [...toFake], }; } + + /** + * Advances the Clock's time until the mode changes. + * + * The time is advanced asynchronously, giving microtasks and events a chance + * to run before each timer runs. + */ + private async _advanceUntilModeChanges() { + if (!this._checkFakeTimers()) { + return; + } + const {counter} = this.autoTickMode; + + while (this.autoTickMode.counter === counter && this._fakingTime) { + // nextAsync always resolves in a setTimeout, even when there are no timers. + // https://github.com/sinonjs/fake-timers/blob/710cafad25abe9465c807efd8ed9cf3a15985fb1/src/fake-timers-src.js#L1517-L1546 + await this._clock.nextAsync(); + } + } } diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 477be4b7ef22..cc65ca0250b2 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -2407,6 +2407,17 @@ export default class Runtime { ); } }, + setAdvanceTimersAutomatically: (autoAdvance: boolean) => { + const fakeTimers = _getFakeTimers(); + + if (fakeTimers === this._environment.fakeTimersModern) { + fakeTimers.setAdvanceTimersAutomatically(autoAdvance); + } else { + throw new TypeError( + '`jest.setAdvanceTimersAutomatically()` is not available when using legacy fake timers.', + ); + } + }, setMock: (moduleName, mock) => setMockFactory(moduleName, () => mock), setSystemTime: now => { const fakeTimers = _getFakeTimers();