Reputation: 10752
Let's say I have a mapbox map. The mapbox map has an onclick handler, and within the onclick handler's callback, a function is called:
import { Map } from 'mapbox-gl'
const map = new Map({
container: someContainer,
center,
zoom
})
map.on('click', e => {
const { lng, lat } = e.lngLat
runSomeFunction(lat, lng). // <---- i want to test this function
})
I want to test my map using jest. There's already a lot of questions out there surrounding the fact that the mapbox-gl fails pretty hard outside the browser environment. While some people have tried to find workarounds (fixing the window.URL. createObjectURL
problem, using the defunct mapbox-gl-js-mock repo, overcoming the lack of GL in the jest envinroment), I've been going down that rabbit hole for about 2 days. Its time for another approach.
Many talk about mocking mapbox, like this:
jest.mock('mapbox-gl', () => ({
Map: jest.fn(() => ({
on: jest.fn(),
otherMethods: jest.fn(),
})),
}));
I can use this method and get my tests to pass. But I can't really test any interesting behavior. How can I mock the behavior of a user clicking on the map, and test that I expect(runSomeFunction).toHaveBeenCalledWith(whatever)
?
I really don't know where to start, as I can't find a single example of a jest unit test that actually tests map behavior or events. In hopes of finding something parallel, I've also looked into how to test leaflet map events in jest, but I haven't found anything helpful.
Upvotes: 4
Views: 1810
Reputation: 1119
I see two parts to the question:
runSomeFunction()
(b/c the comment // <---- i want to test this function
points at runSomeFunction()
)To me, the click handler is the anonymous function:
(e) => {
const { lng, lat } = e.lngLat;
runSomeFunction(lat, lng);
}
runSomeFunction()
could possibly be tested in isolation with a unit test.
The click handler could possibly be tested in isolation with a unit test if it's extracted to a named function.
The click handler can also be tested by firing a click event and
confirming that it calls a mocked / spied runSomeFunction()
with the correct arguments as suggested by the OP, or confirming the end result of the click. I
would call these
integration tests instead of unit tests because they don't test the
click handler in isolation. I like this take on integration vs
unit
testing.
Here is a basic Map
jest mock to handle events with
mapbox-gl
3.1.2
.
The testing environment is jsdom with babel compilation, set up per the jest docs.
// tests/__mocks__/mapbox-gl.js
import EventEmitter from 'events';
import { TextDecoder } from 'util';
// Add `TextDecoder` to globals.
globalThis.TextDecoder = TextDecoder;
const mapboxActual = jest.requireActual('mapbox-gl');
/**
* Mock Map class.
*
* Extends the `EventEmitter` class and adds mock properties & methods.
*
* The real `Map` extends `Evented`, but `Evented.fire()` is now private
* and may be deprecated in the future.
*
* @see https://nodejs.org/api/events.html#class-eventemitter
* @see https://github.com/mapbox/mapbox-gl-js/blob/main/CHANGELOG.md#%EF%B8%8F-breaking-changes-5
*/
class MockMap extends EventEmitter {
// ... Put mock properties & methods here.
}
mapboxActual.Map = MockMap;
export default mapboxActual;
This is a trivial example that tests the runSomeFunction()
follow-through with a spy, only b/c I don't know a better way to confirm
that console.log()
was called correctly. In a real app, if
runSomeFunction()
renders something, you can test that it appears on
the screen as expected.
// runSomeFunction.js
export default function runSomeFunction(lat, lng) {
console.log(`Map clicked at ${lat}, ${lng}.`);
}
// map.js
import mapboxgl from 'mapbox-gl';
import runSomeFunction from "./runSomeFunction";
// If you pull in `mapbox-gl` with an html `script` tag instead of
// importing it, you'll need to add `mapboxgl` to the globals for the
// test environment so that it's available here.
mapboxgl.accessToken = 'pk.eyJ1Ijoia2VudHIiLCJhIjoiY2tva3YxOWFzMDdpcDJvcno5ZjBzM3JvNCJ9.ZdH2WahCpmtdiLsNLWZNFQ';
const map = new mapboxgl.Map({
container: 'map', // container ID
center: [ -74.5, 40 ], // starting position [lng, lat]
zoom: 9, // starting zoom
});
map.on('click', (e) => {
const { lng, lat } = e.lngLat;
runSomeFunction(lat, lng);
});
export default map;
// map.test.js
import map from './map';
jest.spyOn(console, 'log');
it('logs click on map', () => {
const testLatLng = {
lat: 39,
lng: -74,
};
// Confirm `console.log` not called before event is fired.
expect(console.log)
.not
.toBeCalled();
// Emit click event.
map.emit(
'click',
{
lngLat: testLatLng,
}
);
// Check result.
// May need delay for async event.
expect(console.log)
.toBeCalledTimes(1);
expect(console.log)
.toBeCalledWith(`Map clicked at ${testLatLng.lat}, ${testLatLng.lng}.`);
});
mapbox-gl
via script
tagIf you're pulling in mapbox-gl
via an html script
tag instead of
importing it into map.js
, here's an example of setting the global for
tests.
// jest.config.js
/** @type {import('jest').Config} */
const config = {
setupFiles: [
'<rootDir>/tests/setupTests.js'
],
testEnvironment: "jsdom",
};
module.exports = config;
// tests/setupTests.js
import mapboxgl from "mapbox-gl";
globalThis.mapboxgl = mapboxgl;
Upvotes: 2