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

fix(breaking): use real timers internally to fix awaiting with fake timers #568

Merged
merged 12 commits into from Mar 2, 2021
Merged

Conversation

mikeduminy
Copy link
Contributor

@mikeduminy mikeduminy commented Oct 8, 2020

Summary

If you use Timer Mocks in your tests and waitFor then you cannot await this as the timers internal to waitFor would be replaced by the mock timers and thus never resolve. This PR borrows techniques from wait-for-expect and @testing-library/dom to detect if fake timers are in-use, switch back to real timers for a specific call, then re-instate the fake timers using the timer implementation that was present before.

This implementation also ensures that any mocks of Date or Date.now will not affect waitFor.

Outstanding work: update the docs if this lands.

fixes #506

Test plan

Added multiple tests to ensure this works. Unfortunately there are problems when awaiting a promise that uses 'modern' timers (the default in jest 27) and the react-native preset polyfilling Promise.

NOTE: jest.useFakeTimers('modern') and awaiting a promise in the context of react-native will timeout

There is a way to fix this, but it requires patching the @jest/fake-timers library as described here.

@@ -1,5 +1,6 @@
// @flow
import { printDeprecationWarning } from './helpers/errors';
import { setImmediate } from './helpers/getTimerFuncs';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure about this change. Should promises always be flushed through real timers? Where does this flush even get called?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@thymikee do you know more about where this flush is used and if it should always use real timers?

@mikeduminy
Copy link
Contributor Author

@thymikee and other maintainers, what do you think of this PR?

@thymikee
Copy link
Member

Still haven't found enough time to review this properly. Please bare with us a while longer 🙈

@mikeduminy
Copy link
Contributor Author

No problem! I appreciate the demands of reviewing code like this :)

@thymikee thymikee requested review from mdjastrzebski and cross19xx and removed request for mdjastrzebski October 21, 2020 10:42
Copy link
Member

@mdjastrzebski mdjastrzebski left a comment

Choose a reason for hiding this comment

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

I'm not a specialist on intricacies of jest timers so I just toss my two cents.

src/__tests__/timerUtils.js Outdated Show resolved Hide resolved
src/waitFor.js Outdated
let interval = options?.interval ?? DEFAULT_INTERVAL;

if (interval < 1) interval = 1;
const maxTries = Math.ceil(timeout / interval);
Copy link
Member

Choose a reason for hiding this comment

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

maxTries maybe wrong, since interval is not guaranted to be exact, for small intervals the delay might be larger due to evenc cycle time.

delay Optional
The time, in milliseconds (thousandths of a second), the timer should wait before the specified function or code is executed. If this parameter is omitted, a value of 0 is used, meaning execute "immediately", or more accurately, the next event cycle. Note that in either case, the actual delay may be longer than intended; see Reasons for delays longer than specified below.

https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout

Not sure if that affects your code logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's an interesting point but I don't think it affects things. There is a slight delay when scheduling timeouts and this may lead to a case where a timeout is reached later than it should have been, but this should only happen if the interval is 0 or otherwise very low. We could set a minimum interval?

Other notes:

  • it's a good idea to not rely on Date since this can also be mocked
  • this approach is what RNTL v6 used
  • in the future this can be improved by borrowing an approach from dom-testing-library

Copy link
Member

Choose a reason for hiding this comment

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

Maybe we should adopt wait-for from DOM Testing Library, what do you think? https://github.com/testing-library/dom-testing-library/blob/master/src/wait-for.js
Interesting thing is that inside they advance fake timers, which we currently miss. There may be some other small edge-cases they handle already. Sounds like a sane thing to do

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed waitFor to be based on DOM Testing Library's implementation

@pke
Copy link
Contributor

pke commented Nov 19, 2020

@michaelduminy thanks for this patch, I really hope this solves the rest of the problems the modern fake timers introduced. Could you please solve the potential merge conflicts so this PR could be further reviewed and merged?

@GoMino
Copy link

GoMino commented Nov 24, 2020

UP 🙄

@flextremedev
Copy link

Any updates on when this gets reviewed?

@thymikee
Copy link
Member

