Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Exposing jest.runToFrame() from sinon/fake_timers #14598

Merged
merged 13 commits into from
Oct 5, 2023
10 changes: 10 additions & 0 deletions docs/JestObjectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,16 @@ This function is not available when using legacy fake timers implementation.

:::

### `jest.runToFrame()`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I add these changes to 29.7 docs or just the base docs like I have now?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just base 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we call it runToNextFrame? runToFrame sounds to me like it'd take which frame (or a number of frames) as an argument

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just mirrored the sinon api without too much thought 😊

runToNextFrame() feels nice and fits with runOnlyPendingTimers().
Another idea: advanceTimersToNextFrame() - fits in with `advanceTimersToNextTimer()

Which do you lean towards? (Or something else?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, advanceTimersToNextFrame sounds perfect to align with existing functions 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cc @SimenB

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm? I believe I answered the question? 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, for some reason I didn't see your reply


Advances all timers by the needed milliseconds to execute the next animation frame. This function is a helpful way to execute code that is scheduled using `requestAnimationFrame`.

:::info

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

:::

### `jest.clearAllTimers()`

Removes any pending timers from the timer system.
Expand Down
22 changes: 22 additions & 0 deletions docs/TimerMocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,28 @@ it('calls the callback after 1 second via advanceTimersByTime', () => {

Lastly, it may occasionally be useful in some tests to be able to clear all of the pending timers. For this, we have `jest.clearAllTimers()`.

## Advance Timers by frame

In applications, often you want to schedule work inside of an animation frame (via `requestAnimationFrame`). We expose a convenance method `jest.runToFrame()` to advance all timers enough to execute all actively scheduled animation frames.

```javascript
jest.useFakeTimers();
it('calls the animation frame callback after runToFrame()', () => {
const callback = jest.fn();

requestAnimationFrame(callback);

// At this point in time, the callback should not have been called yet
expect(callback).not.toBeCalled();

jest.runToFrame();

// Now our callback should have been called!
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);
});
```

## Selective Faking

Sometimes your code may require to avoid overwriting the original implementation of one or another API. If that is the case, you can use `doNotFake` option. For example, here is how you could provide a custom mock function for `performance.mark()` in jsdom environment:
Expand Down
219 changes: 219 additions & 0 deletions packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,30 @@ describe('FakeTimers', () => {
timers.useFakeTimers();
expect(global.clearImmediate).not.toBe(origClearImmediate);
});

it('mocks requestAnimationFrame if it exists on global', () => {
const global = {
Date,
clearTimeout,
requestAnimationFrame: () => -1,
setTimeout,
} as unknown as typeof globalThis;
const timers = new FakeTimers({config: makeProjectConfig(), global});
timers.useFakeTimers();
expect(global.requestAnimationFrame).toBeDefined();
});

it('mocks cancelAnimationFrame if it exists on global', () => {
const global = {
Date,
cancelAnimationFrame: () => {},
clearTimeout,
setTimeout,
} as unknown as typeof globalThis;
const timers = new FakeTimers({config: makeProjectConfig(), global});
timers.useFakeTimers();
expect(global.cancelAnimationFrame).toBeDefined();
});
});

describe('runAllTicks', () => {
Expand Down Expand Up @@ -570,6 +594,182 @@ describe('FakeTimers', () => {
});
});

describe('runToFrame', () => {
it('runs scheduled animation frames in order', () => {
const global = {
Date,
clearTimeout,
process,
requestAnimationFrame: () => -1,
setTimeout,
} as unknown as typeof globalThis;

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

const runOrder: Array<string> = [];
const mock1 = jest.fn(() => runOrder.push('mock1'));
const mock2 = jest.fn(() => runOrder.push('mock2'));
const mock3 = jest.fn(() => runOrder.push('mock3'));

global.requestAnimationFrame(mock1);
global.requestAnimationFrame(mock2);
global.requestAnimationFrame(mock3);

timers.runToFrame();

expect(runOrder).toEqual(['mock1', 'mock2', 'mock3']);
});

it('should only run currently scheduled animation frames', () => {
const global = {
Date,
clearTimeout,
process,
requestAnimationFrame: () => -1,
setTimeout,
} as unknown as typeof globalThis;

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

const runOrder: Array<string> = [];
function run() {
runOrder.push('first-frame');

// scheduling another animation frame in the first frame
global.requestAnimationFrame(() => runOrder.push('second-frame'));
}

global.requestAnimationFrame(run);

// only the first frame should be executed
timers.runToFrame();

expect(runOrder).toEqual(['first-frame']);

timers.runToFrame();

expect(runOrder).toEqual(['first-frame', 'second-frame']);
});

it('should allow cancelling of scheduled animation frames', () => {
const global = {
Date,
cancelAnimationFrame: () => {},
clearTimeout,
process,
requestAnimationFrame: () => -1,
setTimeout,
} as unknown as typeof globalThis;

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

const runOrder: Array<string> = [];
const callback = () => runOrder.push('frame');

const timerId = global.requestAnimationFrame(callback);
global.cancelAnimationFrame(timerId);

// no frames should be executed
timers.runToFrame();

expect(runOrder).toEqual([]);
});

it('should only advance as much time is needed to get to the next frame', () => {
const global = {
Date,
cancelAnimationFrame: () => {},
clearTimeout,
process,
requestAnimationFrame: () => -1,
setTimeout,
} as unknown as typeof globalThis;

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

const runOrder: Array<string> = [];
const start = global.Date.now();

const callback = () => runOrder.push('frame');
global.requestAnimationFrame(callback);

// Advancing timers less than a frame (which is 16ms)
timers.advanceTimersByTime(6);
expect(global.Date.now()).toEqual(start + 6);

// frame not yet executed
expect(runOrder).toEqual([]);

// move timers forward to execute frame
timers.runToFrame();

// frame has executed as time has moved forward 10ms to get to the 16ms frame time
expect(runOrder).toEqual(['frame']);
expect(global.Date.now()).toEqual(start + 16);
});

it('should execute any timers on the way to the animation frame', () => {
const global = {
Date,
cancelAnimationFrame: () => {},
clearTimeout,
process,
requestAnimationFrame: () => -1,
setTimeout,
} as unknown as typeof globalThis;

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

const runOrder: Array<string> = [];

global.requestAnimationFrame(() => runOrder.push('frame'));

// scheduling a timeout that will be executed on the way to the frame
global.setTimeout(() => runOrder.push('timeout'), 10);

// move timers forward to execute frame
timers.runToFrame();

expect(runOrder).toEqual(['timeout', 'frame']);
});

it('should not execute any timers scheduled inside of a frame', () => {
const global = {
Date,
cancelAnimationFrame: () => {},
clearTimeout,
process,
requestAnimationFrame: () => -1,
setTimeout,
} as unknown as typeof globalThis;

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

const runOrder: Array<string> = [];

global.requestAnimationFrame(() => {
runOrder.push('frame');
// scheduling a timer inside of a frame
global.setTimeout(() => runOrder.push('timeout'), 1);
});

timers.runToFrame();

// timeout not yet executed
expect(runOrder).toEqual(['frame']);

// validating that the timer will still be executed
timers.advanceTimersByTime(1);
expect(runOrder).toEqual(['frame', 'timeout']);
});
});

describe('reset', () => {
it('resets all pending setTimeouts', () => {
const global = {
Expand Down Expand Up @@ -649,6 +849,25 @@ describe('FakeTimers', () => {
timers.advanceTimersByTime(50);
expect(mock1).toHaveBeenCalledTimes(0);
});

it('resets all scheduled animation frames', () => {
const global = {
Date,
clearTimeout,
process,
requestAnimationFrame: () => -1,
setTimeout,
} as unknown as typeof globalThis;
const timers = new FakeTimers({config: makeProjectConfig(), global});
timers.useFakeTimers();

const mock1 = jest.fn();
global.requestAnimationFrame(mock1);

timers.reset();
timers.runAllTimers();
expect(mock1).toHaveBeenCalledTimes(0);
});
});

describe('runOnlyPendingTimers', () => {
Expand Down
10 changes: 10 additions & 0 deletions packages/jest-fake-timers/src/__tests__/sinon-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ const mockWithGlobal = {
install: mockInstall,
timers: {
Date: jest.fn(),
cancelAnimationFrame: jest.fn(),
clearImmediate: jest.fn(),
clearInterval: jest.fn(),
clearTimeout: jest.fn(),
hrtime: jest.fn(),
nextTick: jest.fn(),
performance: jest.fn(),
queueMicrotask: jest.fn(),
requestAnimationFrame: jest.fn(),
setImmediate: jest.fn(),
setInterval: jest.fn(),
setTimeout: jest.fn(),
Expand Down Expand Up @@ -57,13 +59,15 @@ describe('`@sinonjs/fake-timers` integration', () => {
shouldClearNativeTimers: true,
toFake: [
'Date',
'cancelAnimationFrame',
'clearImmediate',
'clearInterval',
'clearTimeout',
'hrtime',
'nextTick',
'performance',
'queueMicrotask',
'requestAnimationFrame',
'setImmediate',
'setInterval',
'setTimeout',
Expand Down Expand Up @@ -93,12 +97,14 @@ describe('`@sinonjs/fake-timers` integration', () => {
shouldAdvanceTime: true,
shouldClearNativeTimers: true,
toFake: [
'cancelAnimationFrame',
'clearImmediate',
'clearInterval',
'clearTimeout',
'hrtime',
'performance',
'queueMicrotask',
'requestAnimationFrame',
'setImmediate',
'setInterval',
'setTimeout',
Expand Down Expand Up @@ -126,12 +132,14 @@ describe('`@sinonjs/fake-timers` integration', () => {
shouldAdvanceTime: true,
shouldClearNativeTimers: true,
toFake: [
'cancelAnimationFrame',
'clearImmediate',
'clearInterval',
'clearTimeout',
'hrtime',
'nextTick',
'performance',
'requestAnimationFrame',
'setImmediate',
'setInterval',
'setTimeout',
Expand Down Expand Up @@ -166,12 +174,14 @@ describe('`@sinonjs/fake-timers` integration', () => {
shouldClearNativeTimers: true,
toFake: [
'Date',
'cancelAnimationFrame',
'clearImmediate',
'clearInterval',
'clearTimeout',
'nextTick',
'performance',
'queueMicrotask',
'requestAnimationFrame',
'setImmediate',
'setInterval',
'setTimeout',
Expand Down
6 changes: 6 additions & 0 deletions packages/jest-fake-timers/src/modernFakeTimers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ export default class FakeTimers {
}
}

runToFrame(): void {
if (this._checkFakeTimers()) {
this._clock.runToFrame();
}
}

useRealTimers(): void {
if (this._fakingTime) {
this._clock.uninstall();
Expand Down