Home > Enterprise >  How to use user-event library in conjunction with Jest's fake timers?
How to use user-event library in conjunction with Jest's fake timers?

Time:11-02

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, whereas user-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:

  1. a mouse movement1
  2. the actual click

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.

CodePudding user response:

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')!)

CodePudding user response:

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

  • Related