Hey, it's a bit of a pain to get through it. Bare with us please. I've got @jaworek and @suezzo working on it, hopefully they can provide some findings here :)

@@ -130,30 +131,21 @@ test('waits with custom interval', async () => {
expect(mockFn).toHaveBeenCalledTimes(4);
});

test('works with legacy fake timers', async () => {
jest.useFakeTimers('legacy');
test.each([TimerMode.Default, TimerMode.Legacy])(
Copy link
Member

Choose a reason for hiding this comment

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

we should now test all timers legacy and modern, the same, as they're supposed to work the same

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately this does not work right now because the RN jest setup patches global.Promise which breaks modern timers.

This PR would need to land first.

An alternative approach is to manually capture and restore the original global.Promise as seen here. WDYT?

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 went with the alternative approach

@mikeduminy
Copy link
Contributor Author

Update: tests are all passing using legacy and modern fake timers.

Please have another look @thymikee @jaworek @suezzo 🙏

@thymikee
Copy link
Member

thymikee commented Feb 8, 2021

Thanks! Booking some time to have a look at this soon

@Trancever
Copy link
Contributor

I can confirm this PR works in my case. I published a fork to the npm, so feel free to try it out - https://www.npmjs.com/package/react-native-testing-library-fork

@mikeduminy
Copy link
Contributor Author

How do we feel about merging this? 😀

@thymikee
Copy link
Member

@michaelduminy I feel very strongly to do it! :D I'm just swamped with work recently and want to have a proper look at this before merging. Targeting next week for the work on this 🙂

@@ -76,39 +77,63 @@ test('waits for element with custom interval', async () => {
// suppress
}

expect(mockFn).toHaveBeenCalledTimes(3);
expect(mockFn).toHaveBeenCalledTimes(2);
Copy link
Member

Choose a reason for hiding this comment

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

this is a change in behavior

@thymikee thymikee changed the title Use real timers internally feat(breaking): use real timers internally; decrease timeout to 1s Mar 2, 2021
@thymikee thymikee changed the title feat(breaking): use real timers internally; decrease timeout to 1s fix(breaking): use real timers internally to fix awaiting with fake timers Mar 2, 2021
@@ -0,0 +1,10 @@
const reactNativePreset = require('react-native/jest-preset');
Copy link

@sbalay sbalay Mar 2, 2021

Choose a reason for hiding this comment

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

Is this being exposed as a jest preset of @testing-library/react-native or is it just used locally for the tests in this codebase? If exposed, it should be documented somewhere

Copy link
Member

Choose a reason for hiding this comment

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

Yup, will do in a followup

Copy link
Member

@thymikee thymikee left a comment

Choose a reason for hiding this comment

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

This makes sense. I'm good with the changes that make us vendor implementation of wait-for from the dom-testing-library. I think it's safe to assume this change may be breaking for some users, so I'd like to include it in next major. We also need to expose the preset and document usage when Jest flips the switch

@thymikee thymikee merged commit 2ccec3c into callstack:master Mar 2, 2021
@thymikee
Copy link
Member

thymikee commented Mar 2, 2021

Thank you @michaelduminy, great contribution!

@thymikee
Copy link
Member

thymikee commented Mar 2, 2021

@flextremedev
Copy link

Good job! Works like a charm.

@mikeduminy
Copy link
Contributor Author

Awesome that this landed, thanks for reviewing and cleaning up things!

@mikeduminy
Copy link
Contributor Author

@thymikee Are we waiting for other PRs to land before releasing version 8 (non-RC)?

@thymikee
Copy link
Member

thymikee commented May 24, 2021

Not really, I should promote the latest version to stable v8. Will do it once I find some time soon

@henrymoulton
Copy link

@thymikee anything we can do to help get v8 out as stable? Happy to test the RC but currently test suite isn't that large.

@thymikee
Copy link
Member

thymikee commented Aug 4, 2021

You can treat the RC as a stable version at this point. I will promote it soon, but not sure when exactly.

@henrymoulton
Copy link

thanks, appreciate you and your work on this awesome library!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Using Jest mock timers and waitFor together causes tests to timeout
9 participants