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 different from source code globals? #2048

Closed
kentcdodds opened this issue Nov 3, 2016 · 21 comments
Closed

Jest globals different from source code globals? #2048

kentcdodds opened this issue Nov 3, 2016 · 21 comments

Comments

@kentcdodds
Copy link
Contributor

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

bug

What is the current behavior?

In some cases, when source code does stuff with built-in globals like Object, Array, etc. it appears that those built-in globals are not the same as the globals available in the test environment.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal repository on GitHub that we can npm install and npm test.

Still working on figuring out a reliable way to reproduce this in an isolated environment... Stand by...

What is the expected behavior?

The globals should be the same in both environments

Run Jest again with --debug and provide the full configuration it prints. Please mention your node and npm version and operating system.

forthcoming...

@cpojer
Copy link
Member

cpojer commented Nov 3, 2016

Can you try "jest@test" and tell me if this fixes your problem?

On Thu, Nov 3, 2016 at 9:01 PM +0000, "Kent C. Dodds" <[email protected]mailto:[email protected]> wrote:

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

bug

What is the current behavior?

In some cases, when source code does stuff with built-in globals like Object, Array, etc. it appears that those built-in globals are not the same as the globals available in the test environment.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal repository on GitHub that we can npm install and npm test.

Still working on figuring out a reliable way to reproduce this in an isolated environment... Stand by...

What is the expected behavior?

The globals should be the same in both environments

Run Jest again with --debug and provide the full configuration it prints. Please mention your node and npm version and operating system.

forthcoming...

You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHubhttps://github.com//issues/2048, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AAA0KFrQY9PqX0WQIDZNhofPno0mfgBWks5q6kuIgaJpZM4Ko6Wt.

@kentcdodds
Copy link
Contributor Author

Doesn't appear to. I'm still working on making a legit reproduction. I'm struggling to do so... Things are a little... complicated. And I'm not sure what's actually causing the issue.

@kentcdodds
Copy link
Contributor Author

kentcdodds commented Nov 3, 2016

Ok, here's the repo. Here's the source:

const Module = require('module')
const path = require('path')
module.exports = requireFromString('fake-thing.js', `
  module.exports = function isObject(obj) {
    return obj.constructor === Object
  }
`)

/*
 * copied and modified from require-from-string
 */
function requireFromString(filename, code) {
  const m = new Module(filename, module.parent)
  m.filename = filename
  m.paths = Module._nodeModulePaths(path.dirname(filename))
  m._compile(code, filename)
  return m.exports
}

I realize this is kinda weird. I'm actually not even sure whether this is specifically what's causing issues in my app, but my slice-js project does this to work some of its own magic for tests and I've definitely run into the problem (so far I've been able to work around it ok).

Any ideas? I'm worried you're going to say: "Yeah, just don't do that." Haha

@kentcdodds
Copy link
Contributor Author

kentcdodds commented Nov 3, 2016

So, here's a little more context. In my app, (one of) the specific line(s) that's having trouble is this one from confit:

obj && obj.constructor !== Object

The obj is coming from another module (via a function call). I'm thinking that perhaps wherever this obj is coming from is compiled with different globals from the confit module.

So, if I add:

console.log(new Error('confit').stack)

To the top of the confit module, I'll get this output:

  console.log node_modules/consumerweb-mocks/node_modules/confit/index.js:18
    Error: confit
        at Object.<anonymous> (/Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/consumerweb-mocks/node_modules/confit/index.js:18:13)
        at Runtime._execModule (/Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/jest-cli/node_modules/jest-runtime/build/index.js:441:13)
        at Runtime.requireModule (/Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/jest-cli/node_modules/jest-runtime/build/index.js:297:14)
        at Runtime.requireModuleOrMock (/Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/jest-cli/node_modules/jest-runtime/build/index.js:366:19)
        at Object.<anonymous> (/Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/consumerweb-mocks/node_modules/kraken-js/lib/config.js:22:14)
        at Runtime._execModule (/Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/jest-cli/node_modules/jest-runtime/build/index.js:441:13)
        at Runtime.requireModule (/Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/jest-cli/node_modules/jest-runtime/build/index.js:297:14)
        at Runtime.requireModuleOrMock (/Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/jest-cli/node_modules/jest-runtime/build/index.js:366:19)
        at Object.<anonymous> (/Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/consumerweb-mocks/node_modules/kraken-js/lib/settings.js:21:14)
        at Runtime._execModule (/Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/jest-cli/node_modules/jest-runtime/build/index.js:441:13)

So I'm thinking that confit is being compiled via Jest.

And if I move that line to the config function (still in confit), the output I get is:

  console.log node_modules/consumerweb-mocks/node_modules/confit/index.js:32
    Error: confit
        at config (/Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/consumerweb-mocks/node_modules/confit/index.js:32:15)
        at complete (/Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/consumerweb-mocks/node_modules/confit/index.js:235:36)
        at complete (/Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/consumerweb-mocks/node_modules/confit/index.js:189:17)
        at /Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/consumerweb-mocks/node_modules/async/lib/async.js:685:17
        at /Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/consumerweb-mocks/node_modules/confit/index.js:176:21
        at /Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/shortstop/lib/resolver.js:165:43
        at /Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/shortstop/node_modules/async/lib/async.js:533:17
        at /Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/shortstop/node_modules/async/lib/async.js:119:25
        at /Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/shortstop/node_modules/async/lib/async.js:24:16
        at /Users/kdodds/Developer/paypal/p2pnodeweb/node_modules/shortstop/node_modules/async/lib/async.js:530:21

