@testing-library/dom version: 7.5.2We 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.
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
The fix is to find another way to see if the timers are being mocked or not, as the current heuristics don't work.
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:
npm package (@latest dist-tag)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:
npm package (@latest dist-tag)Your semantic-release bot :package::rocket:
Most helpful comment
I'd love to