riot-web/test/components/views/location/Map-test.tsx

270 lines
10 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { act, fireEvent, getByTestId, render } from "@testing-library/react";
import * as maplibregl from "maplibre-gl";
import { ClientEvent } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { mocked } from "jest-mock";
import Map from "../../../../src/components/views/location/Map";
import { getMockClientWithEventEmitter, getMockGeolocationPositionError } from "../../../test-utils";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import { TILE_SERVER_WK_KEY } from "../../../../src/utils/WellKnownUtils";
import Modal from "../../../../src/Modal";
import ErrorDialog from "../../../../src/components/views/dialogs/ErrorDialog";
describe("<Map />", () => {
const defaultProps = {
centerGeoUri: "geo:52,41",
id: "test-123",
onError: jest.fn(),
onClick: jest.fn(),
};
const matrixClient = getMockClientWithEventEmitter({
getClientWellKnown: jest.fn().mockReturnValue({
[TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" },
}),
});
const getComponent = (props = {}, renderingFn?: any) =>
(renderingFn ?? render)(
<MatrixClientContext.Provider value={matrixClient}>
<Map {...defaultProps} {...props} />
</MatrixClientContext.Provider>,
);
beforeEach(() => {
jest.clearAllMocks();
matrixClient.getClientWellKnown.mockReturnValue({
[TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" },
});
jest.spyOn(logger, "error").mockRestore();
mocked(maplibregl.GeolocateControl).mockClear();
});
afterEach(() => {
jest.spyOn(logger, "error").mockRestore();
});
const mapOptions = { container: {} as unknown as HTMLElement, style: "" };
const mockMap = new maplibregl.Map(mapOptions);
it("renders", () => {
const { container } = getComponent();
expect(container.firstChild).not.toBeNull();
});
describe("onClientWellKnown emits", () => {
it("updates map style when style url is truthy", () => {
getComponent();
act(() => {
matrixClient.emit(ClientEvent.ClientWellKnown, {
[TILE_SERVER_WK_KEY.name]: { map_style_url: "new.maps.com" },
});
});
expect(mockMap.setStyle).toHaveBeenCalledWith("new.maps.com");
});
it("does not update map style when style url is truthy", () => {
getComponent();
act(() => {
matrixClient.emit(ClientEvent.ClientWellKnown, {
[TILE_SERVER_WK_KEY.name]: { map_style_url: undefined },
});
});
expect(mockMap.setStyle).not.toHaveBeenCalledWith();
});
});
describe("map centering", () => {
it("does not try to center when no center uri provided", () => {
getComponent({ centerGeoUri: null });
expect(mockMap.setCenter).not.toHaveBeenCalled();
});
it("sets map center to centerGeoUri", () => {
getComponent({ centerGeoUri: "geo:51,42" });
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 42 });
});
it("handles invalid centerGeoUri", () => {
const logSpy = jest.spyOn(logger, "error").mockImplementation();
getComponent({ centerGeoUri: "123 Sesame Street" });
expect(mockMap.setCenter).not.toHaveBeenCalled();
expect(logSpy).toHaveBeenCalledWith("Could not set map center");
});
it("updates map center when centerGeoUri prop changes", () => {
const { rerender } = getComponent({ centerGeoUri: "geo:51,42" });
getComponent({ centerGeoUri: "geo:53,45" }, rerender);
getComponent({ centerGeoUri: "geo:56,47" }, rerender);
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 42 });
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 53, lon: 45 });
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 56, lon: 47 });
});
});
describe("map bounds", () => {
it("does not try to fit map bounds when no bounds provided", () => {
getComponent({ bounds: null });
expect(mockMap.fitBounds).not.toHaveBeenCalled();
});
it("fits map to bounds", () => {
const bounds = { north: 51, south: 50, east: 42, west: 41 };
getComponent({ bounds });
expect(mockMap.fitBounds).toHaveBeenCalledWith(
new maplibregl.LngLatBounds([bounds.west, bounds.south], [bounds.east, bounds.north]),
{ padding: 100, maxZoom: 15 },
);
});
it("handles invalid bounds", () => {
const logSpy = jest.spyOn(logger, "error").mockImplementation();
const bounds = { north: "a", south: "b", east: 42, west: 41 };
getComponent({ bounds });
expect(mockMap.fitBounds).not.toHaveBeenCalled();
expect(logSpy).toHaveBeenCalledWith("Invalid map bounds");
});
it("updates map bounds when bounds prop changes", () => {
const { rerender } = getComponent({ centerGeoUri: "geo:51,42" });
const bounds = { north: 51, south: 50, east: 42, west: 41 };
const bounds2 = { north: 53, south: 51, east: 45, west: 44 };
getComponent({ bounds }, rerender);
getComponent({ bounds: bounds2 }, rerender);
expect(mockMap.fitBounds).toHaveBeenCalledTimes(2);
});
});
describe("children", () => {
it("renders without children", () => {
const component = getComponent({ children: null });
// no error
expect(component).toBeTruthy();
});
it("renders children with map renderProp", () => {
const children = ({ map }: { map: maplibregl.Map }) => (
<div data-testid="test-child" data-map={map}>
Hello, world
</div>
);
const { container } = getComponent({ children });
// passes the map instance to the react children
expect(getByTestId(container, "test-child").dataset.map).toBeTruthy();
});
});
describe("onClick", () => {
it("eats clicks to maplibre attribution button", () => {
const onClick = jest.fn();
getComponent({ onClick });
act(() => {
// this is added to the dom by maplibregl
// which is mocked
// just fake the target
const fakeEl = document.createElement("div");
fakeEl.className = "maplibregl-ctrl-attrib-button";
fireEvent.click(fakeEl);
});
expect(onClick).not.toHaveBeenCalled();
});
it("calls onClick", () => {
const onClick = jest.fn();
const { container } = getComponent({ onClick });
act(() => {
fireEvent.click(container.firstChild);
});
expect(onClick).toHaveBeenCalled();
});
});
describe("geolocate", () => {
it("does not add a geolocate control when allowGeolocate is falsy", () => {
getComponent({ allowGeolocate: false });
// didn't create a geolocation control
expect(maplibregl.GeolocateControl).not.toHaveBeenCalled();
});
it("creates a geolocate control and adds it to the map when allowGeolocate is truthy", () => {
getComponent({ allowGeolocate: true });
// didn't create a geolocation control
expect(maplibregl.GeolocateControl).toHaveBeenCalledWith({
positionOptions: {
enableHighAccuracy: true,
},
trackUserLocation: false,
});
// mocked maplibregl shares mock for each mocked instance
// so we can assert the geolocate control was added using this static mock
const mockGeolocate = new maplibregl.GeolocateControl({});
expect(mockMap.addControl).toHaveBeenCalledWith(mockGeolocate);
});
it("logs and opens a dialog on a geolocation error", () => {
const mockGeolocate = new maplibregl.GeolocateControl({});
jest.spyOn(mockGeolocate, "on");
const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {});
jest.spyOn(Modal, "createDialog");
const { rerender } = getComponent({ allowGeolocate: true });
// wait for component to settle
getComponent({ allowGeolocate: true }, rerender);
expect(mockGeolocate.on).toHaveBeenCalledWith("error", expect.any(Function));
const error = getMockGeolocationPositionError(1, "Test");
// @ts-ignore pretend to have geolocate emit an error
mockGeolocate.emit("error", error);
expect(logSpy).toHaveBeenCalledWith("Could not fetch location", error);
expect(Modal.createDialog).toHaveBeenCalledWith(ErrorDialog, {
title: "Could not fetch location",
description:
"Element was denied permission to fetch your location. Please allow location access in your browser settings.",
});
});
it("unsubscribes from geolocate errors on destroy", () => {
const mockGeolocate = new maplibregl.GeolocateControl({});
jest.spyOn(mockGeolocate, "on");
jest.spyOn(mockGeolocate, "off");
jest.spyOn(Modal, "createDialog");
const { unmount } = getComponent({ allowGeolocate: true });
expect(mockGeolocate.on).toHaveBeenCalled();
unmount();
expect(mockGeolocate.off).toHaveBeenCalled();
});
});
});