Skip to content

Testing-library user event with fake timers

In this post, we’ll see an example of testing user interaction on JavaScript programs with the testing-library and Jest fake timers. For a more detailed introduction of Jest and some testing tips, you can see my previous post.

Testing user interaction with the testing-library

User interactions, like having the user click on a button, are complex events that are hard to replicate in the testing environment. While the fireEvent API, can be used to issue DOM events, it’s NOT the recommended method for testing user interaction as it doesn’t reflect how the user really interacts with the DOM. The right approach is to use the userEvent API, which replicates user interaction with more fidelity.

Example component with delayed effects

For this simple demo, we’ll work with the following component.

import React, { useState } from "react";

const Demo = () => {
  const [isDisplayed, setIsDisplayed] = useState(true);

  const toggleHandler = () => setTimeout(()=>setIsDisplayed(prev => !prev), 500)
  return (
    <React.Fragment>
    <div>
      {isDisplayed && <p>Hello World!</p>}
    </div>
    <button onClick={toggleHandler}>toggle</button>
    </React.Fragment>
  );
}

export default Demo;

It consists of a simple text that is hidden or displayed after pressing the “toggle” button. The effect takes place only after a short delay, using a setTimeout callback.

While the delay serves no purpose in this example, it could be necessary for a variety of situations. For example, pressing the button could trigger a fade animation before completely removing the text. Showing the text again could be done with an animation as well, like on this snackbar example. For simplicity, we will not add any of those effects.

Next, let’s test this component.

Testing user interaction

We would like to verify the text disappears after first pressing the button.

import React from "react";

import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";

import Demo from "./Demo";

test("Pressing the button hides the text", async () => {
  const user = userEvent.setup();
  render(<Demo />);
  const button = screen.getByRole("button");
  await user.click(button);
  const text = screen.getByText("Hello World!");
  await waitFor(() => expect(text).not.toBeInTheDocument());
});

The setup method of userEvent is part of user-event@14.0.0-beta, which is the recommended approach at the moment of this writing.

Launching this test will succeed.

 PASS  src/Demo.test.jsx (6.377 s)
  ✓ Pressing the button hides the text (624 ms)

Test Suites: 1 passed, 1 total

However, this test takes more than half a second (624 ms) to complete. Here comes the need for fake timers.

Using fake timers to speed up user interaction tests

We’ll slightly modify our test to use Jest fake timers. They will allow us to manipulate the setTimeout callbacks to be run immediately after pressing the button.

test("Pressing the button hides the text (fake timers)", async () => {
  const user = userEvent.setup();
  jest.useFakeTimers();
  render(<Demo />);
  const button = screen.getByRole("button");
  await user.click(button);
  act(() => {
    jest.runAllTimers();
  });
  const text = screen.queryByText("Hello World!");
  expect(text).not.toBeInTheDocument();
  jest.useRealTimers();
});

See that we changed getByText to queryByText. getBy... query methods fail when there is no matching element. queryBy... methods don’t throw an error when no element is found.

Additionally, we add instructions to active and de-active the fake timers,jest.useFakeTimers and jest.useRealTimers, respectively. jest.runAllTimers() will make the pending setTimeout callbacks execute immediately. This way, we won’t have to wait for the setTimeout delay to complete during testing.

Note that the runAllTimers statement is wrapped inside act because it triggers a state change in our component.

Unfortunately, this test will fail.

 FAIL  src/Demo.test.jsx (10.984 s)
  ✕ Pressing the button hides the text (fake timers) (5010 ms)
  
  ● Pressing the button hides the text (fake timers)

    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."

      ...

Test Suites: 1 failed, 1 total

For some reason, using Jest fake timers doesn’t allow the user-event methods to complete. The test fails due to timeout (which is set to a maximum of 5 seconds by default).

Let’s see how to solve this issue.

Solving timeout errors on user event tests with fake timers

The reason our previous test failed has to do with @testing-library/user-event current implementation. By default, this library waits for a setTimeout delay during its execution. Since jest.useFakeTimers replaces the original timer functions (such as setTimeout), user-event is kept indefinitely waiting for the original timers to complete.

Fortunately, the solution is quite simple. We just need to set the delay option to null so that user-event does not wait on setTimeout.

const user = userEvent.setup({ delay: null });

Besides this single change, our test remains unchanged.

test("Pressing the button hides the text (fake timers)", async () => {
  const user = userEvent.setup({ delay: null });
  jest.useFakeTimers();
  render(<Demo />);
  const button = screen.getByRole("button");
  await user.click(button);
  act(() => {
    jest.runAllTimers();
  });
  const text = screen.queryByText("Hello World!");
  expect(text).not.toBeInTheDocument();
  jest.useRealTimers();
});

Running the test again will pass with no errors.

 PASS  src/Demo.test.jsx (5.032 s)
  ✓ Pressing the button hides the text (fake timers) (118 ms)

Test Suites: 1 passed, 1 total

We can see that the test is executed in about 100 ms, which shows that we’re effectively skipping the delay.

Published inProgramming
Subscribe
Notify of
guest
6 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Denis
Denis
7 months ago

Thank you very much!

Zenek
Zenek
4 months ago

I lost all hope with that. 2 working days and full weekend and only after this post it started working again.

Ever
1 month ago

It really helped me, thank you!! 🙂

Matt Stone
Matt Stone
1 month ago

Thanks, this was very helpful and put me on the right track.

As per https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841 a cleaner solution (preserving delay) might be:

userEvent.setup({
  advanceTimers: jest.advanceTimersByTime
})
Last edited 1 month ago by Matt Stone