/* 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("", () => { 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)( , ); 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 }) => (
Hello, world
); 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(); }); }); });