Popper-core: How to use Popper.js in Jest / JSDOM?

Created on 1 Nov 2017  ·  40Comments  ·  Source: popperjs/popper-core

CodePen demo

https://codepen.io/pen?template=wGqJEz

Steps to reproduce the problem

  1. Run UI tests with jsdom
  2. Get exception TypeError: document.createRange is not a function

What is the expected behavior?

Not to get an exception

What went wrong?

Obviously this is not popper's fault, but maybe it can be fixed here.

Any other comments?

Thanks for the awesome library, I hope this doesn't come across as disrespectful. The jsdom issue gives the feeling that this won't implemented anytime soon. I would like to know more about why createRange is used and can we replace it with something else or fall back to another solution if the browser doesn't support it.

Browsers newer than IE8 already support this so I can't find another use case other than testing with jsdom.

# FAQ 📖 # QUESTION ❔

Most helpful comment

I was having the same issue with enzyme mount() and mocking did not work. However, adding this to my setupTests.js file made everything work.

``` javascript

// setup file
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

if (global.document) {
document.createRange = () => ({
setStart: () => {},
setEnd: () => {},
commonAncestorContainer: {
nodeName: 'BODY',
ownerDocument: document,
},
});
}
```

All 40 comments

I decided to use createRange because it's a convenient way to know the order of two given DOM elements in the DOM.

Previously I used a very complex and unreliable custom code that didn't perform so well.

I may suggest you to simply polyfill createRange since anyway Popper.js is not going to actually work inside JSDOM.

This turned out to be trickier than I imagined. I polyfilled document.createRange but now I get NaN for some computed styles, the reason is jsdom returns 0 for every property of the bounding client rect.

I imagine this will never be the case in a real browser, but if there's a possibility if it can be, then guarding against NaN (or more generally non-finite values) could be an improvement.

Note: There are some polyfills out there for createRange but they all depend on Range or TextRange. I tried to fall back to the document.documentElement case.

Yes as I said, Popper.js is probably never going to work in JSDOM, it relies on real browser DOM APIs to work.

Of course, I don't expect it to work in a simulated environment. I just want it to not cause errors in snapshot tests. jsdom is a big and old project with high barrier of entry for me, but I'm willing to work on Popper.js to fix this issue. 😺

Wouldn't make sense to simply mock Popper.js?

@frontsideair I am dealing with the exact same thing. If you come up with a solution, please share, and I'll do the same.

Since on JSDom the only thing that Popper.js will do is to add some wrong inline style to the popper element, why not mock it away?