But I'm thinking that's irrelevant... So I'm not sure who's actually passing the obj along. Will keep digging.

@kentcdodds
Copy link
Contributor Author

Note, if I change that line in confit to be:

obj && obj.constructor.toString().indexOf('Object') === -1

Then that fixes the problem (and I'm pretty sure it also preserves the behavior, even if it is a bit hacky).

@cpojer
Copy link
Member

cpojer commented Nov 8, 2016

If you are using the "module" module in Jest, it will get pulled in from the parent context with all the parent built-ins instead of the ones from the vm module. You can think of it like in a browser with an iframe. The "Array" constructor from the parent won't be the same as the one from the iframe, which is why you need to use Array.isArray instead of instanceof Array. It seems like the same thing is happening here.

Unfortunately I don't see a good way around this. I would recommend to do something like this:

const exports = eval(`const module = {}; ${code}; module.exports`);

is there a reason that wouldn't work for you and why you'd need to run it as a real module? Another workaround that will work just fine is to do this:

fs.writeFileSync('my-file.js', 'my-code', 'utf8');
require('./my-file.js');

Closing for now as I don't think we'll add a require-string API to Jest and there is other ways to solve it. Happy to continue the discussion.

@cpojer cpojer closed this as completed Nov 8, 2016
@kentcdodds
Copy link
Contributor Author

🎉 🎉 🎉 🎉 🎉 🎉

I figured out the problem (Finally!) It's in the json branch on the reproduction repo. The issue happens when "testEnvironment": "node". With that config, this code fails:

const obj = JSON.parse('{}')

test('should be an object', () => {
  expect(obj.constructor === Object).toBe(true)
})

However, running with just node, this code passes:

const assert = require('assert')
const obj = JSON.parse('{}')
assert(obj.constructor === Object)
console.info('✅ passed')

If you change testEnvironment to jsdom or leave it blank, then the first test will pass.

@cpojer
Copy link
Member

cpojer commented Nov 8, 2016

Ah this is interesting. So the problem is that the vm module in node doesn't seem to bring its own JSON implementation as I see it. So I have to pull it in from the parent context but then it creates objects with the built-ins from the parent context as well.

I think the best way to solve this is to build a recursive function that updates the prototype, like this:

const update = object => {
  if (Array.isArray(object)) {
    return object.map(update);
  } else if (Object.prototype.toString.call(object) === "[object Object]") {
    Object.setPrototypeOf(object, Object);
    Object.keys(object).forEach(key => update(object[key]));
  }
  return object;
}

If you can figure out where the JSON built-in is and how to create a new version of it inside of a context, please let me know. Another alternative is to load a json polyfill in your test and do global.JSON = require('json-polyfill') which will then be evaluated in the test context instead of the parent context.

@kentcdodds
Copy link
Contributor Author

So this works in my example repo, but it's kinda scary :-/

global.JSON = {} // remove all JSON methods so it can be overridden
require('JSON')

I put that in one of my setupFiles and things work alright. Not sure how it impacts performance, but yeah, that makes the errors go away so... :)

@kentcdodds
Copy link
Contributor Author

Also, thanks so much @cpojer :) Jest does quite a bit of magic under the hood which leads to odd things like this sometimes, but for the most part, I've never been happier with a testing framework, so thank you so much for all your work on Jest!

@suchipi
Copy link
Contributor

suchipi commented Nov 9, 2016

Does require('JSON') mutate the global? It'd be nice if there was a module that returned the JSON object itself so that the context could be set up with it.

@cpojer
Copy link
Member

cpojer commented Nov 9, 2016

I don't know what JSON is that @kentcdodds was referring to.

@kentcdodds
Copy link
Contributor Author

It's the only module that I'm aware of that implements the JSON API. It's actually a published version of JSON-js by Douglas Crockford (creator of JSON), so I think it's pretty good. But yes, it does muck with the global which is not as great. If you've got an alternative I'm happy to use that. Or even better, I'd love to find a better solution :)

@suchipi
Copy link
Contributor

suchipi commented Nov 9, 2016

Weird... JSON isn't listed in the node docs, and it behaves weirdly in the repl, too:

$ node
> JSON
{}
> global.JSON
{}
> Object.keys(JSON)
[]
> global.JSON.parse
[Function: parse]
> 

EDIT: Oh, nevermind, they're just not enumerable:

> Object.getOwnPropertyNames(JSON)
[ 'parse', 'stringify' ]

@thomashuston
Copy link
Contributor

I ran into this same issue when using node-fetch in some tests. Internally it has an instanceof Array check that unexpectedly fails in Jest. For anyone else who may run into this issue, I've opened a PR there switching to Array.isArray.

@suchipi
Copy link
Contributor

suchipi commented Jan 9, 2017

What if we added transformations for eg. instanceof Array to Array.isArray to babel-jest? For those who aren't aware that jest uses vm, this error could be really weird/annoying to debug. Unless that transformation is less intuitive/more dangerous

@cpojer
Copy link
Member

cpojer commented Jan 9, 2017

Did you upgrade to Jest 18? This should all work now.

@thomas-huston-zocdoc
Copy link

@cpojer I'm still seeing this in Jest 18.1 - reproduced here: https://github.com/thomas-huston-zocdoc/jest-fetch-array-bug

@cpojer
Copy link
Member

cpojer commented Jan 10, 2017

Would you mind opening a separate issue for that?

@thomashuston
Copy link
Contributor

You got it! #2549

@github-actions
Copy link

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 13, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants