dom-testing-library: findBy* no longer waiting when used with jest fake timers

  • @testing-library/dom version: 8.0.0
  • Testing Framework and version: @testing-library/react v12
  • DOM Environment: jest 27

Relevant code or config:

"jest": {
  "testEnvironment": "jsdom",
  "setupFilesAfterEnv": ["<rootDir>/jest-setup.js"]
}
// jest-setup.js
import '@testing-library/jest-dom'

What you did:

I was trying to use fake timers and findBy*

What happened:

findBy* doesn’t wait when using fake timers as it used to.

Reproduction:

Created a repo with two branches, one using v11 of RTL and another one for v12.

v11 branch works fine (dom v7.31.2) v12 branch is experiencing issues (dom v8)

https://github.com/deini/rtl-find/tree/v11 https://github.com/deini/rtl-find/tree/v12

Problem description:

When doing something like:

jest.useFakeTimers();

render(<Button />)

await screen.findByRole('dialog', {}, { timeout: 5000 })

I would expect it to wait for 5 seconds (unless I’m totally wrong 😅), however, seems like fake timers is now messing up with findBy* and it doesn’t wait anymore. This happens both in jest 26 and 27 with both legacy and modern fake timers.

Suggested solution:

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 17
  • Comments: 39 (12 by maintainers)

Most upvoted comments

Is there any working/recommended solution for this issue?

Let’s keep this open since msw is quite popular and I’d like to ensure compatiblity short-term. I just don’t know where to start so some response from their maintainers would be nice.

Yes, that tracks with my own experience. I have had to increase the timeout globally for all async utils. In src/setupTests.ts:

import { configure } from '@testing-library/dom';
configure({ asyncUtilTimeout: 5000 });

Have you tried running this with msw v.0.30 ? This includes a fix for Jest 27.

My workaround for waitForElementToBeRemoved is wrapping it like this:

import { waitForElementToBeRemoved as originalWaitForElementToBeRemoved } from "@testing-library/react";

export const waitForElementToBeRemoved = async (callback, options) => {
  jest.runOnlyPendingTimers();
  jest.useRealTimers();
  await originalWaitForElementToBeRemoved(callback, options);
  jest.useFakeTimers();
};

Then whenever you need waitForElementToBeRemoved just import your own waitForElementToBeRemoved function instead of importing from testing library.

I believe the same technique can be used for other async utils like waitFor and findBy*.

Hope this helps.

@rbrewington Thanks for the reply, I tried that already. But even after using jest.runOnlyPendingTimers(), it is not passing.

We are experiencing the same issue. jest fake timers are interfering with waitFor and findBy queries. Both of them make two attempts and the test fails irrespective of the timeout value. Some more data: We provided a 5 seconds timeout and both waitFor and findByXXXX made two retries only. The two retries are something consistent in our environment.

Any updates on this issue?

This was added a while ago so that fake timers in Jest don’t interfere with the request handling.

They shouldn’t do that. The point of fake timers is that you do want to inferfere with the time. If libraries just use a different clock, fake timers wouldn’t do anything.

It seems to me msw is using a different clock. I would expect that if I advance the clock by 200ms and flush the microtask queue any response with a delay <200ms would resolve. But that’s not what’s happening:


test("renders number of notifications", async () => {
  const fakeTimersEnabled = true;

  if (fakeTimersEnabled) {
    jest.useFakeTimers();
  }

  const server = setupServer(
    rest.get("/api/notifications", (_req, res, ctx) => {
      console.log("GET 1");
      return res(ctx.delay(100), ctx.json({ notifications: ["Foo"] }));
    })
  );
  server.listen();

  let data = null;
  fetch("/api/notifications")
    .then((response) => {
      console.log("response");
      return response.json();
    })
    .then((_data) => {
      data = _data;
    });

  let timeout = 500;
  const interval = 50;

  await new Promise(async (resolve, reject) => {
    let cancelled = false;
    const timeoutError = new Error("timeout");

    setTimeout(() => {
      cancelled = true;
      reject(timeoutError);
    }, timeout);
    setInterval(() => {
      console.log("ping");
      if (data !== null) {
        resolve();
      }
    }, interval);

    if (fakeTimersEnabled) {
      while (!cancelled) {
        jest.advanceTimersByTime(interval);
        await new Promise((resolve) => {
          setTimeout(resolve, 0);
          jest.advanceTimersByTime(0);
        });
      }
    }
  });

  expect(data).not.toEqual(null);
});

This sounds more like an msw issue to me. Or maybe we’re disagreeing on whether you should or shouldn’t mix clocks (I think you should the clock that’s currently available and not mix real/fake clocks).

Thanks again for the reply. The repo I linked in my previous comments is an isolation of the issue.

Couldn’t come up with something smaller to reproduce it.