jest.mock('popper.js', () => class {
  constructor() { return {
    scheduleUpdate: jest.fn(),
    update: jest.fn(),
  };
});

or something like that?

You may use the TypeScript definitions to mimic the same API:
https://github.com/FezVrasta/popper.js/blob/master/packages/popper/index.d.ts#L113-L127

Thanks @FezVrasta The following works for me.

jest.mock(
  'popper.js',
  () =>
    class Popper {
      static placements = [
        'auto',
        'auto-end',
        'auto-start',
        'bottom',
        'bottom-end',
        'bottom-start',
        'left',
        'left-end',
        'left-start',
        'right',
        'right-end',
        'right-start',
        'top',
        'top-end',
        'top-start'
      ];

      constructor() {
        return {
          destroy: () => {},
          scheduleUpdate: () => {}
        };
      }
    }
);

You can also use static placements = Popper.placements I think, or anyway there's a way to access the original module so that you don't have to copy the placements.

Yeah, I am experimenting with that now, but currently Jest is complaining about it.

babel-plugin-jest-hoist: The module factory of jest.mock() is not allowed to reference any out-of-scope variables.
Invalid variable access: PopperJS

I remember there was a syntax that looked like:

jest.mock(originalImplementation =>
    class Popper {
      static placements = originalImplementation.placements;

      constructor() {
        return {
          destroy: () => {},
          scheduleUpdate: () => {}
        };
      }
    }
);

but I can't find it now

Boom 💥

jest.mock('popper.js', () => {
  const PopperJS = jest.requireActual('popper.js');

  return class {
    static placements = PopperJS.placements;

    constructor() {
      return {
        destroy: () => {},
        scheduleUpdate: () => {}
      };
    }
  };
});

That's it! Thanks :-)

Just to close the loop on this...

I also discovered that following works and may be more desirable for some folks.

Create a folder/file called __mocks__/popper.js.js adjacent to your node_modules directory containing the following code. Jest will pick it up automatically.

import PopperJs from 'popper.js';

export default class Popper {
  static placements = PopperJs.placements;

  constructor() {
    return {
      destroy: () => {},
      scheduleUpdate: () => {}
    };
  }
}

@brentertz would you mind to send a PR to add your last mock somewhere in the docs?

but now I get NaN for some computed styles

@frontsideair It might explain why I have got some flaky red build on Material-UI.

Warning: NaN is an invalid value for the left css style property.

For instance this one. I'm using mocha, jest mocking isn't an option for me on this project.
I have been adding some polyfill to make popper.js testing possible. It sounds like I'm missing something. e.g.:

  global.document.createRange = () => ({
    setStart: () => {},
    setEnd: () => {},
    commonAncestorContainer: {
      nodeName: 'BODY',
    },
  });

@oliviertassinari I ran into the same things as you. Same polyfill, same NaN, etc. Even though you are not using Jest, you do have Sinon, which could be used to stub Popper in a similar fashion.

A quick modification to avoid having to support ES7 static properties in linters/test-runners:

import PopperJs from 'popper.js';

export default class Popper {
  // static placements = PopperJs.placements;
  constructor() {
    this.placements = PopperJs.placements;

    return {
      destroy: () => {},
      scheduleUpdate: () => {}
    };
  }
}

This appears to work for the Popper.js stuff I need to mock in Jest tests.

I used this from one of the comments above in node_modules/__mocks__/popper.js.js but it didn't work:

jest.mock('popper.js', () => {
    const PopperJS = jest.requireActual('popper.js');

    return class {
        static placements = PopperJS.placements;

        constructor() {
            return {
                destroy: () => { },
                scheduleUpdate: () => { }
            };
        }
    };
});

Moving it to jest's setup file worked, though

@github-account-because-they-want-it The aforementioned comment actually said to, "Create a folder/file called __mocks__/popper.js.js adjacent to your node_modules directory", rather than as a child of node_modules as your comment suggests.

Jest is really flexible and provides numerous ways to load mocks, one of which you have demonstrated above. More can be found here. https://facebook.github.io/jest/docs/en/manual-mocks.html

@FezVrasta I tried both __mocks__ and setup file but jest seems to pick up the dist file which contains the popper.js already. Any suggestions? I think popper.js should be removed from that file.

    unhandledRejection TypeError: document.createRange is not a function
        at findCommonOffsetParent (/Users/niko/Code/example/components/node_modules/react-popper/dist/react-popper.js:765:24)
        at getReferenceOffsets (/Users/niko/Code/example/components/node_modules/react-popper/dist/react-popper.js:1227:28)
        at Popper.update (/Users/niko/Code/example/components/node_modules/react-popper/dist/react-popper.js:1401:28)
        at Popper.update$$1 (/Users/niko/Code/example/components/node_modules/react-popper/dist/react-popper.js:2905:21)
        at /Users/niko/Code/example/components/node_modules/react-popper/dist/react-popper.js:584:7
        at <anonymous>

Are you using the latest react-popper? Older versions embedded their own Popper.js version

Oh fuck, nevermind, it's still like this.. wtf

Hmm, I just realized I'm reporting this in the wrong repo, right?

Yup, anyway I'm on it.

Trying to update popper to latest version... Mocking it using the above techniques is no longer functional. Any ideas?

I haven't changed anything in the overall structure... Are you sure it is related to the new version?

@FezVrasta he is having the same issue I have. I didn't check it thoroughly but I think you need to re-publish the react-popper package with the proper dist file which doesn't include popper.

react-popper@next doesn't include Popper.js, try that. But I don't think that's the problem, Jest shouldn't care.

@FezVrasta I just tried the next version (0.9.0) and the error seems to be gone. But the TS declaration file (react-popper.d.ts) seems to be gone from the dist.

I tried with [email protected] and the mock once again works as expected. Is the "next" version safe/stable enough to use?

The build process has been completely rewritten, there are few bugs, but if it works in your own build process I'd say it's safe to use. Just note that TypeScript annotations are missing for now.

I don't know why the problem surfaces again, I'm using [email protected] which refers to [email protected], and the __mocks__ used to be working

I was having the same issue with enzyme mount() and mocking did not work. However, adding this to my setupTests.js file made everything work.

``` javascript

// setup file
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

if (global.document) {
document.createRange = () => ({
setStart: () => {},
setEnd: () => {},
commonAncestorContainer: {
nodeName: 'BODY',
ownerDocument: document,
},
});
}
```

brentertz suggestion worked for me. I'm using popper.js v.1.14.3

After following the suggestion of adding the mock file for popper, I receive the following error after each run of my test suites which test components using reactstrap:

console.error node_modules/prop-types/factoryWithTypeCheckers.js:21 Warning: Invalid argument supplied to oneOf, expected an instance of array.

Ideas?

Im using:
"@testing-library/jest-dom": "^4.0.0",
"@testing-library/react": "^8.0.4",

@jordonedavidson

tippy.js uses @testing-library/react to test popper and has this in their jest setup.

https://github.com/atomiks/tippy.js-react/blob/master/test/setup.js

Fwiw/fyi in create-react-app we had to put this at src/__mocks__/popper.js.js:

import StockPopperJs from "popper.js";

export default class PopperJs {
  static placements = StockPopperJs.placements;

  constructor() {
    return {
      destroy: () => {},
      scheduleUpdate: () => {},
      update: () => {},
    };
  }
}

(And, yes, the double-extension popper.js.js file name is required.)

Thank you all! Mocking this significantly reduced the time it took my jest (jsdom) integration tests to execute. anyone who comes across this; I HIGHLY recommend you do this. It will not affect the functionality of your tooltip/modal based tests.

For anyone looking to force the mock to render out in their tests (I was testing using Vue and Jest) I used the following with and additonal render method mock defined..

        jest.mock('popper.js', () => {
            const PopperJS = jest.requireActual('popper.js');

            return class {
                static placements = PopperJS.placements;

                constructor() {
                    return {
                        destroy: () => { },
                        scheduleUpdate: () => { },
                        render: function (this: any, h){
                            return this.$options._renderChildren
                        }
                    };
                }
            };
        });
Was this page helpful?
0 / 5 - 0 ratings