Skip to content

(jest-fake-timers): Add now() API to get the fake clock time - #13244

Merged
SimenB merged 7 commits into
jestjs:mainfrom
robhogan:feat/fake-timers-now
Sep 10, 2022
Merged

(jest-fake-timers): Add now() API to get the fake clock time#13244
SimenB merged 7 commits into
jestjs:mainfrom
robhogan:feat/fake-timers-now

Conversation

@robhogan

@robhogan robhogan commented Sep 10, 2022

Copy link
Copy Markdown
Contributor

Summary

Currently, users of legacy fake timers have no way to mock other time-related APIs in a way that keeps a consistent clock with the Jest mocked APIs.

Exposing the fake time allows easy mocking of APIs like Date.now(), performance.now(), etc.

Use case

For example, React Native's TimingAnimation uses a combination of time elapsed according to Date.now() to interpolate state within each update, and requestAnimationFrame() to set a timer for the next update.

The legacy timers API in Jest 27+ mocks requestAnimationFrame (taking ~16ms of fake clock time per frame), but Date.now() is not mocked, which means that advanceTimersToTime runs some number of animation frames, but doesn't advance Date.now() time by the corresponding amount.

jest.runAllTimers() "works", but only because it actually ends up running for the real time of the animation, processing as many frames as the CPU allows.

What would be useful is a way to mock APIs like Date.now() or performance.now() in a manner consistent with the internal fake clock, but Jest doesn't expose the internal clock, so we're left guessing about what Date.now() should return after an unknown number of timers/frames have run.

(requestAnimationFrame does actually pass the current high-res clock time to its callback, but for "reasons"(?) TimingAnimation doesn't use it - in any case that doesn't really help a test author, and we'd still need to mock a start time.)

Before

About the best we can do at the moment is to advance frame by frame:

const FRAME_TIME = 16;
const DURATION = 500;
const mockNow = jest.spyOn(Date, 'now');
mockNow.mockReturnValueOnce(0);

// Trigger an animation in product code under test
triggerAnimation({ duration: DURATION });

for (let elapsed = 0; elapsed <= DURATION; elapsed += FRAME_TIME) {
  mockNow.mockReturnValueOnce(elapsed);
  advanceTimersByTime(FRAME_TIME);
}

expect(state).toBe(good);

(Alternatives include a mock implementation of Date.now() that "knows about" the number of frames processed, eg by assuming it's called once per frame, which is also not great!)

After

const DURATION = 500;
jest.spyOn(Date, 'now').mockImplementation(() => jest.now());

// Trigger an animation in product code under test
triggerAnimation({ duration: DURATION });
jest.advanceTimersByTime(DURATION);

expect(state).toBe(good);

Test Plan

I've added tests for modern and legacy timers, and also tested the "After" above with React Native animation.

@SimenB

SimenB commented Sep 10, 2022

Copy link
Copy Markdown
Member

CI is my bad - will fix as soon as I'm home from birthday gift shopping

Comment thread packages/jest-types/__typetests__/jest.test.ts
Comment thread docs/JestObjectAPI.md Outdated

### `jest.now()`

Returns the time in ms of the current fake clock.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Add a note this is equivalent to Date.now() for non-legacy timers?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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