Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jest globals differ from Node globals #2549

Open
thomashuston opened this issue Jan 10, 2017 · 70 comments · May be fixed by #5995 or #8220
Open

Jest globals differ from Node globals #2549

thomashuston opened this issue Jan 10, 2017 · 70 comments · May be fixed by #5995 or #8220

Comments

@thomashuston
Copy link
Contributor

@thomashuston thomashuston commented Jan 10, 2017

Do you want to request a feature or report a bug?
Bug

What is the current behavior?
After making a request with Node's http package, checking if one of the response headers is an instanceof Array fails because the Array class used inside http seems to differ from the one available in Jest's VM.

I specifically came across this when trying to use node-fetch in Jest to verify that cookies are set on particular HTTP responses. The set-cookie header hits this condition and fails to pass in Jest https://github.com/bitinn/node-fetch/blob/master/lib/headers.js#L38

This sounds like the same behavior reported in #2048; re-opening per our discussion there.

If the current behavior is a bug, please provide the steps to reproduce and either a repl.it demo through https://repl.it/languages/jest or a minimal repository on GitHub that we can yarn install and yarn test.
https://github.com/thomas-huston-zocdoc/jest-fetch-array-bug

What is the expected behavior?
The global Array class instance in Jest should match that of Node's packages so type checks behave as expected.

I've submitted a PR to node-fetch switching from instanceof Array to Array.isArray to address the immediate issue, but the Jest behavior still seems unexpected and it took quite a while to track down.

Please provide your exact Jest configuration and mention your Jest, node, yarn/npm version and operating system.
I am using the default Jest configuration (I have not changed any settings in my package.json).
Jest - 18.1.0
Node - 6.9.1 (also tested in 4.7.0 and saw the same error)
npm - 3.10.8
OS - Mac OS X 10.11.6

@suchipi
Copy link
Contributor

@suchipi suchipi commented Jan 10, 2017

This is likely due to the behavior of vm; see nodejs/node-v0.x-archive#1277

Does Jest do anything to try to avoid this right now?

@PlasmaPower
Copy link

@PlasmaPower PlasmaPower commented Mar 1, 2017

I came across the exact scenario. It's very hard to diagnose. I also came across it with Error objects. Writing wrapper workarounds for this is getting annoying.

From the linked nodejs issue:

Yes, Array.isArray() is the best way to test if something is an array.

However, Error.isError is not a function.

@suchipi
Copy link
Contributor

@suchipi suchipi commented Mar 1, 2017

Jest team- should the node (and maybe jsdom) environment(s) be changed to put things like Error, Array, etc from the running context into the vm context? I believe that would solve this issue.

Alternatively, maybe babel-jest could transform instanceof calls against global bindings such that they work across contexts.

@PlasmaPower
Copy link

@PlasmaPower PlasmaPower commented Mar 1, 2017

I don't like the babel-jest idea, if something like that is implemented it should be its own plugin. Other than that, I agree.

@cpojer
Copy link
Collaborator

@cpojer cpojer commented Mar 2, 2017

We can't pull in the data structures from the parent context because we want to sandbox every test. If you guys could enumerate the places where these foreign objects are coming from, we can wrap those places and emit the correct instances. For example, if setTimeout throws an error, then we can wrap that and re-throw with an Error from the vm context.

@suchipi
Copy link
Contributor

@suchipi suchipi commented Mar 2, 2017

Is there any risk to the sandboxing added other than "if someone messes with these objects directly, it will affect other tests"? Or is there something inherent in the way the contexts are set up that would make this dangerous passively? Just trying to understand. I'd guess that instanceof Error checks are more likely than Error.foo = "bar" type stuff.

@cpojer
Copy link
Collaborator

@cpojer cpojer commented Mar 2, 2017

It's one of the guarantees of Jest that two tests cannot conflict with each other, so we cannot change it. The question is where you are getting your Error and Arrays from that are causing trouble.

@PlasmaPower
Copy link

@PlasmaPower PlasmaPower commented Mar 2, 2017

They come from node native libraries like fs or http.

@cpojer
Copy link
Collaborator

@cpojer cpojer commented Mar 2, 2017

Ah, hmm, that's a good point. It works for primitives but not that well for errors or arrays :(

@suchipi
Copy link
Contributor

@suchipi suchipi commented Mar 2, 2017

What if jest transformed instanceof Array and instanceof Error specifically into something like instanceof jest.__parentContextArray and instanceof jest.__parentContextError?

@cpojer
Copy link
Collaborator

@cpojer cpojer commented Mar 2, 2017

meh, I'm not sure I love that :(

@suchipi
Copy link
Contributor

@suchipi suchipi commented Mar 2, 2017

We could override Symbol.hasInstance on the globals in the child context to also check their parent context if the first check fails... But Symbol.hasInstance only works in node 6.10.0+ or babel. Can't remember; does jest use babel everywhere by default?

@cpojer
Copy link
Collaborator

@cpojer cpojer commented Mar 2, 2017

I'm ok if this feature only works in newer versions of node. It seems much cleaner to me; assuming it doesn't have negative performance implications.

@suchipi
Copy link
Contributor

@suchipi suchipi commented Mar 2, 2017

Assuming performance seems fine, which globals should it be applied to? Error and Array... Buffer maybe, too?

@cpojer
Copy link
Collaborator

@cpojer cpojer commented Mar 2, 2017

Yeah, that sounds like a good start.

@suchipi
Copy link
Contributor

@suchipi suchipi commented Mar 2, 2017

I may be able to tackle a PR for this this weekend. I'm assuming we want it in both the node and jsdom environments?

@suchipi
Copy link
Contributor

@suchipi suchipi commented Mar 6, 2017

I've started work on this in https://github.com/suchipi/jest/tree/instanceof_overrides, but am having difficulty reproducing the original issue. @PlasmaPower or @thomashuston do you have a minimal repro I could test against?

@joedynamite
Copy link

@joedynamite joedynamite commented Mar 7, 2017

Not sure if it is 100% related or not but I have issues with exports not being considered Objects. For example the test in this gist will fail but if I run node index and log I get true: https://gist.github.com/joedynamite/b98494be21cd6d8ed0e328535c7df9d0

@PlasmaPower
Copy link

@PlasmaPower PlasmaPower commented Mar 7, 2017

@joedynamite sounds like the same issue

@PlasmaPower
Copy link

@PlasmaPower PlasmaPower commented Mar 7, 2017

Assuming performance seems fine, which globals should it be applied to? Error and Array... Buffer maybe, too?

Why not everything? I'm assuming performance won't be an issue as instanceof shouldn't be called often.

@jakeorr
Copy link

@jakeorr jakeorr commented May 18, 2017

I ran into a related issue with Express+Supertest+Jest. The 'set-cookie' header comes in with all cookies in a single string rather than a string for each cookie. Here is a reproduction case with the output I'm seeing with Jest and with Mocha (it works with mocha): #3547 (comment)

@rexxars
Copy link

@rexxars rexxars commented Jul 25, 2017

Just spent a couple of hours trying to figure out what happened when an app failed in weird ways because of an instanceof Error check.

Basically, http errors seem to not be instances of Error, which is very frustrating.

Very simple, reproducible test case here.

@kayahr
Copy link

@kayahr kayahr commented Jul 25, 2020

I'm using the SingleContextNodeEnvironment workaround in pretty much all of my projects now. Except the ones which use the electron runner which does not provide context isolation anyway and therefor is not affected by this annoying problem.

I have now centralized the workaround in a separate node module. Hopefully it's useful for others, too:

https://www.npmjs.com/package/jest-environment-node-single-context

Usage is very easy, see included README. But be aware that you no longer have context isolation when you use this environment so tests can have side effects on other tests by changing the global context.

@BartaG512
Copy link

@BartaG512 BartaG512 commented Oct 7, 2020

I'm using the SingleContextNodeEnvironment workaround in pretty much all of my projects now. Except the ones which use the electron runner which does not provide context isolation anyway and therefor is not affected by this annoying problem.

I have now centralized the workaround in a separate node module. Hopefully it's useful for others, too:

https://www.npmjs.com/package/jest-environment-node-single-context

Usage is very easy, see included README. But be aware that you no longer have context isolation when you use this environment so tests can have side effects on other tests by changing the global context.

Can we include it in the readme somehow?

@ibratoev
Copy link

@ibratoev ibratoev commented Feb 22, 2021

The option to remove any isolation and run Jest in a single context is not really an option as it breaks core Jest features. I have the same issue with Errors objects coming from the core API. The workaround I did was installing the Error type in the environment.

Edit: This workaround actually broke other scenarios - for example error thrown from the Javascript runtime like TypeError Cannot read property 'XXX' of undefined.

Edit: I went with the approach defining Symbol.hasInstance in a custom EnvironmentNode based Jest environment. The code for reference:

const EnvironmentNode = require('jest-environment-node');
const util = require('util');

