I am currently trying to test a Component wrapped with third party library which is react-map-gl marker.
It keep throwing error like project undefined. so I looked into the react-map-gl code.
let [x, y] = this._context.viewport.project([longitude, latitude]);
Then, I went to see how react-map-gl test its own stuff. Then, I tried to miminc how they do.
But it just timeout no matter how much time I put. Am I missing something?
import {_MapContext as MapContext} from 'react-map-gl'
import WebMercatorViewport from 'viewport-mercator-project'
const mockStaticContext = {
viewport: new WebMercatorViewport({
width: 800,
height: 600,
longitude: -122.58,
latitude: 37.74,
zoom: 14
})
};
const mockInteractiveContext = Object.assign({}, mockStaticContext, {
eventManager: {
on: sinon.spy(),
off: sinon.spy(),
watch: sinon.spy()
}
})
describe('<ListingMarker />', () => {
it('should not explode', async (t) => {
render(
<MapContext.Provider value={mockInteractiveContext}>
<AppMarker cluster={marker} />
</MapContext.Provider>
)
}, 30000)
})
I tried to follow their way of testing.
https://github.com/visgl/react-map-gl/blob/master/test/src/components/marker.spec.js
Related
I am trying to implement openlayers popup in react.
It is implemented in plain js here: https://openlayers.org/en/latest/examples/popup.html
Here is my code:
import {createRef, useEffect, useRef, useState} from 'react';
import './Map.css';
import Map from '../node_modules/ol/Map.js';
import Overlay from '../node_modules/ol/Overlay.js';
import TileLayer from '../node_modules/ol/layer/Tile.js';
import View from '../node_modules/ol/View.js';
import XYZ from '../node_modules/ol/source/XYZ.js';
import {toLonLat} from '../node_modules/ol/proj.js';
import {toStringHDMS} from '../node_modules/ol/coordinate.js';
const MapExample = () => {
const [popupContent, setPopupContent] = useState('');
const containerRef = createRef();
const contentRef = createRef();
const closerRef = createRef();
const key = 'CvOgKFhRDDHIDOwAPhLI';
const overlay = new Overlay({
element: containerRef.current,
autoPan: {
animation: {
duration: 250,
},
},
});
const handleCloser = () => {
overlay.setPosition(undefined);
closerRef.current.blur();
return false;
};
useEffect(() => {
const map = new Map({
layers: [
new TileLayer({
source: new XYZ({
url: 'https://api.maptiler.com/maps/streets/{z}/{x}/{y}.png?key=' + key,
tileSize: 512,
}),
}),
],
overlays: [overlay],
target: 'map',
view: new View({
center: [0, 0],
zoom: 2,
}),
});
map.on('singleclick', (evt) => {
const coordinate = evt.coordinate;
const hdms = toStringHDMS(toLonLat(coordinate));
setPopupContent('You clicked here: ' + hdms)
overlay.setPosition(coordinate);
});
}, [])
return (
<div>
<div id="map" className="map"></div>
<div ref={containerRef} className="ol-popup">
<a ref={closerRef} href="#" className="ol-popup-closer" onClick={handleCloser}></a>
<div ref={contentRef}>{popupContent}</div>
</div>
</div>
)
}
export default MapExample;
I've got an issue with offset width:
picture with an issue: Cannot read properties of null (reading 'offsetWidth') The problem is popup appears on the left bottom corner, not in the clicked place of the map.
How I understand this bug appears, because popup's properties are null. I tried to fix this bug, but could not find a working solution. Will be really grateful for any help )))
There are a number of React lifecycle issues in your code. Do not use createRef in functional components as it will create a new reference at every render - while the useEffect code will run only once. This means that the overlay will lose its connection to the map - especially since you create a new Overlay at each render.
Also I strongly advise you to not use useEffect to create Openlayers components - the reasons are very subtle - but the main problem is that your components won't be reusable or even rerendable.
As the author of rlayers I can tell you that proper wrapping of Openlayers components is not easy - you must manually manage the components' lifecycle. This works best with class-based components on which you should override all lifecycle methods.
import MapView from "#arcgis/core/views/MapView";
useEffect(() => {
const map = new Map({
basemap: "arcgis-topographic", // Basemap layer service
});
const view = new MapView({
map: map,
center: [longitude, latitude], // Longitude, latitude
zoom: 11, // Zoom level
container: container.current, // Div element
popups: false, // Popups
});
}, []);
useEffect(() => {
//I need to use that MapView here
}, []);
I've save the view variable to the state/ref. But didnt work. How to access the instance of MapView as global variable
Hi #Dostonbek,
As I explain in the above comment. You could declare the view variable globally. Either, you could use ref hook To make the MapView instance available to both useEffect hooks. Also, you can use useState hook as wall.
import { useRef } from 'react';
import MapView from "#arcgis/core/views/MapView";
const mapViewRef = useRef();
useEffect(() => {
const map = new Map({
basemap: "arcgis-topographic", // Basemap layer service
});
mapViewRef.current = new MapView({
map: map,
center: [longitude, latitude], // Longitude, latitude
zoom: 11, // Zoom level
container: container.current, // Div element
popups: false, // Popups
});
}, []);
useEffect(() => {
const mapView = mapViewRef.current;
// Use the mapView instance here
}, []);
I am trying to implement OpenLayers6 ("ol": "^6.14.1") into a ReactJS project, but all the documentation is created in normal JS files and I can't find any good examples or tutorials with functional components and OpenLayers6.
I have troubles figuring out how to implement the majority of things, because the docs seem to ignore the lifecycle of React.
What I've managed to do until now is to add a marker and a popup right above the marker; to close the popup and delete the marker by deleting the whole vector layer(which seems overkill).
import { useState, useEffect, useRef } from 'react';
// import ModalUI from '../UI/ModalUI';
import classes from './MapUI.module.css';
import { drawerActions } from '../../store/drawer-slice';
import 'ol/ol.css';
import { Map, View, Overlay, Feature } from 'ol';
import Point from 'ol/geom/Point';
import { Vector as VectorLayer } from 'ol/layer';
import VectorSource from 'ol/source/Vector';
import { fromLonLat, toLonLat } from 'ol/proj';
import { toStringHDMS } from 'ol/coordinate';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import PopUp from './PopUp';
import { useDispatch } from 'react-redux';
export default function MapUI() {
const mapRef = useRef();
const popup = useRef();
const [coordinates, setCoordinates] = useState('');
const [newMarker, setNewMarker] = useState(
new Feature({
geometry: new Point([[]]),
name: '',
})
);
const [newMarkersLayer, setNewMarkersLayer] = useState(
new VectorLayer({
properties: { name: 'newMarkers' },
source: new VectorSource({
features: [newMarker],
}),
})
);
const closePopup = () => {
map.getOverlayById('map-popup').setPosition(undefined);
map.removeLayer(newMarkersLayer);
};
const [map] = useState(
new Map({
target: '',
layers: [
new TileLayer({
source: new OSM(),
}),
new VectorLayer({
properties: { name: 'existingMarkers' },
source: new VectorSource({
// features: [marker],
}),
}),
],
view: new View({
center: fromLonLat([26.08, 44.46]),
zoom: 15,
minZoom: 10,
maxZoom: 20,
}),
})
);
useEffect(() => {
const overlay = new Overlay({
element: popup.current,
id: 'map-popup',
autoPan: {
animation: {
duration: 250,
},
},
});
// console.log('useEffect in MapUI.jsx');
map.addOverlay(overlay);
map.setTarget(mapRef.current);
map.on('singleclick', function (evt) {
map.addLayer(newMarkersLayer);
newMarker.getGeometry().setCoordinates(evt.coordinate);
setCoordinates(toStringHDMS(toLonLat(evt.coordinate)));
overlay.setPosition(evt.coordinate);
});
}, [map]);
return (
<>
<div
style={{ height: '100%', width: '100%' }}
ref={mapRef}
className='map-container'
/>
<div id='map-popup' className={classes['ol-popup']} ref={popup}>
<PopUp coordinates={coordinates} closePopup={closePopup} />
</div>
</>
);
}
The project in the end will have an array of markers that will be requested from a back-end and will populate the given map while also keeping the ability to add new markers to the map (and to the back-end).
The general issue that I face is with how all ol objects are used in the documentation. Everything is just created in a file using const and then operated upon.
But in React I have to use useEffect() and useState() and can't just create dependencies or manipulate state however the docs say.
I am looking for some guidelines on how to properly use OpenLayers6 in React. On this note I have some questions:
How can I remove a marker without removing the whole layer ?
How can I make a marker stay on the map ?
How can I render an array or markers on the map ?
Is it correct the way I use useState() to create the initial map ?
Is it correct the way I use useState() to keep the marker and the VectorLayer on which the marker will be placed ?
Try using this library, Rlayers
this was very helpfull for me to combine with another gis library, like turf and d3
How can I remove a marker without removing the whole layer ?
You can use react-query as hooks (useMutation) fuction to call inside useCallback
How can I make a marker stay on the map ?
What are you meaning about stay? is like show and not hide? just get state and put that's value into true
How can I render an array or markers on the map ?
again use react-query
Is it correct the way I use useState() to create the initial map ?
Yes if you have another map on your application
Is it correct the way I use useState() to keep the marker and the VectorLayer on which the marker will be placed ?
Yes
As you know, OpenLayers uses an imperative API where you add and remove features to layers, etc., while in React land, we generally do things declaratively. You'll need some imperative glue code that takes your React state and mutates the OpenLayers state to match what your React state is - that's what React itself does when it reconciles the DOM.
You can do that in a couple of different ways:
useEffects to do what useEffect does: imperative side effects (updating OL state) based on declarative inputs (React state); described below
The other option, used by e.g. react-leaflet, is that you describe the whole Leaflet state as React elements, and the library reconciles the Leaflet state based on that. It's a lot more work, but feels more React-y.
Here's a pair of custom hooks that make things a bit easier, loosely based on your example. It's in TypeScript, but if you need plain JS, just remove the TypeScript specific things (by e.g. running this through the typescriptlang.org transpiler).
useOpenLayersMap is a hook that sets up an OL Map in a ref and returns it, and takes care that a given container renders the map.
useOpenLayersMarkerSource is a hook that receives a list of marker data (here quite a limited definition, but you could amend it as you like), returns a VectorSource and also takes care that modifications to the list of marker data are reflected into the VectorSource as new Features.
Now, all the MapUI component needs to do is use those two hooks with valid initial data; it can modify its markerData state and the changes get reflected into the map.
import React, { useRef, useState } from "react";
import "ol/ol.css";
import { Feature, Map, View } from "ol";
import Point from "ol/geom/Point";
import { Vector as VectorLayer } from "ol/layer";
import VectorSource from "ol/source/Vector";
import { fromLonLat } from "ol/proj";
import TileLayer from "ol/layer/Tile";
import OSM from "ol/source/OSM";
import { MapOptions } from "ol/PluggableMap";
interface MarkerDatum {
lat: number;
lon: number;
name: string;
}
function convertMarkerDataToFeatures(markerData: MarkerDatum[]) {
return markerData.map(
(md) =>
new Feature({
geometry: new Point(fromLonLat([md.lon, md.lat])),
name: md.name,
}),
);
}
const centerLat = 60.45242;
const centerLon = 22.27831;
function useOpenLayersMap(
mapDivRef: React.MutableRefObject<HTMLDivElement | null>,
getInitialOptions: () => Partial<MapOptions>,
) {
const mapRef = useRef<Map | null>(null);
React.useEffect(() => {
if (!mapRef.current) {
// markersSourceRef.current = new VectorSource();
mapRef.current = new Map({
target: mapDivRef.current ?? undefined,
...getInitialOptions(),
});
}
}, []);
React.useLayoutEffect(() => {
if (mapRef.current && mapDivRef.current) {
mapRef.current.setTarget(mapDivRef.current);
}
}, []);
return mapRef.current;
}
function useOpenLayersMarkerSource(markerData: MarkerDatum[]) {
const [markersSource] = useState(() => new VectorSource());
React.useEffect(() => {
// TODO: this would do better to only remove removed features,
// modify modified features and add new features
markersSource.clear(true);
markersSource.addFeatures(convertMarkerDataToFeatures(markerData));
}, [markerData]);
return markersSource;
}
export default function MapUI() {
const mapDivRef = useRef<HTMLDivElement>(null);
const [markerData, setMarkerData] = React.useState<MarkerDatum[]>([
{
lon: centerLon,
lat: centerLat,
name: "turku",
},
]);
const markerSource = useOpenLayersMarkerSource(markerData);
const map = useOpenLayersMap(mapDivRef, () => ({
layers: [
new TileLayer({
source: new OSM(),
}),
new VectorLayer({
properties: { name: "existingMarkers" },
source: markerSource,
}),
],
view: new View({
center: fromLonLat([centerLon, centerLat]),
zoom: 12,
minZoom: 12,
maxZoom: 20,
}),
}));
const addNewFeature = React.useCallback(() => {
setMarkerData((prevMarkerData) => [
...prevMarkerData,
{
lat: centerLat - 0.05 + Math.random() * 0.1,
lon: centerLon - 0.05 + Math.random() * 0.1,
name: "marker " + (0 | +new Date()).toString(36),
},
]);
}, []);
return (
<>
<button onClick={addNewFeature}>Add new feature</button>
<div
style={{ height: "800px", width: "800px" }}
ref={mapDivRef}
className="map-container"
/>
</>
);
}
I wrote a component built with react 17.0.2 that uses react-intersection-observer 9.1.0
import { useInView } from 'react-intersection-observer'
...
const [ref, inView] = useInView({
threshold: 0.99,
root: scrollRef.current,
delay: 250,
trackVisibility: true,
onChange: (inView: boolean) => {
onChildInView(index, inView)
}
})
to detect sliding behaviours inside or outside the viewport. And the component works fine.
I wrote some unit tests to make the component safer, using #testing-library/react 12.1.4 and #testing-library/jest-dom 5.16.3.
As soon as I test just the existence or visibility of the above component with the following code
describe('method render', () => {
test('renders correctly', () => {
render(
<MyComponent
props={...}
data-testid="component-id"
>
<div />
<div />
</MyComponent>
)
const componentNode = screen.getByTestId('component-id')
expect(componentNode).toBeInTheDocument()
expect(componentNode).toBeVisible()
})
})
the testing library complains with the message error.
ReferenceError: IntersectionObserver is not defined
I tried to fix it with this suggestion of mocking the library (as linked here) written at the top of the test
const intersectionObserverMock = () => ({
observe: () => null
})
declare global {
interface Window {
IntersectionObserver: typeof IntersectionObserver
}
}
window.IntersectionObserver = jest.fn().mockImplementation(intersectionObserverMock);
but it did not work due to
TypeError: observer.unobserve is not a function
Suggestions? Missing something?
To fix this issue I'd recommend using mockAllIsIntersecting from test-utils.js in react-intersection-observer. This function mocks the IntersectionObserver.
e.g.
import { mockAllIsIntersecting } from 'react-intersection-observer/test-utils';
describe('method render', () => {
test('renders correctly', () => {
render(
<MyComponent
props={...}
data-testid="component-id"
>
<div />
<div />
</MyComponent>
)
mockAllIsIntersecting(true)
const componentNode = screen.getByTestId('component-id')
expect(componentNode).toBeInTheDocument()
expect(componentNode).toBeVisible()
})
})
Doing a snapchot test of a component with several nested children components, one which holds a Echart (bar-chart). When rendering in browser, EchartElement is set to the chart dom element:
<div class="echarts-for-react " _echarts_instance_="ec_1551342959315" size-sensor-id="1" style="height: 360px; -webkit-tap-highlight-color: transparent; user-select: none; position: relative; background: transparent;">
...
</div>
But running the test, its null, which makes the test fail. Do I need to mock this somehow? Doesn't sound right...All the properties it is expecting are passed in correctly also in test mode...I checked...
Is there some configuration/setup I need to make Echarts work with Jest?
Turns out that my first answer didn't work for me at the end... I kept running the test and fixing whatever it was complaining about, but there was always something else...
So with some help from a colleague, the solution was to stop React Echarts to call Echarts by mocking the function triggering the rendering.
let spy: any
beforeAll(() => {
spy = jest.spyOn(echarts, 'getInstanceByDom').mockImplementation(() => {
return {
hideLoading: jest.fn(),
setOption: jest.fn(),
showLoading: jest.fn()
}
})
})
afterAll(() => {
spy.mockRestore()
})
it('Renders show correctly', () => {
const tree = renderer
.create(
<Show />
)
.toJSON()
expect(tree).toMatchSnapshot()
})
In our case we were not interested in actually testing the canvas rendering part, so that worked for us.
Yes, I had to mock the ref object:
function createNodeMock(element: any) {
return {
getSomething: jest.fn(() => 'pizza'),
setSomething: jest.fn(),
property: null,
}
}
it('Renders show correctly', () => {
const options = { createNodeMock }
const tree = renderer
.create(
<Show/>,
options
)
.toJSON()
expect(tree).toMatchSnapshot()
})
Every time I ran into an error running the test, I went to where the method or property was called, console logged, looked what the value was while in the browser and set the data in the mockData. Maybe there is a better way of doing this, but it got it done...