-
Notifications
You must be signed in to change notification settings - Fork 273
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
Conversation
src/flushMicroTasks.js
Outdated
@@ -1,5 +1,6 @@ | |||
// @flow | |||
import { printDeprecationWarning } from './helpers/errors'; | |||
import { setImmediate } from './helpers/getTimerFuncs'; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
@thymikee and other maintainers, what do you think of this PR? |
Still haven't found enough time to review this properly. Please bare with us a while longer 🙈 |
No problem! I appreciate the demands of reviewing code like this :) |
There was a problem hiding this 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/waitFor.js
Outdated
let interval = options?.interval ?? DEFAULT_INTERVAL; | ||
|
||
if (interval < 1) interval = 1; | ||
const maxTries = Math.ceil(timeout / interval); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
@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? |
UP 🙄 |
Any updates on when this gets reviewed? |
@@ -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])( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
export other timer helpers, adjust tests and docs
Thanks! Booking some time to have a look at this soon |
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 |
How do we feel about merging this? 😀 |
@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); |
There was a problem hiding this comment.
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
@@ -0,0 +1,10 @@ | |||
const reactNativePreset = require('react-native/jest-preset'); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this 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
Thank you @michaelduminy, great contribution! |
This is released under https://github.com/callstack/react-native-testing-library/releases/tag/v8.0.0-rc.0 |
Good job! Works like a charm. |
Awesome that this landed, thanks for reviewing and cleaning up things! |
@thymikee Are we waiting for other PRs to land before releasing version 8 (non-RC)? |
Not really, I should promote the latest version to stable v8. Will do it once I find some time soon |
@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. |
You can treat the RC as a stable version at this point. I will promote it soon, but not sure when exactly. |
thanks, appreciate you and your work on this awesome library! |
Summary
If you use Timer Mocks in your tests and
waitFor
then you cannotawait
this as the timers internal towaitFor
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
orDate.now
will not affectwaitFor
.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 timeoutThere is a way to fix this, but it requires patching the
@jest/fake-timers
library as described here.