From 5d84dbf2198e59da1eef11e7bb753fce724513f6 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Thu, 26 Sep 2024 10:38:41 -0700 Subject: [PATCH] fixup! feat(jest-fake-timers): Add feature to enable automatically advancing timers --- .../src/__tests__/modernFakeTimers.test.ts | 71 +++++++++++++++++-- .../jest-fake-timers/src/modernFakeTimers.ts | 35 +++++++-- 2 files changed, 94 insertions(+), 12 deletions(-) diff --git a/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts b/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts index feacc59fec38..3d6d8663540d 100644 --- a/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts @@ -1270,6 +1270,7 @@ describe('FakeTimers', () => { }); afterEach(() => { + timers.clearAllTimers(); timers.dispose(); }); @@ -1308,21 +1309,27 @@ describe('FakeTimers', () => { 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)); + const p1 = new Promise(resolve => global.setTimeout(resolve, 1)); + const p2 = new Promise(resolve => global.setTimeout(() => { + p2Resolved = true; + resolve(); + }, 2)); + const p3 = new Promise(resolve => global.setTimeout(resolve, 3)); await expect(p1).resolves.toBeUndefined(); timers.setTickMode('manual'); + // wait real, unpatched time to ensure p2 doesn't resolve on its own await new Promise(resolve => setTimeout(resolve, 5)); expect(p2Resolved).toBe(false); + // simply updating the tick mode should not result in time immediately advancing timers.setTickMode('nextAsync'); + expect(p2Resolved).toBe(false); + + // wait real, unpatched time and observe p2 and p3 resolve on their own await new Promise(resolve => setTimeout(resolve, 5)); - await expect(p2).resolves.toBe(true); + await expect(p2).resolves.toBeUndefined(); await expect(p3).resolves.toBeUndefined(); expect(p2Resolved).toBe(true); }); @@ -1340,6 +1347,58 @@ describe('FakeTimers', () => { ).toMatchSnapshot(); consoleWarnSpy.mockRestore(); }); + + describe('works with manual calls to async tick functions', () => { + let timerLog: number[]; + let allTimersDone: Promise; + beforeEach(() => { + timerLog = []; + allTimersDone = new Promise(resolve => { + global.setTimeout(() => timerLog.push(1), 1); + global.setTimeout(() => timerLog.push(2), 2); + global.setTimeout(() => timerLog.push(3), 3); + global.setTimeout(() => { + timerLog.push(4); + global.setTimeout(() => { + timerLog.push(5); + resolve(); + }, 1); + }, 5); + }); + }); + + afterEach(async () => { + await allTimersDone; + expect(timerLog).toEqual([1, 2, 3, 4, 5]); + }); + + it('runAllTimersAsync', async () => { + await timers.runAllTimersAsync(); + expect(timerLog).toEqual([1, 2, 3, 4, 5]); + }); + + it('runOnlyPendingTimersAsync', async () => { + await timers.runOnlyPendingTimersAsync(); + // 5 should not resolve because it wasn't queued when we called "only pending timers" + expect(timerLog).toEqual([1, 2, 3, 4]); + }); + + it('advanceTimersToNextTimerAsync', async () => { + await timers.advanceTimersToNextTimerAsync(); + expect(timerLog).toEqual([1]); + await timers.advanceTimersToNextTimerAsync(); + expect(timerLog).toEqual([1, 2]); + await timers.advanceTimersToNextTimerAsync(); + expect(timerLog).toEqual([1, 2, 3]); + }); + + it('advanceTimersByTimeAsync', async () => { + await timers.advanceTimersByTimeAsync(2); + expect(timerLog).toEqual([1, 2]); + await timers.advanceTimersByTimeAsync(1); + expect(timerLog).toEqual([1, 2, 3]); + }); + }); }); }); diff --git a/packages/jest-fake-timers/src/modernFakeTimers.ts b/packages/jest-fake-timers/src/modernFakeTimers.ts index 3f77142dbbb9..5d39e0fe7ab1 100644 --- a/packages/jest-fake-timers/src/modernFakeTimers.ts +++ b/packages/jest-fake-timers/src/modernFakeTimers.ts @@ -19,6 +19,7 @@ export type TimerTickMode = 'manual' | 'nextAsync' | 'interval'; export default class FakeTimers { private _clock!: InstalledClock; + private _nativeTimeout: typeof setTimeout; private readonly _config: Config.ProjectConfig; private _fakingTime: boolean; private _usingSinonAdvanceTime = false; @@ -38,6 +39,7 @@ export default class FakeTimers { }) { this._global = global; this._config = config; + this._nativeTimeout = global.setTimeout; this._fakingTime = false; this._fakeTimers = withGlobal(global); @@ -61,7 +63,7 @@ export default class FakeTimers { async runAllTimersAsync(): Promise { if (this._checkFakeTimers()) { - await this._clock.runAllAsync(); + await this._runWithoutNextAsyncTickMode(() => this._clock.runAllAsync()); } } @@ -73,7 +75,7 @@ export default class FakeTimers { async runOnlyPendingTimersAsync(): Promise { if (this._checkFakeTimers()) { - await this._clock.runToLastAsync(); + await this._runWithoutNextAsyncTickMode(() => this._clock.runToLastAsync()); } } @@ -94,9 +96,11 @@ export default class FakeTimers { async advanceTimersToNextTimerAsync(steps = 1): Promise { if (this._checkFakeTimers()) { for (let i = steps; i > 0; i--) { - await this._clock.nextAsync(); - // Fire all timers at this point: https://github.com/sinonjs/fake-timers/issues/250 - await this._clock.tickAsync(0); + await this._runWithoutNextAsyncTickMode(async () => { + await this._clock.nextAsync(); + // Fire all timers at this point: https://github.com/sinonjs/fake-timers/issues/250 + await this._clock.tickAsync(0); + }); if (this._clock.countTimers() === 0) { break; @@ -113,7 +117,7 @@ export default class FakeTimers { async advanceTimersByTimeAsync(msToRun: number): Promise { if (this._checkFakeTimers()) { - await this._clock.tickAsync(msToRun); + await this._runWithoutNextAsyncTickMode(() => this._clock.tickAsync(msToRun)); } } @@ -297,10 +301,29 @@ export default class FakeTimers { } const {counter} = this.tickMode; + // Wait a macrotask to prevent advancing time immediately when + await new Promise(resolve => void this._nativeTimeout(resolve)); while (this.tickMode.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(); } } + + /** + * Temporarily disables the `nextAsync` tick mode while the given function + * executes. Used to prevent the auto-advance from advancing while the + * user is waiting for a manually requested async tick. + */ + private async _runWithoutNextAsyncTickMode(fn: () => Promise) { + let resetModeToNextAsync = false; + if (this.tickMode.mode === 'nextAsync') { + this.setTickMode('manual'); + resetModeToNextAsync = true; + } + await fn(); + if (resetModeToNextAsync) { + this.setTickMode('nextAsync'); + } + } }