Dom-testing-library: Jest has changed its timer implementation causing waitFor() not to work properly

Created on 8 Jun 2020  路  12Comments  路  Source: testing-library/dom-testing-library

  • @testing-library/dom version: 7.5.2
  • Testing Framework and version: 10.0.4
  • DOM Environment: Jest
    Jest version: 25.1.0
    Node v12

We had some code which uses fakeTimers, I noticed it was screwing up waitFor().
So i needed to run jest.useRealTimers() just before calling waitFor() to fix the issue.

This shouldn't be needed, as testing-library tries to safe-guard against fakeTimer use, you can see here:
https://github.com/testing-library/dom-testing-library/blob/master/src/helpers.js#L5-L8

However, i noticed this line will always return undefined regardless of whether you have fakeTimers on or not. This is due to the fact that globalObj.setTimeout._isMockFunction is always undefined.
My guess is Jest used to mock setTimeout using its own utilities and this property once existed, but now Jest uses @Sinon/MockTimers where this property is not added.

When did this happen?

Looks like the change happened in Jest v26
https://github.com/facebook/jest/releases/tag/v26.0.0

there were also implementation changes in v25.1
https://github.com/facebook/jest/releases?after=v25.5.1

_isMockFunction is added here:
https://github.com/facebook/jest/blob/master/packages/jest-fake-timers/src/legacyFakeTimers.ts#L370

It was swapped out in v25, plan here:
https://github.com/facebook/jest/pull/7776#issuecomment-513552169

Solution

The fix is to find another way to see if the timers are being mocked or not, as the current heuristics don't work.

bug help wanted released

Most helpful comment

I'd love to

All 12 comments

Awesome write-up. I also encountered some strange behavior when using Jest fakeTimers and trying to add a typing delay to userEvents.type, from testing-library/user-event.

Thank you for the detail! We could definitely fix this. I invite anyone to help figure out how to solve this. 馃憤

:tada: This issue has been resolved in version 7.15.1 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

@kentcdodds I think this condition (below) will be true for someone using @sinonjs/fake-timers instead of jest.useFakeTimers because the timer functions will have a clock attribute in either case (if useFakeTimers was called with 'modern'):

  const usingJestFakeTimers =
    globalObj.setTimeout &&
    (globalObj.setTimeout._isMockFunction ||
      typeof globalObj.setTimeout.clock !== 'undefined') &&
    typeof jest !== 'undefined'

This may be problematic because @sinonjs/fake-timers users are likely to restore the timer functions by uninstalling their clock instance (rather than using jest.useRealTimers).

Since legacy is the default for jest.useFakeTimers in Jest 26, the timer functions are not likely to be restored for these users and they'll probably be deleted by this code when their clock is uninstalled because the functions will not have a hadOwnProperty attribute.

I noticed this when one of my tests began failing in CI after 7.15.1 was released. My component was being unmounted by RTL's cleanup and clearInterval wasn't defined any longer.

    ReferenceError: clearInterval is not defined

      150 |              // clean up useEffect
      151 |             return () => {
    > 152 |                     clearInterval(intervalId.current);
          |                     ^
      153 |             };
      154 |     }, [activityInterval, idlePeriod, timeoutCallback, manageWarningCallback, warningPeriod]);
      155 | };

(Sorry, I don't have a publicly available repro yet.)

Do you have a suggestion for a solution?

Still experimenting, but I wonder if something like this could work:

  const usingJestFakeTimers =
    globalObj.setTimeout &&
    (globalObj.setTimeout._isMockFunction ||
      typeof globalObj.setTimeout.clock !== 'undefined') &&
    typeof jest !== 'undefined'

  const variant = usingJestFakeTimers && globalObj.setTimeout._isMockFunction ? 'legacy' : 'modern';

  // ...when we restore the fakes
  if (usingJestFakeTimers) {
    jest.useFakeTimers(variant);
  }

This way, we're being explicit. Jest 26's default is 'legacy', but the release notes for 27 indicate a change to 'modern'. I'll give this a shot today and report back.

That seems reasonable to me 馃憤

Okay, I tried my approach and it doesn't work because of potential differences in the configuration of the clock created by Jest and one created by a user of @sinonjs/fake-timers.

We need a way to differentiate between timers being faked by Jest's modern fake timers and sinonjs (and possibly other libraries that attach a clock to the faked timer functions).

We can use globalObj.setTimeout.clock and jest.getRealSystemTime (docs) to make that determination:

function runWithRealTimers(callback) {
  const usingJestAndTimers =
    typeof jest !== "undefined" && typeof globalObj.setTimeout !== "undefined";
  const usingLegacyJestFakeTimers =
    usingJestAndTimers &&
    typeof globalObj.setTimeout._isMockFunction !== "undefined";

  let usingModernJestFakeTimers = false;
  if (
    usingJestAndTimers &&
    typeof globalObj.setTimeout.clock !== "undefined" &&
    typeof jest.getRealSystemTime !== "undefined"
  ) {
    try {
      // jest.getRealSystemTime throws when not using modern timers
      jest.getRealSystemTime();
      usingModernJestFakeTimers = true;
    } catch {
      // not using Jest's modern fake timers
    }
  }

  const usingJestFakeTimers =
    usingLegacyJestFakeTimers || usingModernJestFakeTimers;

  if (usingJestFakeTimers) {
    jest.useRealTimers();
  }

  const callbackReturnValue = callback();

  if (usingJestFakeTimers) {
    jest.useFakeTimers(usingModernJestFakeTimers ? "modern" : "legacy");
  }

  return callbackReturnValue;
}

A bit hacky, but should limit the functionality to Jest's fake timers and restore them with the correct variant ('modern' or 'legacy').

Hacky is the thing we do in libraries so people's code works the way it should 馃槄

This looks fine to me. Thanks for your research! Would you like to contribute a pull request for this?

I'd love to

Submitted #652

:tada: This issue has been resolved in version 7.16.2 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rbrtsmith picture rbrtsmith  路  4Comments

PaulInglis picture PaulInglis  路  3Comments

JeffBaumgardt picture JeffBaumgardt  路  4Comments

ngbrown picture ngbrown  路  4Comments

nicolasschabram picture nicolasschabram  路  3Comments