Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

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

Merged
merged 7 commits into from
Sep 10, 2022

Conversation

robhogan
Copy link
Contributor

@robhogan robhogan commented Sep 10, 2022

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
Copy link
Member

SimenB commented Sep 10, 2022

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

@@ -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.
Copy link
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
Contributor Author

Choose a reason for hiding this comment

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

I guess it could be useful for modern timers too if doNotFake: ['Date', 'performance'] is set?

Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed, that’s a good point. I was also wondering what does it do if called without faking time? Would it be worth mentioning that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah good point - currently, it'll pass through to now() in the default (modern) fake timers implementation, which will try to use this._clock.now, which will throw as this._clock is only initialised by useFakeTimers(). Not good!

Other APIs in that file wrap calls in _checkTimers() which prints a warning if fake timers aren't active, and then usually return some dummy value.

So I guess two options:

  1. Return the real Date.now() (and document that behaviour)
  2. Warn on the console and return some dummy value, probably 0

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps 1? Since jest.now() is equivalent to mocked Date.now(), then for me it makes sense to return the real Date.now() also in case if time is not faked.

Co-authored-by: Tom Mrazauskas <tom@mrazauskas.de>
@robhogan
Copy link
Contributor Author

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

No rush! 😅

Copy link
Member

@SimenB SimenB left a comment

Choose a reason for hiding this comment

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

great stuff, thanks!

@SimenB
Copy link
Member

SimenB commented Sep 10, 2022

requestAnimationFrame does actually pass the current high-res clock time to its callback, but for "reasons"(?) TimingAnimation doesn't use it

It's relatively new that requestAnimationFrame in legacy timers provide it: #11567, might be why?

EDIT: Ah, I see you point it out as well 😀

@SimenB SimenB merged commit bedbed1 into jestjs:main Sep 10, 2022
@robhogan
Copy link
Contributor Author

Thanks for merging, but I was looking to sort out the issue @mrazauskas pointed out above re undefined behaviour when real timers are used. I'll follow up in another PR with that - planning to fall back to real Date.now()

@SimenB
Copy link
Member

SimenB commented Sep 10, 2022

Ah sorry, thought it was ready

@robhogan
Copy link
Contributor Author

Ah sorry, thought it was ready

No worries, I should've been clearer. You're very quick! 😅

@SimenB
Copy link
Member

SimenB commented Sep 28, 2022

@github-actions
Copy link

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 30, 2022
@robhogan robhogan deleted the feat/fake-timers-now branch December 19, 2022 20:20
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants