Skip to content

Commit 8c677c6

Browse files
authored
mapbox component jest tests (#311)
added jest tests for Mapbox component, specifically for the two prop fields: `getCoordinate` and `onDrag`. The remaining props are best tested through storybook. note that `mapbox-gl` is mocked as it seems to use some web browser functionality on initialization that is not supported in jest test environment (jsdom). Without mocking, jest would present an error `Error: Failed to initialize WebGL.`. - tried third party libraries `jest-webgl-canvas-mock` and `jest-canvas-mock` still result in more window properties access related errors (`[TypeError: e.window.Worker is not a constructor]`, which could be resolve by [manually mocking Worker](jestjs/jest#3449), `[TypeError: this.target.addEventListener is not a function]`...). - Looked into how pageJS test mapbox provider in their map component: the map wrapper component invoke a [load function](https://github.com/yext/pages/blob/main/packages/pages/src/components/map/map.tsx#L112) to construct [a script tag](https://github.com/yext/components-tsx-maps/blob/main/src/Providers/Mapbox.js#L137) from `yext/components-tsx-maps` and the assertions in the unit tests seem to check for the rendering of the wrapper component only and not waiting for the script to load. No content/interaction of the map was tested from my understanding. - Looked into how mapbox-gl-js does their own testing: they had a couple util files to set up the environment for their tests (mock requests, [mock HtmlCanvas/WebLG in window](https://github.com/mapbox/mapbox-gl-js/blob/main/src/util/window.js) object, etc.). I decided this complexity is probably unnecessary to add to the repo but we can discuss if other may think differently. We can have jest strictly test new isolated functionalities added from the component and leave all the UI rendering and mapbox interaction to storybook tests. SLAP-2222 TEST=auto new jest tests passed
1 parent ab88584 commit 8c677c6

File tree

3 files changed

+116
-2
lines changed

3 files changed

+116
-2
lines changed

docs/search-ui-react.mapboxmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@ For the map to work properly, be sure to include Mapbox GL stylesheet in the app
3232

3333
For instance, user may add the following import statement in their application's index file or in the file where `MapboxMap` is used: `import 'mapbox-gl/dist/mapbox-gl.css';`
3434

35-
Or, user may add a stylesheet link in their html page: `<link href="https://api.mapbox.com/mapbox-gl-js/VERSION/mapbox-gl.css" rel="stylesheet">`
35+
Or, user may add a stylesheet link in their html page: `<link href="https://api.mapbox.com/mapbox-gl-js/v2.9.2/mapbox-gl.css" rel="stylesheet" />`
3636

src/components/MapboxMap.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export interface MapboxMapProps<T> {
7474
* `import 'mapbox-gl/dist/mapbox-gl.css';`
7575
*
7676
* Or, user may add a stylesheet link in their html page:
77-
* `<link href="https://api.mapbox.com/mapbox-gl-js/VERSION/mapbox-gl.css" rel="stylesheet">`
77+
* `<link href="https://api.mapbox.com/mapbox-gl-js/v2.9.2/mapbox-gl.css" rel="stylesheet" />`
7878
*
7979
* @param props - {@link MapboxMapProps}
8080
* @returns A React element containing a Mapbox Map

tests/components/MapboxMap.test.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { render } from '@testing-library/react';
2+
import { mockAnswersState } from '../__utils__/mocks';
3+
import { CoordinateGetter, MapboxMap, Coordinate } from '../../src/components/MapboxMap';
4+
import { Source, State } from '@yext/search-headless-react';
5+
import { Map, Marker } from 'mapbox-gl';
6+
7+
jest.mock('@yext/search-headless-react');
8+
jest.mock('mapbox-gl');
9+
10+
interface Location {
11+
customCoordinate: Coordinate
12+
}
13+
14+
const mockedStateDefaultCoordinate: Partial<State> = {
15+
vertical: {
16+
verticalKey: 'vertical',
17+
results: [{
18+
rawData: {
19+
yextDisplayCoordinate: {
20+
latitude: 1,
21+
longitude: 1
22+
}
23+
},
24+
source: Source.KnowledgeManager
25+
}]
26+
}
27+
};
28+
29+
const mockedStateCustomCoordinate: Partial<State> = {
30+
vertical: {
31+
verticalKey: 'vertical',
32+
results: [{
33+
rawData: {
34+
customCoordinate: {
35+
latitude: 2,
36+
longitude: 2
37+
}
38+
},
39+
source: Source.KnowledgeManager
40+
}]
41+
}
42+
};
43+
44+
const mockedStateWrongCoordinateType: Partial<State> = {
45+
vertical: {
46+
verticalKey: 'vertical',
47+
results: [{
48+
rawData: {
49+
yextDisplayCoordinate: [1, 1]
50+
},
51+
source: Source.KnowledgeManager
52+
}]
53+
}
54+
};
55+
56+
describe('default "getCoordinate"', () => {
57+
it('uses result\'s "yextDisplayCoordinate" for marker location', () => {
58+
mockAnswersState(mockedStateDefaultCoordinate);
59+
const setLngLat = jest.spyOn(Marker.prototype, 'setLngLat').mockReturnValue(Marker.prototype);
60+
render(<MapboxMap mapboxAccessToken='TEST_KEY' />);
61+
expect(setLngLat).toBeCalledWith({ lat: 1, lng: 1 });
62+
});
63+
64+
it('displays an error when "yextDisplayCoordinate" field is not present in result', () => {
65+
mockAnswersState(mockedStateCustomCoordinate);
66+
const setLngLat = jest.spyOn(Marker.prototype, 'setLngLat').mockReturnValue(Marker.prototype);
67+
const errorSpy = jest.spyOn(console, 'error').mockImplementation();
68+
render(<MapboxMap mapboxAccessToken='TEST_KEY' />);
69+
expect(errorSpy).toBeCalledTimes(1);
70+
const expectedMessage = 'Unable to use the default "yextDisplayCoordinate" field as the result\'s coordinate';
71+
expect(errorSpy).toBeCalledWith(expect.stringContaining(expectedMessage));
72+
expect(setLngLat).not.toBeCalled();
73+
});
74+
75+
it('displays an error when "yextDisplayCoordinate" field is not of type "Coordinate"', () => {
76+
mockAnswersState(mockedStateWrongCoordinateType);
77+
const setLngLat = jest.spyOn(Marker.prototype, 'setLngLat').mockReturnValue(Marker.prototype);
78+
const errorSpy = jest.spyOn(console, 'error').mockImplementation();
79+
render(<MapboxMap mapboxAccessToken='TEST_KEY' />);
80+
expect(errorSpy).toBeCalledTimes(1);
81+
const expectedMessage = 'The default `yextDisplayCoordinate` field from result is not of type "Coordinate".';
82+
expect(errorSpy).toBeCalledWith(expect.stringContaining(expectedMessage));
83+
expect(setLngLat).not.toBeCalled();
84+
});
85+
});
86+
87+
it('executes custom "getCoordinate" and use the derived coordinate for marker location', () => {
88+
mockAnswersState(mockedStateCustomCoordinate);
89+
const setLngLat = jest.spyOn(Marker.prototype, 'setLngLat').mockReturnValue(Marker.prototype);
90+
const customGetCoordinate: CoordinateGetter<Location> = jest.fn().mockImplementation(
91+
result => result.rawData.customCoordinate);
92+
93+
const errorSpy = jest.spyOn(console, 'error').mockImplementation();
94+
render(<MapboxMap mapboxAccessToken='TEST_KEY' getCoordinate={customGetCoordinate} />);
95+
expect(errorSpy).toBeCalledTimes(0);
96+
expect(customGetCoordinate).toBeCalledTimes(1);
97+
expect(setLngLat).toBeCalledWith({ lat: 2, lng: 2 });
98+
});
99+
100+
it('registers "onDrag" callback to Mapbox\'s event listener for "drag to pan" interaction', () => {
101+
jest.useFakeTimers();
102+
jest.spyOn(Marker.prototype, 'setLngLat').mockReturnValue(Marker.prototype);
103+
const mapOnEventListener = jest.spyOn(Map.prototype, 'on')
104+
.mockImplementation((e, cb) => {
105+
e === 'drag' && cb({});
106+
return Map.prototype;
107+
});
108+
const onDragFn = jest.fn();
109+
render(<MapboxMap mapboxAccessToken='TEST_KEY' onDrag={onDragFn} />);
110+
expect(mapOnEventListener).toBeCalledWith('drag', expect.anything());
111+
expect(onDragFn).toBeCalledTimes(0);
112+
jest.advanceTimersByTime(100); //debounce time
113+
expect(onDragFn).toBeCalledTimes(1);
114+
});

0 commit comments

Comments
 (0)