I noticed some strange behavior when attempting to run tests with both RN and rxjs imported. It appears that when you import react-native into a jest test, global.requestAnimationFrame is defined but global.cancelAnimationFrame is not.
I tracked the issue down to the interaction between Libraries/Core/Timers/JSTimers and Libraries/Core/InitializeCore,
which, in turns calls defineLazyObjectProperty for the timers in JSTimers. Somehow, when running a jest test, the lazy definition is causing requestAnimationFrame to be defined when cancelAnimationFrame is not.
I determined that the lazy evaluation was the root cause by stripping out the react-native dependency in my test and inlining only the relevant portions. You can see that here.
It seems that whichever lazily defined property is accessed first is defined, but the others are not. For instance:
// succeeds
it('has a defined requestAnimationFrame', () => {
if (global.requestAnimationFrame) global.requestAnimationFrame();
expect(global.requestAnimationFrame).toBeDefined(); // passes
});
// fails
it('has a defined cancelAnimationFrame', () => {
if (global.cancelAnimationFrame) global.cancelAnimationFrame();
expect(global.cancelAnimationFrame).toBeDefined(); // fails
});
The requestAnimationFrame test succeeds. But if you swap them, only the cancelAnimationFrame test succeeds.
I've got more detail in this comment and the one after it in an issue I raised in the jest repo. Basically, the (edit: this is no longer the case, in getValue in defineLazyObjectProperty is calling require('JSTimers') in initializeCore, which is, in turn, calling getValue for the same property. However, this is not the case when you import react-native while running the app.master defineLazyObjectProperty has sets valueSet in getValue, which prevents the "recursion" (it wasn't actually), though the require('JSTimers') still seems to be behaving oddly.)
Not sure, but it seems like the issue might be related to the issue described in these comments:
function getValue(): T {
// WORKAROUND: A weird infinite loop occurs where calling `getValue` calls
// `setValue` which calls `Object.defineProperty` which somehow triggers
// `getValue` again. Adding `valueSet` breaks this loop.
if (!valueSet) {
// Calling `get()` here can trigger an infinite loop if it fails to
// remove the getter on the property, which can happen when executing
// JS in a V8 context. `valueSet = true` will break this loop, and
// sets the value of the property to undefined, until the code in `get()`
// finishes, at which point the property is set to the correct value.
valueSet = true;
setValue(get());
}
return value;
I wasn't able to repro on RNPlay, but you can see a minimal example here. Note that it has no RN dependency - it's not using the jest or babel preset and it's not importing it. It's just got the relevant portions inlined into the test file.
If you'd like to see an example that actually imports react-native, you can check out the master branch of that repo.
In either case, you should be able to see the issue by doing a yarn install and then a yarn test. The second test should fail.
I have a workaround posted at the end of this comment. @lfabreges workaround here seems to be better, I'd recommend you try that first.
Not sure how to actually fix this, but I plan to keep digging.
0.41.0-rc.0 (and at least 0.40.0 and 0.38.0)running into the same issue on 0.41.2
Running into the same issue on 0.41.2, the workaround doesn't work when I jest.mock() a module requiring "rxjs"
Both tests succeed if you do not require the timer inside defineLazyTimer :
const myJSTimers = require('../MyJSTimers');
const defineLazyTimer = name => {
defineLazyObjectProperty(global, name, {
get: () => myJSTimers[name],
writable: true,
enumerable: true,
});
};
@lfabreges Sorry to hear the workaround's not helping there. I'd seen that moving the require outside of defineLazyTimer fixes the tests, but doesn't it also eager load the timers instead of lazy loading them?
If the solution is to not lazy load these things, that would seem to solve the problem (and make the code a little simpler), but I think it would require more of a refactor so that the code isn't implying that it's lazy loading something when it isn't. I'm also not sure why these things are being lazy loaded, perhaps one of the core contributors could shed some light there?
You're right !
I'm still investigating, but my guess is that both tests are running in parallel. I manage to make it work by modifying the jest-runtime, in the requireModule function, by putting moduleRegistry[modulePath] = localModule; at the end of the block. But it is not a viable solution since the script is run two times and doesn't take advantage of the moduleRegistry.
Bellow the steps :
If import "rxjs" is not enough when jest.mocking, here's a workaround, add these lines in a Jest setupFile:
const JSTimers = require("react-native/Libraries/Core/Timers/JSTimers");
global.cancelAnimationFrame = JSTimers.cancelAnimationFrame;
global.requestAnimationFrame = JSTimers.requestAnimationFrame;
Hi there! This issue is being closed because it has been inactive for a while. Maybe the issue has been fixed in a recent release, or perhaps it is not affecting a lot of people. Either way, we're automatically closing issues after a period of inactivity. Please do not take it personally!
If you think this issue should definitely remain open, please let us know. The following information is helpful when it comes to determining if the issue should be re-opened:
If you would like to work on a patch to fix the issue, contributions are very welcome! Read through the contribution guide, and feel free to hop into #react-native if you need help planning your contribution.
Most helpful comment
If
import "rxjs"is not enough whenjest.mocking, here's a workaround, add these lines in a Jest setupFile: