Seth Lutske
Seth Lutske

Reputation: 10752

Mock mapbox click handler callback in jest

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

Answers (1)

kentr
kentr

Reputation: 1119

I see two parts to the question:

  • testing runSomeFunction() (b/c the comment // <---- i want to test this function points at runSomeFunction())
  • testing that the map handles click events correctly (seems to be real intent of the question)

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.

Jest Mock

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;

Using it

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 tag

If 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

Related Questions