Skip to content

Commit

Permalink
react_maps: write js tests for react leaflet map
Browse files Browse the repository at this point in the history
  • Loading branch information
vellip committed Jan 16, 2024
1 parent 13d8783 commit 14a4a74
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 112 deletions.
8 changes: 4 additions & 4 deletions adhocracy4/maps_react/static/a4maps_react/AddMarkerControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,7 +36,7 @@ class AddMarkerControlClass extends L.Control {
}

updateMarker (latlng) {
const isInsideConstraints = checkPointInsidePolygon(latlng, this.map.constraints)
const isInsideConstraints = checkPointInsidePolygon(latlng, this.map.markerConstraints)
if (isInsideConstraints) {
this.oldCoords = latlng
if (this.marker) {
Expand All @@ -51,11 +51,11 @@ class AddMarkerControlClass extends L.Control {

onDragend (e) {
const targetPosition = e.target.getLatLng()
const isInsideConstraints = checkPointInsidePolygon(targetPosition, this.map.constraints)
const isInsideConstraints = checkPointInsidePolygon(targetPosition, this.map.markerConstraints)
if (!isInsideConstraints) {
e.target.setLatLng(this.oldCoords)
} else {
this.oldCoords = targetPosition
this.updateMarker(targetPosition)
}
}

Expand Down
12 changes: 6 additions & 6 deletions adhocracy4/maps_react/static/a4maps_react/Map.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useRef, useImperativeHandle } from 'react'
import { MapContainer, GeoJSON } from 'react-leaflet'
import MaplibreGlLayer from './MaplibreGlLayer'
import ZoomControl from './ZoomControl'

const polygonStyle = {
color: '#0076ae',
Expand All @@ -10,7 +9,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()
Expand All @@ -21,23 +20,24 @@ export const Map = React.forwardRef(function Map (
return
}
map.current.fitBounds(polygon.getBounds())
map.current.options.minZoom = map.current.getZoom()
map.current.constraints = polygon
map.current.setMinZoom(map.current.getZoom())
// used in AddMarkerControl to specify where markers can be placed
map.current.markerConstraints = polygon
}

return (
<MapContainer
style={{ minHeight: 300 }}
zoom={13}
maxZoom={18}
zoomControl={false}
{...rest}
ref={map}
>
{polygon && <GeoJSON style={polygonStyle} data={polygon} ref={refCallback} />}
<MaplibreGlLayer attribution={attribution} baseUrl={baseUrl} omtToken={omtToken} />
<ZoomControl position="topleft" />
{children}
</MapContainer>
)
})

export default Map
48 changes: 0 additions & 48 deletions adhocracy4/maps_react/static/a4maps_react/ZoomControl.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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(), markerConstraints: 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: 10, lng: 5 }

expect(instance.marker).toBe(null)
instance.updateMarker(latlng)
expect(instance.marker).toBeDefined()
expect(input.value).toEqual(expect.stringContaining('5,10'))
instance.updateMarker({ lat: 2, lng: 5 })
expect(input.value).toEqual(expect.stringContaining('5,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)
})
})
66 changes: 66 additions & 0 deletions adhocracy4/maps_react/static/a4maps_react/__tests__/Map.jest.jsx
Original file line number Diff line number Diff line change
@@ -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) => (
<div data-testid="map">
<ActualReactLeaflet.MapContainer ref={ref} {...props} />
</div>
))
MapContainer.displayName = 'MapContainer'

const GeoJSON = React.forwardRef((props, ref) => (
<div data-testid="geojson"><ActualReactLeaflet.GeoJSON ref={ref} props={props} /></div>
))
GeoJSON.displayName = 'GeoJSON'

return {
__esModule: true,
...ActualReactLeaflet,
GeoJSON,
MapContainer
}
})

describe('Map component tests', () => {
test('component renders', () => {
render(<Map />)
const mapNode = screen.getByTestId('map')

expect(mapNode).toBeTruthy()
})

test('renders map with GeoJSON when polygon prop is provided', () => {
render(<Map polygon={polygonData} />)
const geoJsonNode = screen.getByTestId('geojson')

expect(geoJsonNode).toBeTruthy()
})

test('does not render GeoJSON when polygon prop is not provided', () => {
render(<Map />)
const geoJsonNode = screen.queryByTestId('geojson')

expect(geoJsonNode).toBeFalsy()
})
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load react_maps_tags %}

{% react_choose_point polygon=polygon point=point name=name %}
{% react_choose_point polygon point name %}
<input id="id_{{ name }}" type="hidden" name="{{ name }}" {% if point %}value="{{ point }}"{% endif %}>
21 changes: 15 additions & 6 deletions adhocracy4/maps_react/templatetags/react_maps_tags.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import json

from django import template
from django.utils.html import format_html

from adhocracy4.maps_react.utils import react_tag_factory
from adhocracy4.maps_react.utils import get_map_settings

register = template.Library()

register.simple_tag(
react_tag_factory("choose-point"),
False,
"react_choose_point",
)

@register.simple_tag()
def react_choose_point(polygon, point, name):
attributes = {
"map": get_map_settings(polygon=polygon, point=point, name=name),
}

return format_html(
'<div data-a4-widget="choose-point" ' 'data-attributes="{attributes}"></div>',
attributes=json.dumps(attributes),
)
46 changes: 0 additions & 46 deletions adhocracy4/maps_react/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import json

from django.conf import settings
from django.urls import reverse
from django.utils.html import format_html


def get_map_settings(**kwargs):
Expand All @@ -29,45 +25,3 @@ def get_map_settings(**kwargs):

# Filter out the keys that have a value of ""
return {key: val for key, val in map_settings.items() if val != ""}


def react_tag_factory(tag_name, api_url_name=None):
"""
:param tag_name: The name of the template tag.
:param api_url_name: The name of the API URL (optional).
:return: A formatted HTML string containing the React tag with required props.
This method creates a function that generates a React tag with the given name and
attributes. It takes the following parameters:
If the `api_url_name` parameter is provided, the `module` parameter must also be
provided. Otherwise, a `ValueError` is raised.
The function generated by this method takes a variable number of keyword arguments,
which are used to populate the attributes of the React tag. If the `module`
parameter is provided and the keyword argument "polygon" is not included, the
function automatically adds the "polygon" attribute using the `polygon` setting
from the `module` object.
"""

def func(**kwargs):
module = kwargs.pop("module", None)
if module and "polygon" not in kwargs:
kwargs["polygon"] = module.settings_instance.polygon

attributes = {"map": get_map_settings(**kwargs)}
if api_url_name:
if not module:
raise ValueError("Module must be provided if api_url_name is provided")
attributes["apiUrl"] = reverse(
api_url_name, kwargs={"module_pk": module.pk}
)

return format_html(
f'<div data-mb-widget="{tag_name}" data-attributes="{{attributes}}"></div>',
attributes=json.dumps(attributes),
)

# set the correct name on the function
func.__name__ = tag_name
return func
8 changes: 7 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ const config = {
],
transform: {
'^.+\\.[t|j]sx?$': 'babel-jest'
}
},
transformIgnorePatterns: [
'node_modules/(?!(@?react-leaflet)/)'
],
setupFiles: [
'<rootDir>/setupTests.js'
]
}

module.exports = config
3 changes: 3 additions & 0 deletions setupTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
if (typeof window.URL.createObjectURL === 'undefined') {
window.URL.createObjectURL = () => {}
}

0 comments on commit 14a4a74

Please sign in to comment.