Reputation: 4582
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
Reputation: 1168
I've struggled with this for a while, and finally came up with a workable solution:
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
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
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
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