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 23, 2024
1 parent de306d5 commit a238999
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 117 deletions.
27 changes: 23 additions & 4 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -674,11 +674,30 @@ type FakeableAPI =

type ModernFakeTimersConfig = {
/**
* If set to `true` all timers will be advanced automatically by 20 milliseconds
* every 20 milliseconds. A custom time delta may be provided by passing a number.
* The default is `false`.
* There are 3 different types of modes for advancing timers:
*
* - 'manual': Timers do not advance without explicit, manual calls to the tick
* APIs (`jest.advanceTimersToNextTimer`, `jest.runAllTimers`, etc). This mode is equivalent to `false`.
* - 'nextAsync': Jest will continuously await 'jest.advanceTimersToNextTimerAsync' until the mode changes.
* With this mode, jest will advance the clock to the next timer in the queue after a macrotask.
* As a result, 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.
* - 'interval': In this mode, all timers will be advanced automatically
* by the number of milliseconds provided in the delta. If the delta is
* not specified, 20 will be used by default. This mode is equivalent to `true` or providing a number for the delta.
*
* The 'nextAsync' mode differs from `interval` 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 interval,
* without manually advancing time in the test, would take `1000 / advanceTimersMs`
* real time to reach and execute the timer.
*
* @defaultValue
* The default mode is `'manual'` (equivalent to `false`).
*/
advanceTimers?: boolean | number;
advanceTimers?: boolean | number | AdvanceTimersConfig;
/**
* List of names of APIs that should not be faked. The default is `[]`, meaning
* all APIs are faked.
Expand Down
36 changes: 25 additions & 11 deletions docs/JestObjectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -882,11 +882,30 @@ type FakeableAPI =

type FakeTimersConfig = {
/**
* If set to `true` all timers will be advanced automatically by 20 milliseconds
* every 20 milliseconds. A custom time delta may be provided by passing a number.
* The default is `false`.
* There are 3 different types of modes for advancing timers:
*
* - 'manual': Timers do not advance without explicit, manual calls to the tick
* APIs (`jest.advanceTimersToNextTimer`, `jest.runAllTimers`, etc). This mode is equivalent to `false`.
* - 'nextAsync': Jest will continuously await 'jest.advanceTimersToNextTimerAsync' until the mode changes.
* With this mode, jest will advance the clock to the next timer in the queue after a macrotask.
* As a result, 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.
* - 'interval': In this mode, all timers will be advanced automatically
* by the number of milliseconds provided in the delta. If the delta is
* not specified, 20 will be used by default. This mode is equivalent to `true` or providing a number for the delta.
*
* The 'nextAsync' mode differs from `interval` 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 interval,
* without manually advancing time in the test, would take `1000 / advanceTimersMs`
* real time to reach and execute the timer.
*
* @defaultValue
* The default mode is `'manual'` (equivalent to `false`).
*/
advanceTimers?: boolean | number;
advanceTimers?: boolean | number | AdvanceTimersConfig;
/**
* List of names of APIs that should not be faked. The default is `[]`, meaning
* all APIs are faked.
Expand Down Expand Up @@ -1067,14 +1086,9 @@ 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:
### `jest.setAdvanceTimers(config)`

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.
Used to update the configured `AdvanceTimersConfig` after the fake timers have been installed. See `jest.useFakeTimers` for more information.

### `jest.now()`

Expand Down
29 changes: 19 additions & 10 deletions packages/jest-environment/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,6 @@ export interface Jest {
* Not available when using legacy fake timers implementation.
*/
advanceTimersToNextTimerAsync(steps?: number): Promise<void>;
/**
* 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.
*/
Expand Down Expand Up @@ -425,4 +415,23 @@ export interface Jest {
* performance, time and timer APIs.
*/
useRealTimers(): Jest;
/**
* Updates the mode of advancing timers when using fake timers.
*
* @param config The configuration to use for advancing timers
*
* When mode is 'nextAsync', 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 mode is '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.
* In addition, the mode can only be changed from 'nextAsync' to 'manual' or vice versa.
* It cannot currently be used with 'interval' from `AdvanceTimersConfig`.
*/
setAdvanceTimers(config: {mode: 'manual' | 'nextAsync'}): void;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`FakeTimers advanceTimers nextAsync warns when trying to set tick mode when already using interval 1`] = `"\`setTickMode\` cannot be used when fake timers are configured to advance at an interval."`;

exports[`FakeTimers runAllTimers warns when trying to advance timers while real timers are used 1`] = `"A function to advance timers was called but the timers APIs are not replaced with fake timers. Call \`jest.useFakeTimers()\` in this test file or enable fake timers for all tests by setting 'fakeTimers': {'enableGlobally': true} in Jest configuration file."`;
161 changes: 93 additions & 68 deletions packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1251,6 +1251,98 @@ describe('FakeTimers', () => {
});
});

describe('advanceTimers', () => {
let global: typeof globalThis;
let timers: FakeTimers;
beforeEach(() => {
global = {
Date,
Promise,
clearInterval,
clearTimeout,
console,
process,
setInterval,
setTimeout,
} as unknown as typeof globalThis;

timers = new FakeTimers({config: makeProjectConfig(), global});
});

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

describe('nextAsync', () => {
beforeEach(() => {
timers.useFakeTimers({advanceTimers: {mode: 'nextAsync'}});
});

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

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

it('warns when trying to set tick mode when already using interval', () => {
const consoleWarnSpy = jest
.spyOn(console, 'warn')
.mockImplementation(() => {
// nothing
});
timers.useFakeTimers({advanceTimers: {mode: 'interval'}});
timers.setTickMode('nextAsync');
expect(
consoleWarnSpy.mock.calls[0][0].split('\nStack Trace')[0],
).toMatchSnapshot();
consoleWarnSpy.mockRestore();
});
});
});

describe('runAllTimersAsync', () => {
it('should advance the clock to the last scheduled timer', async () => {
const global = {
Expand All @@ -1260,6 +1352,7 @@ describe('FakeTimers', () => {
process,
setTimeout,
} as unknown as typeof globalThis;

const timers = new FakeTimers({config: makeProjectConfig(), global});
timers.useFakeTimers();
timers.setSystemTime(0);
Expand Down Expand Up @@ -1330,74 +1423,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';
Loading

0 comments on commit a238999

Please sign in to comment.