Mapbox-gl-js: MapboxGL fails outside browser (e.g. with jsdom) throwing 'createObjectURL is not a function'

Created on 21 Oct 2016  ·  10Comments  ·  Source: mapbox/mapbox-gl-js

mapbox-gl-js 0.26.0:

Steps to Trigger Behavior

Minimal-ish reproduction repo. The specific test-case is here

Presuming a set-up using webpack and jsdom for running tests

  1. Include mapbox-gl/dist/mapbox-gl.js in a test file

Expected Behavior

No error

Actual Behavior

Throws this error:

TypeError: window.URL.createObjectURL is not a function

Consequences

If I want to test a file that interacts with the mapbox-gl library, I won't be able to without mocking out the include.

I guess this might also fail on browsers that don't implement createObjectURL, but I haven't tested it, and probably there are other technologies you're using that would fail too!

Cause

This file is included in the built file: /js/util/browser/web_worker.js, which includes this line:

const workerURL = window.URL.createObjectURL(new WebWorkify(require('../../source/worker'), {bare: true}));

jsdom hasn't implemented window.URL.createObjectURL, hence the error. createObjectURL is used in /js/util/ajax.js also but since that's not in a function it doesn't throw an error unless that function is called.


I've been working around this by pinning back to 0.24 — which doesn't fail in this manner.

Possibly the recommendation is to mock out the library? Good to know if so.

Most helpful comment

I know this is a long closed issue, but I had to keep going a bit further from the last comment https://github.com/mapbox/mapbox-gl-js/issues/3436#issuecomment-459460798 to get a working set up. I started with the code there, but kept hitting undefined errors for every static method or Map instance method. To get around those I just kept tagging them on to where the code was expecting them. At the time of this writing, I'm at:

In src/setupTests.ts

jest.mock('mapbox-gl/dist/mapbox-gl', () => ({
  GeolocateControl: jest.fn(),
  Map: jest.fn(() => ({
    addControl: jest.fn(),
    on: jest.fn(),
    remove: jest.fn()
  })),
  NavigationControl: jest.fn()
}));

export default undefined;

Assuming tests will fail again if I use another static or instance method like queryRenderedFeatures or similar. If so, I'll at that key to the return object of Map and set the value to jest.fn(). Not sure if this is the right way, but it's the way that's working at the moment.

All 10 comments

This isn't a supported use of Mapbox GL JS, however, you may be able to get it to work if you don't use the dist version -- use require('mapbox-gl') instead, which will get you a version tailored for a node environment.

You might also be interested in https://github.com/mapbox/mapbox-gl-js-mock.

@neoeno How did you end up working around this? Did you go with mapbox-gl-js-mock or some other solution? Thanks, in advance, for the advice.

+1 re: @stdavis a quick example showing how to correctly utilize mapbox-gl-js-mock would be really helpful.

Currently importing the mock lib is causing:

./node_modules/mapbox-gl-js-mock/node_modules/mapbox-gl/src/geo/transform.js
Module parse failed: Unexpected token (23:12)

Yeah, the readme for 'mapbox-gl-js-mock' is useless.

You can add this line to your jest.stubs.js file:

window.URL.createObjectURL = function() {};

For others landing here looking for a solution on how to mock mapbox-gl the solution can be found in Jest documentation: Mocking Node Modules

I found the mocking solution that worked for me here.

jest.mock('mapbox-gl/dist/mapbox-gl', () => ({
  Map: () => ({}),
}));

Thanks @jenyeeiam

I know this is a long closed issue, but I had to keep going a bit further from the last comment https://github.com/mapbox/mapbox-gl-js/issues/3436#issuecomment-459460798 to get a working set up. I started with the code there, but kept hitting undefined errors for every static method or Map instance method. To get around those I just kept tagging them on to where the code was expecting them. At the time of this writing, I'm at:

In src/setupTests.ts

jest.mock('mapbox-gl/dist/mapbox-gl', () => ({
  GeolocateControl: jest.fn(),
  Map: jest.fn(() => ({
    addControl: jest.fn(),
    on: jest.fn(),
    remove: jest.fn()
  })),
  NavigationControl: jest.fn()
}));

export default undefined;

Assuming tests will fail again if I use another static or instance method like queryRenderedFeatures or similar. If so, I'll at that key to the return object of Map and set the value to jest.fn(). Not sure if this is the right way, but it's the way that's working at the moment.

If you put this at __mocks__/mapbox-gl.js, then it'll be loaded automatically on all jest tests.

module.exports = {
  // whatever properties and functions you need access to
};

Another path, if you prefer to keep your mocking per-test:

// ...imports here, _except_ the offending module

function loadOffendingModule() {
  const {default, namedExport} = require('path/to/offendingModule');
  return {offendingModule: default, namedExport};
}

describe('testBlock', () => {
  /* global global */
  // JSDOM what do we pay you for??
  const originalURL = global.URL;
  const originalWindow = global.window;
  const originalDocument = global.document;

  beforeEach(() => {
    global.URL = {createObjectURL: () => ''};
    global.window = {
      ...originalWindow,
      URL: global.URL,
      // ...anything else you need to mock
    };
    global.document = {
      ...originalDocument,
      createElement: () => ({
        setAttribute: () => {},
      }),
      // ...anything else you need to mock
    };
  });

  afterEach(() => {
    global.URL = originalURL;
    global.window = originalWindow;
    global.document = originalDocument;
  });

  test('a test...', () => {
    const {offendingModule, namedExport} = loadOffendingModule();
    // ...use the "imports" as normal in the test
  });
});

This works when the "offending module" is importing something that immediately (on import/require) executes code that throws in some environment, e.g. Node/JSDOM-mocked browser. The idea here is:

  • set up URL/window/document mocks in a beforeEach()
  • _then_ pull in the offending module via a require()
  • then execute tests,
  • then clean up mocks with afterEach().

Messy, but it works. Note also that I mocked out more here than you'd likely need just for mapbox-gl, I was hitting all sorts of NPEs (kepler.gl, d3, react-sortable-hoc). There goes my morning 😝

Was this page helpful?
0 / 5 - 0 ratings