Skip to content

Commit

Permalink
fixup! feat(jest-fake-timers): Add feature to enable automatically ad…
Browse files Browse the repository at this point in the history
…vancing timers
  • Loading branch information
atscott committed Sep 20, 2024
1 parent de306d5 commit 540e0b2
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 114 deletions.
30 changes: 15 additions & 15 deletions docs/JestObjectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,21 @@ Advances all timers by the needed milliseconds so that only the next timeouts/in

Optionally, you can provide `steps`, so it will run `steps` amount of next timeouts/intervals.

### `jest.advanceTimersToNextTimerAsync(mode)`

Configures whether timers advance automatically. When 'auto', 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.

:::info

This function is not available when using legacy fake timers implementation.

:::

### `jest.advanceTimersToNextTimerAsync(steps)`

Asynchronous equivalent of `jest.advanceTimersToNextTimer(steps)`. It allows any scheduled promise callbacks to execute _before_ running the timers.
Expand Down Expand Up @@ -1067,25 +1082,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.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()`.
Expand Down
26 changes: 17 additions & 9 deletions packages/jest-environment/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
*/

import type {Context} from 'vm';
import type {LegacyFakeTimers, ModernFakeTimers} from '@jest/fake-timers';
import type {
LegacyFakeTimers,
ModernFakeTimers,
TimerTickMode,
} from '@jest/fake-timers';
import type {Circus, Config, Global} from '@jest/types';
import type {Mocked, ModuleMocker} from 'jest-mock';

Expand Down Expand Up @@ -82,24 +86,28 @@ export interface Jest {
*/
advanceTimersToNextTimer(steps?: number): void;
/**
* Advances the clock to the the moment of the first scheduled timer, firing it.
* When called with no arguments, advances the clock to the moment of the first
* scheduled timer, firing it.
* Optionally, you can provide steps, so it will run steps amount of
* next timeouts/intervals.
*
* @remarks
* Not available when using legacy fake timers implementation.
*/
advanceTimersToNextTimerAsync(steps?: number): Promise<void>;
/**
* Configures whether timers advance automatically. With automatically advancing
* When called with a `TimerTickMode`, either 'manual' or 'auto', updates the
* behavior of the timer advancement without.
*
* When 'automatic', 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.
*
* When 'manual' (the default), timers will not advance automatically. Instead,
* timers must be advanced using APIs such as `advanceTimersToNextTimer`, `advanceTimersByTime`, etc.
*
* @remarks
* Not available when using legacy fake timers implementation.
*/
setAdvanceTimersAutomatically(autoAdvance: boolean): void;
advanceTimersToNextTimerAsync(
stepsOrTickMode?: number | TimerTickMode,
): Promise<void>;
/**
* Disables automatic mocking in the module loader.
*/
Expand Down
140 changes: 72 additions & 68 deletions packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,78 @@ describe('FakeTimers', () => {
expect(timers.now()).toBe(200);
expect(spy).toHaveBeenCalled();
});

describe('auto advance', () => {
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.advanceTimersToNextTimerAsync('auto');
});

afterEach(() => {
timers.dispose();
});

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.advanceTimersToNextTimerAsync('manual');
await new Promise(resolve => setTimeout(resolve, 5));
expect(p2Resolved).toBe(false);

timers.advanceTimersToNextTimerAsync('auto');
await new Promise(resolve => setTimeout(resolve, 5));
await expect(p2).resolves.toBe(true);
await expect(p3).resolves.toBeUndefined();
expect(p2Resolved).toBe(true);
});
});
});

describe('runAllTimersAsync', () => {
Expand Down Expand Up @@ -1330,74 +1402,6 @@ 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;
Expand Down
1 change: 1 addition & 0 deletions packages/jest-fake-timers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@

export {default as LegacyFakeTimers} from './legacyFakeTimers';
export {default as ModernFakeTimers} from './modernFakeTimers';
export type {TimerTickMode} from './modernFakeTimers';
20 changes: 14 additions & 6 deletions packages/jest-fake-timers/src/modernFakeTimers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import {
import type {Config} from '@jest/types';
import {formatStackTrace} from 'jest-message-util';

export type TimerTickMode = 'manual' | 'auto';

export default class FakeTimers {
private _clock!: InstalledClock;
private readonly _config: Config.ProjectConfig;
private _fakingTime: boolean;
private readonly _global: typeof globalThis;
private readonly _fakeTimers: FakeTimerWithContext;
private autoTickMode: {counter: number; mode: 'manual' | 'auto'} = {
private autoTickMode: {counter: number; mode: TimerTickMode} = {
counter: 0,
mode: 'manual',
};
Expand Down Expand Up @@ -88,9 +90,16 @@ export default class FakeTimers {
}
}

async advanceTimersToNextTimerAsync(steps = 1): Promise<void> {
async advanceTimersToNextTimerAsync(
stepsOrMode: number | TimerTickMode = 1,
): Promise<void> {
if (typeof stepsOrMode === 'string') {
this._setTickMode(stepsOrMode);
return;
}

if (this._checkFakeTimers()) {
for (let i = steps; i > 0; i--) {
for (let i = stepsOrMode; 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);
Expand Down Expand Up @@ -146,18 +155,17 @@ export default class FakeTimers {
this._fakingTime = true;
}

setAdvanceTimersAutomatically(autoAdvance: boolean): void {
private _setTickMode(newMode: TimerTickMode): 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) {
if (newMode === 'auto') {
this._advanceUntilModeChanges();
}
}
Expand Down
20 changes: 4 additions & 16 deletions packages/jest-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2322,13 +2322,12 @@ export default class Runtime {
},
advanceTimersToNextTimer: steps =>
_getFakeTimers().advanceTimersToNextTimer(steps),
advanceTimersToNextTimerAsync: async steps => {
advanceTimersToNextTimerAsync: stepsOrTickMode => {
const fakeTimers = _getFakeTimers();

if (fakeTimers === this._environment.fakeTimersModern) {
await fakeTimers.advanceTimersToNextTimerAsync(steps);
} else {
throw new TypeError(
return fakeTimers.advanceTimersToNextTimerAsync(stepsOrTickMode);

Check failure on line 2328 in packages/jest-runtime/src/index.ts

View workflow job for this annotation

GitHub Actions / Lint

Delete `············`
} else {

Check failure on line 2329 in packages/jest-runtime/src/index.ts

View workflow job for this annotation

GitHub Actions / Lint

Delete `··`
throw new TypeError(

Check failure on line 2330 in packages/jest-runtime/src/index.ts

View workflow job for this annotation

GitHub Actions / Lint

Delete `············`
'`jest.advanceTimersToNextTimerAsync()` is not available when using legacy fake timers.',
);
}
Expand Down Expand Up @@ -2407,17 +2406,6 @@ 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();
Expand Down

0 comments on commit 540e0b2

Please sign in to comment.