Reputation: 9137
I'm writing Jest tests for a React component, and using the @testing-library/user-event
library to simulate user interaction. This works great, except in tests that use Jest's fake timers.
Here's a sample test:
it(`fires onClick prop function when the button is clicked`, async () => {
// jest.useFakeTimers()
let propFn = jest.fn()
let app = RTL.render(
<SampleComp onClick={propFn} />
)
await userEvent.click(app.queryByText('Test button')!)
expect(propFn).toHaveBeenCalled()
// jest.useRealTimers()
})
And here's the component:
function SampleComp({ onClick }) {
// a simple bridge for debugging
function _onClick(e) {
console.log(`_onClick invoked`)
return onClick(e)
}
return (
<button onClick={_onClick}>
Test button
</button>
)
}
Without fake timers, the test runs in a fraction of a second and passes. With the fake timers, the test times out and then fails:
● fires onClick prop function when the button is clicked
thrown: "Exceeded timeout of 5000 ms for a test.
Use jest.setTimeout(newTimeout) to increase the timeout value, if this is a long-running test."
It also fails to emit the debug line, so evidently the click event is never fired.
I've done a whole bunch of debugging, and have determined that the root cause is that the user-event library relies on a setTimeout
internally to create a brief delay between events.
Yes, "events" plural -- recall that the value-proposition of the user-event library is:
fireEvent
dispatches DOM events, whereasuser-event
simulates full interactions, which may fire multiple events and do additional checks along the way.
-- user-event docs
In this case, user-event creates two events for me:
1Although user-event's internal event object for the first event has only a target
and no type
, it makes sense to me that the first event would be either mouse-move or mouse-over on the button. Regardless: I've verified that the list of actions user-event creates internally does have 2 items, and the second one is clearly marked as "[MouseLeft]".
The delay is inserted between events by user-event's pointerAction
function. In my case, user-event stalls out during the delay between events 1 and 2.
The timeout is created in user-event's wait
module:
new Promise<void>(resolve => globalThis.setTimeout(() => resolve(), delay))
I've verified that this is the problem by monkeypatching that line inside my node_modules to simply resolve immediately without a timer, like so:
new Promise<void>(resolve => resolve())
Unfortunately, my application code has some important timers, and it won't do to make the test suite wait for them in real time. And even if I could, I suspect folks who find this question later might not have that freedom.
It does not work to run Jest's fake timers to permit user-event to proceed, like so:
it(`fires onClick prop function when the button is clicked`, async () => {
jest.useFakeTimers()
let propFn = jest.fn()
let app = RTL.render(
<SampleComp onClick={propFn} />
)
let userAction = userEvent.click(app.queryByText('Test button')!)
jest.runOnlyPendingTimers() // no good
jest.runAllTimers() // also no good
await userAction
expect(propFn).toHaveBeenCalled()
jest.useRealTimers()
})
So, how is one supposed to use Jest's fake timers in conjunction with the user-event library?
In this case, versions do seem to matter: this test worked great with earlier versions of React, Jest, and user-event. Here are the versions I'm using now (where this breaks), as well as the versions I was using (where this works):
Library | Current (breaks) |
Previous (worked) |
---|---|---|
jest | v27.5.1 | v26.6.3 |
react | v18.1.0 | v16.13.1 |
@testing-library/user-event | v14.3.0 | v12.8.3 |
I should note that it's not possible to change the package versions being used. I need a solution that works with the specific versions listed in the "Current" column.
Note: I think this problem is not specific to React, and folks using Vue or Svelte or anything else would have the same problem as long as they're using Jest's fake timers + user-event (which are both framework agnostic). So, I'm not tagging it as React.
Upvotes: 14
Views: 4754
Reputation: 3811
Update:
In user-event
v14.1 and later you can use the advanceTimers option to advance the fake timers:
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime })
or, more explicitly:
const user = userEvent.setup({
advanceTimers: (delay) => {
jest.advanceTimersByTime(delay)
}
})
user-event
calls the advanceTimers
function when simulating the delay between input events. By calling jest.advanceTimersByTime
, we advance the fake timers and the test doesn't time out.
Previous answer: (no longer recommended)
You can disable the delay using the user-event
setup function:
const user = userEvent.setup({ delay: null })
Make sure to then use the instance returned by setup()
:
await user.click(app.queryByText('Test button')!)
Upvotes: 10
Reputation: 968
Did you try this:
const ue = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
In my case it didn't work if it was not set up. More here Mocking setTimeout with Jest
Upvotes: 7