mapbox / mapbox-gl-js

Interactive, thoroughly customizable maps in the browser, powered by vector tiles and WebGL
https://docs.mapbox.com/mapbox-gl-js/
Other
11.19k stars 2.22k forks source link

MapboxGL fails outside browser (e.g. with jsdom) throwing 'createObjectURL is not a function' #3436

Closed neoeno closed 8 years ago

neoeno commented 8 years ago

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.

jfirebaugh commented 8 years ago

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.

stdavis commented 7 years ago

@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.

kuanb commented 6 years ago

+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)
goeurojesse commented 6 years ago

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

krzysztofzuraw commented 6 years ago

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

window.URL.createObjectURL = function() {};
rlueder commented 6 years ago

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

billyhunt commented 5 years ago

I found the mocking solution that worked for me here.

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

Thanks @jenyeeiam

tylergaw commented 5 years ago

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.

supersonicclay commented 4 years ago

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
};
ericsoco commented 3 years ago

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:

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 😝

Dylansc22 commented 2 years ago

This issue seems very similar to what I am encountering when attempting to use Quokka on the mapbox-gl library, and initializing a very basic map. Quokka enables live logging and testing of function results to streamline debugging. See Quokka's Website and a Youtube Demo of the basics of Quokka for more info.

TLDR/Summary How do I get Quokka's Live logging to work in a basic mapbox sandbox/testing-environment?

While this issue is closed, and I'm sure I'm doing a lot wrong on my end, but I am having a lot of trouble making progress and finding additional information online. This thread seems to be the closest to my current problem.

The Error:

window.URL.createObjectURL is not a function 
  ​​​​​at ​​​​​​​​define​​​ ​./node_modules/mapbox-gl/dist/mapbox-gl.js:25​
  ​​​​​at ​​​​​​./node_modules/mapbox-gl/dist/mapbox-gl.js:35​
  ​​​​​at ​​​​​​./node_modules/mapbox-gl/dist/mapbox-gl.js:3​
  ​​​​​at ​​​​​​./node_modules/mapbox-gl/dist/mapbox-gl.js:6​
  ​​​​​at ​​​​​​​​Object.<anonymous>​​​ ​./node_modules/mapbox-gl/dist/mapbox-gl.js:46​

Navigating to at ​​​​​​​​define​​​ ​./node_modules/mapbox-gl/dist/mapbox-gl.js:25, it is the following code...

      if (typeof window !== 'undefined') {
        mapboxgl.workerUrl = window.URL.createObjectURL(new Blob([workerBundleString], { type: 'text/javascript' }));
    }

Steps to reproduce: (probably unnecessarily long-winded --- sorry)

  1. Opened VSCode and open a new blank folder by clicking 'Open Folder..."
  2. Install the Quokka Plugin via VSCode -- Click "Extensions" (in left sidebar) > Searched "quokka" > Clicked "Install"
  3. Open a Javascript File monitored by Quokka -- Click F1 or (Ctrl + Shift + P) > Type ">quokka.js New Javascript File"
  4. Add the mapbox-gl library -- Add import mapboxgl from 'mapbox-gl';
  5. The above import statement will trigger a Quokka terminal window in VSCode that asks you:

​Install "mapbox-gl" package for the current quokka file​ ​Install "mapbox-gl" package into the project​   Cannot find module 'mapbox-gl'  Require stack: 

// set accessToken mapboxgl.accessToken = 'YOUR_PERSONAL_TOKEN_HERE'

// init map const map = new mapboxgl.Map({ container: 'map', style: 'mapbox://styles/mapbox/streets-v11', center: [-74.50, 40], zoom: 9 });

8. This results in the error I listed above `window.URL.createObjectURL is not a function`
9. If I delete the code below from `node_modules/mapbox-gl/dist/mapbox-gl.js`, (which I imagine is *is very bad* but it works as a workaround.)
  if (typeof window !== 'undefined') {
    mapboxgl.workerUrl = window.URL.createObjectURL(new Blob([workerBundleString], { type: 'text/javascript' }));
}
... I get a new error: `Container 'map' not found`
10. So I create an `index.html` in the same root directory with the following

<!DOCTYPE html>

Document

And I still get the error

Container 'map' not found.


**My Machine** 
Ubuntu 20.04
node: v16.13.1
Quokka global config file​​​​ (`~/.quokka/config.json`) is as follows:

{"pro":true,"plugins":["jsdom-quokka-plugin"]}



I'd really appreciate some feedback. 

Best
Dylan
ryanhamley commented 2 years ago

Hey @Dylansc22 unfortunately, this seems to be a limitation of Quokka as documented in their How Does It Work? section:

Quokka may not work if you are trying to use code or components that require special runtime initialization, or if you are running code whose runtime is not compatible with node.js.

For example, if you try and run Quokka on a React functional component, while the component will work in the Browser because of React’s browser runtime, no code will be executed by Quokka in node.js

This would explain why you're encountering an error around accessing window properties since those are not available in Node. I suspect the container 'map' not found error is similar since Node does not have a DOM object. You can attempt to stub/mock these functions (there are npm packages that do this sort of thing) in order to get GL JS running, but I suspect that will be a lot of work for limited payoff. At the end of the day, GL JS is designed to run in a browser environment with the window and document APIs.

Dylansc22 commented 2 years ago

@ryanhamley Thanks for the info! While not the answer I was hoping for, it's good to have the confirmation the error is not on my end (and learn something along the way -- wasn't even familiar with the concept of "mock" libraries). I see mapbox-gl-js-mock isn't really maintained. Very unfortunate that there isn't a simple workaround, as it would really help my workflow.

If I do self-teach my way through a solution, I'll post it here in case someone stumbles across this issue.

Cheers

johncarmack1984 commented 4 months ago

Just in case anybody in 2024 is solving the same issue with Vitest:

vi.mock("mapbox-gl/dist/mapbox-gl", () => {
  const defaultExport = {
    GeolocateControl: vi.fn(),
    Map: vi.fn(() => ({
      addControl: vi.fn(),
      on: vi.fn(),
      remove: vi.fn(),
    })),
    NavigationControl: vi.fn(),
    default: vi.fn(),
  }
  return {
    ...defaultExport,
    default: defaultExport,
  }
})