diff --git a/CHANGELOG.md b/CHANGELOG.md index 85442042d9ff..94166694052f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Features +- `[@jest/fake-timers]` Add `jest.now()` to return the current fake clock time ([#13244](https://github.com/facebook/jest/pull/13244)) + ### Fixes ### Chore & Maintenance diff --git a/docs/JestObjectAPI.md b/docs/JestObjectAPI.md index 8fcc8f2048fb..274d7c919d7f 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -799,6 +799,10 @@ 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.now()` + +Returns the time in ms of the current fake clock. This is equivalent to `Date.now()` if `Date` has been mocked. + ### `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 e5a4c3037e65..a683bba060cf 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -152,6 +152,10 @@ export interface Jest { * Returns the number of fake timers still left to run. */ getTimerCount(): number; + /** + * Returns the current time in ms of the fake timer clock. + */ + now(): number; /** * Determines if the given function is a mocked function. */ diff --git a/packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts b/packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts index 11267959751e..736cc354a00d 100644 --- a/packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts @@ -1589,4 +1589,45 @@ describe('FakeTimers', () => { expect(timers.getTimerCount()).toEqual(0); }); }); + + describe('now', () => { + it('returns the current clock', () => { + const timers = new FakeTimers({ + config, + global: globalThis, + moduleMocker, + timerConfig, + }); + + timers.useFakeTimers(); + globalThis.setTimeout(() => {}, 2); + globalThis.setTimeout(() => {}, 100); + + expect(timers.now()).toEqual(0); + + // This should run the 2ms timer, and then advance _now by 3ms + timers.advanceTimersByTime(5); + expect(timers.now()).toEqual(5); + + // Advance _now even though there are no timers to run + timers.advanceTimersByTime(5); + expect(timers.now()).toEqual(10); + + // Run up to the 100ms timer + timers.runAllTimers(); + expect(timers.now()).toEqual(100); + + // Verify that runOnlyPendingTimers advances now only up to the first + // recursive timer + globalThis.setTimeout(function infinitelyRecursingCallback() { + globalThis.setTimeout(infinitelyRecursingCallback, 20); + }, 10); + timers.runOnlyPendingTimers(); + expect(timers.now()).toEqual(110); + + // Reset should set now back to 0 + timers.reset(); + expect(timers.now()).toEqual(0); + }); + }); }); diff --git a/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts b/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts index fd3c7f37a3a1..b7e9120c41c6 100644 --- a/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts @@ -948,4 +948,44 @@ describe('FakeTimers', () => { expect(timers.getTimerCount()).toEqual(0); }); }); + + describe('now', () => { + it('returns the current clock', () => { + const timers = new FakeTimers({ + config: makeProjectConfig(), + global: globalThis, + }); + + timers.useFakeTimers(); + timers.setSystemTime(0); + globalThis.setTimeout(() => {}, 2); + globalThis.setTimeout(() => {}, 100); + + expect(timers.now()).toEqual(0); + + // This should run the 2ms timer, and then advance _now by 3ms + timers.advanceTimersByTime(5); + expect(timers.now()).toEqual(5); + + // Advance _now even though there are no timers to run + timers.advanceTimersByTime(5); + expect(timers.now()).toEqual(10); + + // Run up to the 100ms timer + timers.runAllTimers(); + expect(timers.now()).toEqual(100); + + // Verify that runOnlyPendingTimers advances now only up to the first + // recursive timer + globalThis.setTimeout(function infinitelyRecursingCallback() { + globalThis.setTimeout(infinitelyRecursingCallback, 20); + }, 10); + timers.runOnlyPendingTimers(); + expect(timers.now()).toEqual(110); + + // For modern timers, reset() explicitly preserves the clock time + timers.reset(); + expect(timers.now()).toEqual(110); + }); + }); }); diff --git a/packages/jest-fake-timers/src/legacyFakeTimers.ts b/packages/jest-fake-timers/src/legacyFakeTimers.ts index e4b719f5d6b3..a5b7766d0279 100644 --- a/packages/jest-fake-timers/src/legacyFakeTimers.ts +++ b/packages/jest-fake-timers/src/legacyFakeTimers.ts @@ -134,6 +134,10 @@ export default class FakeTimers { this._timers = new Map(); } + now(): number { + return this._now; + } + runAllTicks(): void { this._checkFakeTimers(); // Only run a generous number of ticks and then bail. @@ -200,13 +204,15 @@ export default class FakeTimers { // This is just to help avoid recursive loops let i; for (i = 0; i < this._maxLoops; i++) { - const nextTimerHandle = this._getNextTimerHandle(); + const nextTimerHandleAndExpiry = this._getNextTimerHandleAndExpiry(); // If there are no more timer handles, stop! - if (nextTimerHandle === null) { + if (nextTimerHandleAndExpiry === null) { break; } + const [nextTimerHandle, expiry] = nextTimerHandleAndExpiry; + this._now = expiry; this._runTimerHandle(nextTimerHandle); // Some of the immediate calls could be enqueued @@ -239,7 +245,10 @@ export default class FakeTimers { timerEntries .sort(([, left], [, right]) => left.expiry - right.expiry) - .forEach(([timerHandle]) => this._runTimerHandle(timerHandle)); + .forEach(([timerHandle, timer]) => { + this._now = timer.expiry; + this._runTimerHandle(timerHandle); + }); } advanceTimersToNextTimer(steps = 1): void { @@ -265,21 +274,16 @@ export default class FakeTimers { // This is just to help avoid recursive loops let i; for (i = 0; i < this._maxLoops; i++) { - const timerHandle = this._getNextTimerHandle(); + const timerHandleAndExpiry = this._getNextTimerHandleAndExpiry(); // If there are no more timer handles, stop! - if (timerHandle === null) { - break; - } - const timerValue = this._timers.get(timerHandle); - if (timerValue === undefined) { + if (timerHandleAndExpiry === null) { break; } - const nextTimerExpiry = timerValue.expiry; + const [timerHandle, nextTimerExpiry] = timerHandleAndExpiry; + if (this._now + msToRun < nextTimerExpiry) { - // There are no timers between now and the target we're running to, so - // adjust our time cursor and quit - this._now += msToRun; + // There are no timers between now and the target we're running to break; } else { msToRun -= nextTimerExpiry - this._now; @@ -288,6 +292,9 @@ export default class FakeTimers { } } + // Advance the clock by whatever time we still have left to run + this._now += msToRun; + if (i === this._maxLoops) { throw new Error( `Ran ${this._maxLoops} timers, and there are still more! ` + @@ -557,7 +564,7 @@ export default class FakeTimers { return this._timerConfig.idToRef(uuid); } - private _getNextTimerHandle() { + private _getNextTimerHandleAndExpiry(): [string, number] | null { let nextTimerHandle = null; let soonestTime = MS_IN_A_YEAR; @@ -568,13 +575,19 @@ export default class FakeTimers { } }); - return nextTimerHandle; + if (nextTimerHandle === null) { + return null; + } + + return [nextTimerHandle, soonestTime]; } private _runTimerHandle(timerHandle: TimerID) { const timer = this._timers.get(timerHandle); if (!timer) { + // Timer has been cleared - we'll hit this when a timer is cleared within + // another timer in runOnlyPendingTimers return; } diff --git a/packages/jest-fake-timers/src/modernFakeTimers.ts b/packages/jest-fake-timers/src/modernFakeTimers.ts index 633ddd22272a..14b0e1809669 100644 --- a/packages/jest-fake-timers/src/modernFakeTimers.ts +++ b/packages/jest-fake-timers/src/modernFakeTimers.ts @@ -122,6 +122,10 @@ export default class FakeTimers { return Date.now(); } + now(): number { + return this._clock.now; + } + getTimerCount(): number { if (this._checkFakeTimers()) { return this._clock.countTimers(); diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 6d7ee64ea5a3..cf0521e324db 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -2128,6 +2128,7 @@ export default class Runtime { isolateModules, mock, mocked, + now: () => _getFakeTimers().now(), requireActual: this.requireActual.bind(this, from), requireMock: this.requireMock.bind(this, from), resetAllMocks, diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index 972e242cd93a..53627054f652 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -413,6 +413,9 @@ expectError(jest.clearAllTimers(false)); expectType(jest.getTimerCount()); expectError(jest.getTimerCount(true)); +expectType(jest.now()); +expectError(jest.now('1995-12-17T03:24:00')); + expectType(jest.getRealSystemTime()); expectError(jest.getRealSystemTime(true));