function fixInstanceOfError(error) {
    const originalHasInstance = error[Symbol.hasInstance];
    Object.defineProperty(error, Symbol.hasInstance, {
        value(potentialInstance) {
            return this === error
                ? util.types.isNativeError(potentialInstance)
                : originalHasInstance.call(this, potentialInstance);
        },
    });
}

class CustomEnvironmentNode extends EnvironmentNode {
    constructor(config) {
        super(config);

        // Fix how `instanceof Error` works. Based on https://github.com/facebook/jest/pull/8220
        // This workarounds an issue where `Error`-s coming from the base API are from different context
        // hence code like `error instanceof Error` fails in Jest but works without Jest.
        // More info: https://github.com/facebook/jest/issues/2549
        // An actual fix requires changes in Node discussed here: https://github.com/nodejs/node/issues/31852
        fixInstanceOfError(this.global.Error);
    }
}

module.exports = CustomEnvironmentNode;

@ShikChen
Copy link

@ShikChen ShikChen commented Mar 14, 2021

This also breaks v8.serialize() / v8.deserialize(). A minimal example:

test("map", () => {
  const v8 = require('v8')
  const m1 = new Map();
  const m2 = v8.deserialize(v8.serialize(m1));
  expect(m1).toEqual(m2);
});

It will fail with a confusing message:

  ● map

    expect(received).toEqual(expected) // deep equality

    Expected: Map {}
    Received: serializes to the same string

The toEqual() failed because m1 and m2 have different constructor , so iterableEquality() returns false here:

if (a.constructor !== b.constructor) {
return false;
}

@benawhite
Copy link

@benawhite benawhite commented Jul 21, 2021

I have added a setup script with the setupFilesAfterEnv config that contains the following.
It auto detects the types that need fixed, and fixes them all.

Object.getOwnPropertyNames(globalThis)
    // find types that need fixed
    .filter((name) => {
        const // pad
            code = name.charCodeAt(0),
            prop = globalThis[name];
        // type means name starts with capital A-Z and has a typeof function
        return code >= 65 && code <= 90 && typeof prop === 'function';
    })
    // fix each type
    .forEach((name) => {
        // override the instanceOf handler for each type
        const stringTypeName = `[object ${name}]`;
        Object.defineProperty(globalThis[name], Symbol.hasInstance, {
            value(target) {
                return Object.prototype.toString.call(target) == stringTypeName;
            },
            writable: true
        });
    });

@jacksteamdev
Copy link

@jacksteamdev jacksteamdev commented Aug 23, 2021

YMMV with the above snippet. For me, it fixed my initial problems, but broke Buffer.isBuffer. 😞

@cakoose
Copy link

@cakoose cakoose commented Sep 2, 2021

I'd like to figure out how to work around this, but I don't have a comprehensive understanding of what's going on. Is there a writeup somewhere of what kinds of instanceof checks will and won't work?

For example (assuming I'm running in Node and my program itself isn't doing anything weird with VMs or contexts):

  • I'm using the big.js library. Since that's not defined in a core Node library, will instanceof Big work reliably under Jest?
  • Node has a util.types.isNativeError. If I replace the usual e instanceof Error with e instanceof Error || util.types.isNativeError(e), will that work reliably under Jest?

@cmcnicholas
Copy link

@cmcnicholas cmcnicholas commented Sep 2, 2021

I'd like to figure out how to work around this, but I don't have a comprehensive understanding of what's going on. Is there a writeup somewhere of what kinds of instanceof checks will and won't work?

For example (assuming I'm running in Node and my program itself isn't doing anything weird with VMs or contexts):

  • I'm using the big.js library. Since that's not defined in a core Node library, will instanceof Big work reliably under Jest?
  • Node has a util.types.isNativeError. If I replace the usual e instanceof Error with e instanceof Error || util.types.isNativeError(e), will that work reliably under Jest?

We simply used the jest config moduleNameMapper to map all imports a module to the same node_modules location e.g.

{
  moduleNameMapper: {
    '^@myorg/mymodule/(.*)$': path.resolve(__dirname, '../project-under-test/node_modules/@myorg/mymodule/$1'),
  }

with regards to will it work, using big.js directly in the test suite will work, having code in another project/module which imports big.js and then also importing big.js into your test project and passing them between functions will result in errors when performing instanceof checks.

I say this coming from a typescript project where we use project references, we have been on a journey to remove instanceof as in some cases it doesn't play well with cypress automation too.

@cakoose
Copy link

@cakoose cakoose commented Sep 2, 2021

When I run a test file with Jest, what contexts are created and what code runs in each context?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.