Rovanion
Rovanion

Reputation: 4582

Testing a react-mapbox-gl with jsodom and jest

Consider we have the following Map component. This is in TypeScript, but the same should apply for normal JavaScript.

import * as React from 'react';
import ReactMapboxGl from 'react-mapbox-gl';

const MapBox = ReactMapboxGl({
  accessToken: 'pk.<redacted>'
});

export default class Map extends React.Component {
  render() {
    return (
      <MapBox
        style="mapbox://styles/mapbox/streets-v9"
        zoom={[0]}
        containerStyle={{
          height: '500px',
          width: 'inherit'
        }}
      />);
  }
}

It is then part of some react application that is rendered as such:

import * as React from 'react';

export default class App extends React.Component {
  render() {
    return (
          <Map />
    );
  }
}

In order to test this setup we use Jest and JSDOM.

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';

it('renders without crashing', () => {
  const div = document.createElement('div');
  ReactDOM.render(<App />, div);
});

This test will fail to run and produce the following output:

    TypeError: window.URL.createObjectURL is not a function

      at Object.254.../../source/worker (node_modules/mapbox-gl/dist/mapbox-gl.js:509:100)
      at s (node_modules/mapbox-gl/dist/mapbox-gl.js:1:711)
      at node_modules/mapbox-gl/dist/mapbox-gl.js:1:762
      at Object.280.../ (node_modules/mapbox-gl/dist/mapbox-gl.js:561:28)
      at s (node_modules/mapbox-gl/dist/mapbox-gl.js:1:711)
      at node_modules/mapbox-gl/dist/mapbox-gl.js:1:762
      at Object.263../worker_pool (node_modules/mapbox-gl/dist/mapbox-gl.js:527:29)
      at s (node_modules/mapbox-gl/dist/mapbox-gl.js:1:711)
      at node_modules/mapbox-gl/dist/mapbox-gl.js:1:762
      at Object.191.../render/glyph_manager (node_modules/mapbox-gl/dist/mapbox-gl.js:383:809)
      at s (node_modules/mapbox-gl/dist/mapbox-gl.js:1:711)
      at node_modules/mapbox-gl/dist/mapbox-gl.js:1:762
      at Object.248.../geo/lng_lat (node_modules/mapbox-gl/dist/mapbox-gl.js:497:338)
      at s (node_modules/mapbox-gl/dist/mapbox-gl.js:1:711)
      at node_modules/mapbox-gl/dist/mapbox-gl.js:1:762
      at Object.72.../package.json (node_modules/mapbox-gl/dist/mapbox-gl.js:144:148)
      at s (node_modules/mapbox-gl/dist/mapbox-gl.js:1:711)
      at e (node_modules/mapbox-gl/dist/mapbox-gl.js:1:882)
      at node_modules/mapbox-gl/dist/mapbox-gl.js:1:900
      at Object.<anonymous>.i (node_modules/mapbox-gl/dist/mapbox-gl.js:1:177)
      at Object.<anonymous> (node_modules/mapbox-gl/dist/mapbox-gl.js:1:413)
      at Object.<anonymous> (node_modules/react-mapbox-gl/lib/map.js:21:16)
      at Object.<anonymous> (node_modules/react-mapbox-gl/lib/index.js:3:13)
      at Object.<anonymous> (src/Map.tsx:14:25)
      at Object.<anonymous> (src/NewOrder.tsx:21:13)
      at Object.<anonymous> (src/Routes.ts:17:18)
      at Object.<anonymous> (src/App.tsx:16:16)
      at Object.<anonymous> (src/App.test.tsx:6:169)
          at <anonymous>

The question to you, my dear reader, is simply: Is it possible to work around this issue? Are there seams we can use to inject a mocked MapBoxGL library?

I've found multiple issues on GitHub related to this problem but none of them provide a solution: 1, 2. Some points toward using mapbox-gl-js-mock while others claim it is of no use since it too will require a real browser to run.

There is also the related issue on the JSDOM project about adding URL.createObjectURL which would perhaps resolve the underlying issue.

Upvotes: 14

Views: 9368

Answers (5)

edan
edan

Reputation: 1168

I've struggled with this for a while, and finally came up with a workable solution:

  1. I mock out a map object, to track the map center/zoom/bounds.
  2. I mock out the all the Mapbox react components to force them to render their children. Otherwise, Mapbox totally disables them, and I can't test any custom views/logic.

I'm using vitest and React Testing Lib, but these should work more generally:

beforeAll(() => {
  mockMap.reset()

  // Mock Mapbox components
  vi.mock('react-map-gl', async () => {
    const originalModule = await vi.importActual('react-map-gl');

    return {
      ...originalModule,

      // Mock the map returned by useMap
      // This allows us to assert against the state of the map (center, etc)
      useMap: () => ({ default: mockMap }),

      // Mapbox will not event attempt to render these components from a test
      // By mocking them out this way, we can at least render the child components,
      // which may include UI behavior that we want to test
      // (eg. we can test the content of a <Popup />
      default: ({children}) => (<>{children}</>),
      AttributionControl: ({children}) => (<>{children}</>),
      FullscreenControl: ({children}) => (<>{children}</>),
      GeolocateControl: ({children}) => (<>{children}</>),
      Layer: ({children}) => (<>{children}</>),
      Marker: ({children}) => (<>{children}</>),
      NavigationControl: ({children}) => (<>{children}</>),
      Popup: ({children}) => (<>{children}</>),
      ScaleControl: ({children}) => (<>{children}</>),
      Source: ({children}) => (<>{children}</>),
    };
  });
})
// this is simple, but could be expanded as needed
class MockMap {
  center: [number, number] | null = null;
  zoom: number | null = null;
  bounds: [number, number, number, number] | null = null;

  flyTo({ center, zoom }): void {
    this.center = center;
    this.zoom = zoom;
    this.bounds = null;
  }

  fitBounds(bounds: [number, number, number, number]): void {
    this.center = null;
    this.zoom = null;
    this.bounds = bounds;
  }

  reset() {
    this.center = null;
    this.zoom = null;
    this.bounds = null;
  }
}

export const mockMap = new MockMap();

Upvotes: 0

Mareș Ștefan
Mareș Ștefan

Reputation: 500

You can mock the entire map with a custom element like this:

jest.mock('react-mapbox-gl', () => ({
  __esModule: true,
  default: () => function () {
    return <span>Mock map</span>;
  },
  Cluster: () => <span>Mock cluster</span>,
}));

Upvotes: 1

Seb
Seb

Reputation: 99

If your are using import and typescript:

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

Upvotes: -1

Morlo Mbakop
Morlo Mbakop

Reputation: 3756

You can add this to your test entry file for setupTest.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(),
}));

Upvotes: 8

meteor
meteor

Reputation: 2568

I had the same issue and when i added the below code as listed here to the top of my test block it worked.

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

Upvotes: 14

Related Questions