react-hooks-testing-library: `waitFor` doesn't work if jest fake timers are used
react-hooks-testing-libraryversion: 7.0.0reactversion: 17.0.2react-domversion: 17.0.2nodeversion: 14.16.0npmversion: 7.10.0
Problem
When using waitFor when Jest has been configured to use fake timers then the waitFor will not work and only “polls” once. After that the test just hangs until Jest comes in and fails the test with that the test exceeds the timeout time. Below is some code that showcases the problem.
import { renderHook } from '@testing-library/react-hooks'
it('does not work', async () => {
jest.useFakeTimers()
const { waitFor } = renderHook(() => {})
await waitFor(() => {
console.log('poll') // Is printed just once
expect(false).toBe(true)
}, { timeout: 25, interval: 10 })
// Fails with Exceeded timeout of 5000 ms for a test.
})
Basically the waitFor from @testing-library/react-hooks is using the faked setTimeout or setInterval which prevents it from working correctly.
There is a workaround (see suggested solution) but I recommend providing a nice error message when waitFor is used together with faked timers or maybe change the implemenation so it will work with fake timers.
Suggested solution
I found this issue and it seems that person has already been fixed in @testing-library/dom. From my perspective I can suggest maybe reuse that function instead of implementing it yourselves but I don’t really know the internal structure / code.
But after finding that issue and realizing that is has been fixed there, then I use the following code as a workaround which works fine.
import { waitFor } from '@testing-library/react'
it('works', async () => {
jest.useFakeTimers()
await waitFor(() => {
console.log('poll') // Is printed twice
expect(false).toBe(true)
}, { timeout: 25, interval: 10 })
// Fails with false is not equal to true
})
A more real world scenario
If curios on the actual problem I’m facing is to test the following hook:
function useSomething({ onSuccess }) {
const poll = useCallback(async () => {
const result = await fetch(/* ... */)
if (result.ok) onSuccess()
}, [onSuccess])
useEffect(() => {
const id = setInterval(() => { poll() }, 2000)
return () => clearInterval(id)
}, [poll])
}
What I want to do is test that it invokes the onSuccess function on a successfull poll.
it('invokes the `onSuccess` on successfull poll', async () => {
const onSuccess = jest.fn()
jest.useFakeTimers()
const { waitFor } = renderHook(() => useSomething({ onSuccess }))
jest.runOnlyPendingTimers()
await waitFor(() => expect(onSuccess).toHaveBeenCalled())
})
About this issue
- Original URL
- State: open
- Created 3 years ago
- Reactions: 22
- Comments: 45 (30 by maintainers)
As a workaround, you can switch back to using real timers immediately before invoking
waitFor()e.g.
I wanted only
Dateto be faked actually (while allowing all promises and intervals) hence I had to list implicitly other things as well:waitForunder the hood usessetIntervalwhich is also faked when usingjest.useFakeTimers.I’m very surprised no one has mentioned to tell jest not to fake other things it fakes.
This is an exhaustive list of things jest fakes when using
jest.useFakeTimers. IMO it’s best to be very selective on what you let it fake, some things are not obviousCan someone try
jest.useFakeTimers({doNotFake: ['setInterval']});?Thanks @enzoferey!!! I was struggling with this problem migrating tests to vite for a few hours…
vi.useFakeTimers({ toFake: ['Date'], now: new Date(2022, 2, 20, 8, 0, 0) });Works like a charm!Very good pointer @joeythomaschaske 🎯
Indeed,
waitForusessetInterval, so if you usejest.useFakeTimers({ doNotFake: ["setInterval"] })as you suggested it works.For Vitest users, the API is a bit better as you can directly specify the only thing you want to fake. For example:
vi.useFakeTimers({ toFake: ["setTimeout"] }).My apologies @jcready for the waste of your time.
This library has been effectively deprecated for quite some time now.
While I understand upgrading to React 18 is no small feat, I do feel like it was a relevant thing for me to point out as many of my users would be looking to make that upgrade and along with it would likely need to migrate away from this library anyway. After all, React 18 has been out now for almost 2 years now.
All that being said, I’ll update the release notes to mention that it was not included to hopefully save someone else from wasting their time also.
Just had to update RTL, which resolved it for me.
The implementation in the alpha branch has some issues and I never found time to get to the bottom of it. It’s unlikely we’ll ever merge that into the main branch now.
For what it’s worth, the new RTL version already supports fake timers. I’m not sure about the new RNTL version though.
@chris110408 Yes, I think there is still work to be done here. Using fake timers stills fails in the scenario described here.
@bsk26 I would not expect that test to pass, regardless of the use of fake timers or not. The callback gets called synchronously once before using timeouts to poll for subsequent checks. With the current implementation, I’d expect the test to timeout with our error if not using fake timers, or jest’s timeout if you are. Can you please that you are
awaitingthe result ofwaitForotherwise the test will breeze past it and appear to pass because it isn’t actually waiting for the promise to resolve:I assume you mean other than us not supporting the use of fake timers (yet)?
There has been a bit of deviation in our implementations which is not ideal, but for the most part the core use case is consistent, that is, it will run the callback on an interval until it stops throwing an error. We implemented it to also work of returning a boolean (unsupported by
@testing-library/dom, where@testing-library/reactget’s it’s implementation from) to simplify a common use case we see:I suspect that this use case is less useful to
@testing-library/reactas the result of a loading state for them is a spinner in the DOM so they would usewaitForElementToBeRemovedto check for it, but we deal with much more raw data being returned from hooks so we cannot rely on something external changing.The other main difference is they now support returning a
Promisefrom their callback which will delay running another interval check until the promise resolves. I actually didn’t realise this was a thing until looking this now to answer you and I conceptually like it and think it would be a great feature for us to support as well.There are also some subtle differences in that they they run additional checks with a
MutationObserverwhereas we run them ifrenderis called. The difference is simply because hooks do not rely on DOM changes, but rather component changes and in many cases we don’t even have a DOM in the test environment so we could not rely on the same implementation.Fundamentally though, the implementation is different because our use cases and our dependencies are different. We try to offer the best utilities specifically for testing hook functions, rather than ensuring 100% API consistency with the libraries designed for testing DOM output.