diff --git a/adhocracy4/maps_react/static/a4maps_react/AddMarkerControl.js b/adhocracy4/maps_react/static/a4maps_react/AddMarkerControl.js index 498ca51fe..88dd09854 100644 --- a/adhocracy4/maps_react/static/a4maps_react/AddMarkerControl.js +++ b/adhocracy4/maps_react/static/a4maps_react/AddMarkerControl.js @@ -19,7 +19,7 @@ export function checkPointInsidePolygon (marker, polygons) { const markerProps = { icon: makeIcon(), draggable: true } -class AddMarkerControlClass extends L.Control { +export class AddMarkerControlClass extends L.Control { constructor ({ input, point }) { super() this.marker = null diff --git a/adhocracy4/maps_react/static/a4maps_react/Map.jsx b/adhocracy4/maps_react/static/a4maps_react/Map.jsx index 067cea639..826efe3b5 100644 --- a/adhocracy4/maps_react/static/a4maps_react/Map.jsx +++ b/adhocracy4/maps_react/static/a4maps_react/Map.jsx @@ -10,7 +10,7 @@ const polygonStyle = { fillOpacity: 0.2 } -export const Map = React.forwardRef(function Map ( +const Map = React.forwardRef(function Map ( { attribution, baseUrl, polygon, omtToken, children, ...rest }, ref ) { const map = useRef() @@ -41,3 +41,5 @@ export const Map = React.forwardRef(function Map ( ) }) + +export default Map diff --git a/adhocracy4/maps_react/static/a4maps_react/ZoomControl.js b/adhocracy4/maps_react/static/a4maps_react/ZoomControl.js index cb308dd71..37deb9927 100644 --- a/adhocracy4/maps_react/static/a4maps_react/ZoomControl.js +++ b/adhocracy4/maps_react/static/a4maps_react/ZoomControl.js @@ -2,7 +2,7 @@ import { createControlComponent } from '@react-leaflet/core' import L from 'leaflet' // Create a Leaflet Control -const createLeafletElement = (props) => { +export const createLeafletElement = (props) => { const zoomControl = L.control.zoom(props) const updateDisabled = () => { diff --git a/adhocracy4/maps_react/static/a4maps_react/__tests__/AddMarkerControl.jest.js b/adhocracy4/maps_react/static/a4maps_react/__tests__/AddMarkerControl.jest.js new file mode 100644 index 000000000..665d12c3c --- /dev/null +++ b/adhocracy4/maps_react/static/a4maps_react/__tests__/AddMarkerControl.jest.js @@ -0,0 +1,60 @@ +import L from 'leaflet' +import { AddMarkerControlClass } from '../AddMarkerControl' +import { polygonData } from './Map.jest' +import { jest } from '@jest/globals' + +describe('AddMarkerControlClass', () => { + const polygonGeoJSON = L.geoJSON(polygonData) + const map = { on: jest.fn(), off: jest.fn(), addLayer: jest.fn(), constraints: polygonGeoJSON } + const point = JSON.stringify({ + type: 'Feature', + properties: {}, + geometry: { + type: 'Point', + coordinates: [5, 5] + } + }) + + it('sets a marker', () => { + const input = document.createElement('input') + const instance = new AddMarkerControlClass({ input }) + instance.map = map + + const latlng = { lat: 5, lng: 5 } + + expect(instance.marker).toBe(null) + instance.updateMarker(latlng) + expect(instance.marker).toBeDefined() + expect(input.value).toEqual(expect.stringContaining('5,5')) + instance.updateMarker({ lat: 2, lng: 2 }) + expect(input.value).toEqual(expect.stringContaining('2,2')) + }) + + it('does not set a marker when outside', () => { + const input = document.createElement('input') + const instance = new AddMarkerControlClass({ input }) + instance.map = map + const latlng = { lat: 15, lng: 15 } + expect(instance.marker).toBe(null) + instance.updateMarker(latlng) + expect(instance.marker).toBe(null) + expect(input.value).toEqual('') + }) + + it('updates on drag', () => { + const input = document.createElement('input') + const instance = new AddMarkerControlClass({ input, point }) + instance.map = map + expect(instance.oldCoords).toStrictEqual([5, 5]) + const newCoords = { lat: 10, lng: 10 } + + const e = { target: { getLatLng: () => newCoords, setLatLng: jest.fn() } } + instance.onDragend(e) + expect(instance.oldCoords).toStrictEqual(newCoords) + + const e2 = { target: { getLatLng: () => ({ lat: 15, lng: 15 }), setLatLng: jest.fn() } } + instance.onDragend(e2) + expect(e2.target.setLatLng).toHaveBeenCalledWith(newCoords) + expect(instance.oldCoords).toStrictEqual(newCoords) + }) +}) diff --git a/adhocracy4/maps_react/static/a4maps_react/__tests__/Map.jest.jsx b/adhocracy4/maps_react/static/a4maps_react/__tests__/Map.jest.jsx new file mode 100644 index 000000000..278f07b1d --- /dev/null +++ b/adhocracy4/maps_react/static/a4maps_react/__tests__/Map.jest.jsx @@ -0,0 +1,66 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import Map from '../Map' + +export const polygonData = { + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [0, 0], + [10, 0], + [10, 10], + [0, 10] + ] + ] + } +} + +jest.mock('react-leaflet', () => { + const ActualReactLeaflet = jest.requireActual('react-leaflet') + const React = require('react') + + const MapContainer = React.forwardRef((props, ref) => ( +
+ +
+ )) + MapContainer.displayName = 'MapContainer' + + const GeoJSON = React.forwardRef((props, ref) => ( +
+ )) + GeoJSON.displayName = 'GeoJSON' + + return { + __esModule: true, + ...ActualReactLeaflet, + GeoJSON, + MapContainer + } +}) + +describe('Map component tests', () => { + test('component renders', () => { + render() + const mapNode = screen.getByTestId('map') + + expect(mapNode).toBeTruthy() + }) + + test('renders map with GeoJSON when polygon prop is provided', () => { + render() + const geoJsonNode = screen.getByTestId('geojson') + + expect(geoJsonNode).toBeTruthy() + }) + + test('does not render GeoJSON when polygon prop is not provided', () => { + render() + const geoJsonNode = screen.queryByTestId('geojson') + + expect(geoJsonNode).toBeFalsy() + }) +}) diff --git a/adhocracy4/maps_react/static/a4maps_react/__tests__/ZoomControl.jest.js b/adhocracy4/maps_react/static/a4maps_react/__tests__/ZoomControl.jest.js new file mode 100644 index 000000000..c33c38b6e --- /dev/null +++ b/adhocracy4/maps_react/static/a4maps_react/__tests__/ZoomControl.jest.js @@ -0,0 +1,38 @@ +import { createLeafletElement } from '../ZoomControl' +import L from 'leaflet' + +describe('ZoomControl', () => { + it('createLeafletElement should return a zoom control', () => { + const zoomControl = createLeafletElement({}) + expect(zoomControl).toBeInstanceOf(L.Control.Zoom) + + const map = { + _zoom: 5, + getMinZoom: jest.fn(() => 3), + getMaxZoom: jest.fn(() => 10), + getZoom: jest.fn(), + on: jest.fn(), + off: jest.fn() + } + L.DomUtil.addClass = jest.fn() + L.DomUtil.removeClass = jest.fn() + zoomControl._map = map + + const onAdd = jest.spyOn(L.Control.Zoom.prototype, 'onAdd') + const onRemove = jest.spyOn(L.Control.Zoom.prototype, 'onRemove') + + zoomControl.onAdd(map) + + expect(map.on).toHaveBeenCalledWith('zoom', expect.any(Function)) + expect(onAdd).toHaveBeenCalledWith(map) + expect(map.getMinZoom).toHaveBeenCalled() + expect(map.getMaxZoom).toHaveBeenCalled() + expect(L.DomUtil.addClass).toHaveBeenCalledTimes(0) + expect(L.DomUtil.removeClass).toHaveBeenCalledTimes(2) + + zoomControl.onRemove(map) + + expect(map.off).toHaveBeenCalledWith('zoom', expect.any(Function)) + expect(onRemove).toHaveBeenCalledWith(map) + }) +}) diff --git a/jest.config.js b/jest.config.js index ca88e87bd..aa923a902 100644 --- a/jest.config.js +++ b/jest.config.js @@ -29,7 +29,13 @@ const config = { ], transform: { '^.+\\.[t|j]sx?$': 'babel-jest' - } + }, + transformIgnorePatterns: [ + 'node_modules/(?!(@?react-leaflet)/)' + ], + setupFiles: [ + '/setupTests.js' + ] } module.exports = config diff --git a/setupTests.js b/setupTests.js new file mode 100644 index 000000000..47fc61ff2 --- /dev/null +++ b/setupTests.js @@ -0,0 +1,3 @@ +if (typeof window.URL.createObjectURL === 'undefined') { + window.URL.createObjectURL = () => {} +}