278 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			278 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			TypeScript
		
	
	
/*
 | 
						|
Copyright 2022 The Matrix.org Foundation C.I.C.
 | 
						|
 | 
						|
Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
you may not use this file except in compliance with the License.
 | 
						|
You may obtain a copy of the License at
 | 
						|
 | 
						|
    http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
Unless required by applicable law or agreed to in writing, software
 | 
						|
distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
See the License for the specific language governing permissions and
 | 
						|
limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
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();
 | 
						|
        });
 | 
						|
    });
 | 
						|
});
 |