327 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			327 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
/*
 | 
						|
Copyright 2024 New Vector Ltd.
 | 
						|
Copyright 2021 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, render, RenderResult } from "@testing-library/react";
 | 
						|
import * as maplibregl from "maplibre-gl";
 | 
						|
import { RoomMember, MatrixClient } from "matrix-js-sdk/src/matrix";
 | 
						|
import { mocked } from "jest-mock";
 | 
						|
import { logger } from "matrix-js-sdk/src/logger";
 | 
						|
 | 
						|
import LocationPicker from "../../../../src/components/views/location/LocationPicker";
 | 
						|
import { LocationShareType } from "../../../../src/components/views/location/shareLocation";
 | 
						|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
 | 
						|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
 | 
						|
import { getMockClientWithEventEmitter, mockPlatformPeg } from "../../../test-utils";
 | 
						|
import { findMapStyleUrl, LocationShareError } from "../../../../src/utils/location";
 | 
						|
 | 
						|
jest.mock("../../../../src/utils/location/findMapStyleUrl", () => ({
 | 
						|
    findMapStyleUrl: jest.fn().mockReturnValue("tileserver.com"),
 | 
						|
}));
 | 
						|
 | 
						|
// dropdown uses this
 | 
						|
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });
 | 
						|
 | 
						|
describe("LocationPicker", () => {
 | 
						|
    describe("<LocationPicker />", () => {
 | 
						|
        const roomId = "!room:server.org";
 | 
						|
        const userId = "@user:server.org";
 | 
						|
        const sender = new RoomMember(roomId, userId);
 | 
						|
        const defaultProps = {
 | 
						|
            sender,
 | 
						|
            shareType: LocationShareType.Own,
 | 
						|
            onChoose: jest.fn(),
 | 
						|
            onFinished: jest.fn(),
 | 
						|
        };
 | 
						|
        const mockClient = getMockClientWithEventEmitter({
 | 
						|
            isGuest: jest.fn(),
 | 
						|
            getClientWellKnown: jest.fn(),
 | 
						|
        });
 | 
						|
        const getComponent = (props = {}): RenderResult =>
 | 
						|
            render(<LocationPicker {...defaultProps} {...props} />, {
 | 
						|
                wrapper: ({ children }) => (
 | 
						|
                    <MatrixClientContext.Provider value={mockClient}>{children}</MatrixClientContext.Provider>
 | 
						|
                ),
 | 
						|
            });
 | 
						|
 | 
						|
        const mapOptions = { container: {} as unknown as HTMLElement, style: "" };
 | 
						|
        const mockMap = new maplibregl.Map(mapOptions);
 | 
						|
        const mockGeolocate = new maplibregl.GeolocateControl({});
 | 
						|
        const mockMarker = new maplibregl.Marker();
 | 
						|
 | 
						|
        const mockGeolocationPosition = {
 | 
						|
            coords: {
 | 
						|
                latitude: 43.2,
 | 
						|
                longitude: 12.4,
 | 
						|
                altitude: 12.3,
 | 
						|
                accuracy: 21,
 | 
						|
            },
 | 
						|
            timestamp: 123,
 | 
						|
        };
 | 
						|
        const mockClickEvent = {
 | 
						|
            lngLat: {
 | 
						|
                lat: 43.2,
 | 
						|
                lng: 12.4,
 | 
						|
            },
 | 
						|
        };
 | 
						|
 | 
						|
        beforeEach(() => {
 | 
						|
            jest.spyOn(logger, "error").mockRestore();
 | 
						|
            jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient as unknown as MatrixClient);
 | 
						|
            jest.clearAllMocks();
 | 
						|
            mocked(mockMap).addControl.mockReset();
 | 
						|
            mocked(findMapStyleUrl).mockReturnValue("tileserver.com");
 | 
						|
        });
 | 
						|
 | 
						|
        it("displays error when map emits an error", () => {
 | 
						|
            // suppress expected error log
 | 
						|
            jest.spyOn(logger, "error").mockImplementation(() => {});
 | 
						|
            const { getByTestId, getByText } = getComponent();
 | 
						|
 | 
						|
            act(() => {
 | 
						|
                // @ts-ignore
 | 
						|
                mocked(mockMap).emit("error", { error: "Something went wrong" });
 | 
						|
            });
 | 
						|
 | 
						|
            expect(getByTestId("map-rendering-error")).toBeInTheDocument();
 | 
						|
            expect(
 | 
						|
                getByText(
 | 
						|
                    "This homeserver is not configured correctly to display maps, " +
 | 
						|
                        "or the configured map server may be unreachable.",
 | 
						|
                ),
 | 
						|
            ).toBeInTheDocument();
 | 
						|
        });
 | 
						|
 | 
						|
        it("displays error when map display is not configured properly", () => {
 | 
						|
            // suppress expected error log
 | 
						|
            jest.spyOn(logger, "error").mockImplementation(() => {});
 | 
						|
            mocked(findMapStyleUrl).mockImplementation(() => {
 | 
						|
                throw new Error(LocationShareError.MapStyleUrlNotConfigured);
 | 
						|
            });
 | 
						|
 | 
						|
            const { getByText } = getComponent();
 | 
						|
 | 
						|
            expect(getByText("This homeserver is not configured to display maps.")).toBeInTheDocument();
 | 
						|
        });
 | 
						|
 | 
						|
        it("displays error when WebGl is not enabled", () => {
 | 
						|
            // suppress expected error log
 | 
						|
            jest.spyOn(logger, "error").mockImplementation(() => {});
 | 
						|
            mocked(findMapStyleUrl).mockImplementation(() => {
 | 
						|
                throw new Error("Failed to initialize WebGL");
 | 
						|
            });
 | 
						|
 | 
						|
            const { getByText } = getComponent();
 | 
						|
 | 
						|
            expect(
 | 
						|
                getByText("WebGL is required to display maps, please enable it in your browser settings."),
 | 
						|
            ).toBeInTheDocument();
 | 
						|
        });
 | 
						|
 | 
						|
        it("displays error when map setup throws", () => {
 | 
						|
            // suppress expected error log
 | 
						|
            jest.spyOn(logger, "error").mockImplementation(() => {});
 | 
						|
 | 
						|
            // throw an error
 | 
						|
            mocked(mockMap).addControl.mockImplementation(() => {
 | 
						|
                throw new Error("oups");
 | 
						|
            });
 | 
						|
 | 
						|
            const { getByText } = getComponent();
 | 
						|
 | 
						|
            expect(
 | 
						|
                getByText(
 | 
						|
                    "This homeserver is not configured correctly to display maps, " +
 | 
						|
                        "or the configured map server may be unreachable.",
 | 
						|
                ),
 | 
						|
            ).toBeInTheDocument();
 | 
						|
        });
 | 
						|
 | 
						|
        it("initiates map with geolocation", () => {
 | 
						|
            getComponent();
 | 
						|
 | 
						|
            expect(mockMap.addControl).toHaveBeenCalledWith(mockGeolocate);
 | 
						|
            act(() => {
 | 
						|
                // @ts-ignore
 | 
						|
                mocked(mockMap).emit("load");
 | 
						|
            });
 | 
						|
 | 
						|
            expect(mockGeolocate.trigger).toHaveBeenCalled();
 | 
						|
        });
 | 
						|
 | 
						|
        const testUserLocationShareTypes = (shareType: LocationShareType.Own | LocationShareType.Live) => {
 | 
						|
            describe("user location behaviours", () => {
 | 
						|
                it("closes and displays error when geolocation errors", () => {
 | 
						|
                    // suppress expected error log
 | 
						|
                    jest.spyOn(logger, "error").mockImplementation(() => {});
 | 
						|
                    const onFinished = jest.fn();
 | 
						|
                    getComponent({ onFinished, shareType });
 | 
						|
 | 
						|
                    expect(mockMap.addControl).toHaveBeenCalledWith(mockGeolocate);
 | 
						|
                    act(() => {
 | 
						|
                        // @ts-ignore
 | 
						|
                        mockMap.emit("load");
 | 
						|
                        // @ts-ignore
 | 
						|
                        mockGeolocate.emit("error", {});
 | 
						|
                    });
 | 
						|
 | 
						|
                    // dialog is closed on error
 | 
						|
                    expect(onFinished).toHaveBeenCalled();
 | 
						|
                });
 | 
						|
 | 
						|
                it("sets position on geolocate event", () => {
 | 
						|
                    const { container, getByTestId } = getComponent({ shareType });
 | 
						|
                    act(() => {
 | 
						|
                        // @ts-ignore
 | 
						|
                        mocked(mockGeolocate).emit("geolocate", mockGeolocationPosition);
 | 
						|
                    });
 | 
						|
 | 
						|
                    // marker added
 | 
						|
                    expect(maplibregl.Marker).toHaveBeenCalled();
 | 
						|
                    expect(mockMarker.setLngLat).toHaveBeenCalledWith(new maplibregl.LngLat(12.4, 43.2));
 | 
						|
                    // submit button is enabled when position is truthy
 | 
						|
                    expect(getByTestId("location-picker-submit-button")).not.toBeDisabled();
 | 
						|
                    expect(container.querySelector(".mx_BaseAvatar")).toBeInTheDocument();
 | 
						|
                });
 | 
						|
 | 
						|
                it("disables submit button until geolocation completes", () => {
 | 
						|
                    const onChoose = jest.fn();
 | 
						|
                    const { getByTestId } = getComponent({ shareType, onChoose });
 | 
						|
 | 
						|
                    // button is disabled
 | 
						|
                    expect(getByTestId("location-picker-submit-button")).toBeDisabled();
 | 
						|
                    fireEvent.click(getByTestId("location-picker-submit-button"));
 | 
						|
                    // nothing happens on button click
 | 
						|
                    expect(onChoose).not.toHaveBeenCalled();
 | 
						|
 | 
						|
                    act(() => {
 | 
						|
                        // @ts-ignore
 | 
						|
                        mocked(mockGeolocate).emit("geolocate", mockGeolocationPosition);
 | 
						|
                    });
 | 
						|
 | 
						|
                    // submit button is enabled when position is truthy
 | 
						|
                    expect(getByTestId("location-picker-submit-button")).not.toBeDisabled();
 | 
						|
                });
 | 
						|
 | 
						|
                it("submits location", () => {
 | 
						|
                    const onChoose = jest.fn();
 | 
						|
                    const { getByTestId } = getComponent({ onChoose, shareType });
 | 
						|
                    act(() => {
 | 
						|
                        // @ts-ignore
 | 
						|
                        mocked(mockGeolocate).emit("geolocate", mockGeolocationPosition);
 | 
						|
                        // make sure button is enabled
 | 
						|
                    });
 | 
						|
 | 
						|
                    fireEvent.click(getByTestId("location-picker-submit-button"));
 | 
						|
                    // content of this call is tested in LocationShareMenu-test
 | 
						|
                    expect(onChoose).toHaveBeenCalled();
 | 
						|
                });
 | 
						|
            });
 | 
						|
        };
 | 
						|
 | 
						|
        describe("for Own location share type", () => {
 | 
						|
            testUserLocationShareTypes(LocationShareType.Own);
 | 
						|
        });
 | 
						|
 | 
						|
        describe("for Live location share type", () => {
 | 
						|
            const shareType = LocationShareType.Live;
 | 
						|
            testUserLocationShareTypes(shareType);
 | 
						|
 | 
						|
            it("renders live duration dropdown with default option", () => {
 | 
						|
                const { getByText } = getComponent({ shareType });
 | 
						|
                expect(getByText("Share for 15m")).toBeInTheDocument();
 | 
						|
            });
 | 
						|
 | 
						|
            it("updates selected duration", () => {
 | 
						|
                const { getByText, getByLabelText } = getComponent({ shareType });
 | 
						|
 | 
						|
                // open dropdown
 | 
						|
                fireEvent.click(getByLabelText("Share for 15m"));
 | 
						|
 | 
						|
                fireEvent.click(getByText("Share for 1h"));
 | 
						|
 | 
						|
                // value updated
 | 
						|
                expect(getByText("Share for 1h")).toMatchSnapshot();
 | 
						|
            });
 | 
						|
        });
 | 
						|
 | 
						|
        describe("for Pin drop location share type", () => {
 | 
						|
            const shareType = LocationShareType.Pin;
 | 
						|
            it("initiates map with geolocation", () => {
 | 
						|
                getComponent({ shareType });
 | 
						|
 | 
						|
                expect(mockMap.addControl).toHaveBeenCalledWith(mockGeolocate);
 | 
						|
                act(() => {
 | 
						|
                    // @ts-ignore
 | 
						|
                    mocked(mockMap).emit("load");
 | 
						|
                });
 | 
						|
 | 
						|
                expect(mockGeolocate.trigger).toHaveBeenCalled();
 | 
						|
            });
 | 
						|
 | 
						|
            it("removes geolocation control on geolocation error", () => {
 | 
						|
                // suppress expected error log
 | 
						|
                jest.spyOn(logger, "error").mockImplementation(() => {});
 | 
						|
                const onFinished = jest.fn();
 | 
						|
                getComponent({ onFinished, shareType });
 | 
						|
                act(() => {
 | 
						|
                    // @ts-ignore
 | 
						|
                    mockMap.emit("load");
 | 
						|
                    // @ts-ignore
 | 
						|
                    mockGeolocate.emit("error", {});
 | 
						|
                });
 | 
						|
 | 
						|
                expect(mockMap.removeControl).toHaveBeenCalledWith(mockGeolocate);
 | 
						|
                // dialog is not closed
 | 
						|
                expect(onFinished).not.toHaveBeenCalled();
 | 
						|
            });
 | 
						|
 | 
						|
            it("does not set position on geolocate event", () => {
 | 
						|
                mocked(maplibregl.Marker).mockClear();
 | 
						|
                const { container } = getComponent({ shareType });
 | 
						|
                act(() => {
 | 
						|
                    // @ts-ignore
 | 
						|
                    mocked(mockGeolocate).emit("geolocate", mockGeolocationPosition);
 | 
						|
                });
 | 
						|
 | 
						|
                // marker not added
 | 
						|
                expect(container.querySelector("mx_Marker")).not.toBeInTheDocument();
 | 
						|
            });
 | 
						|
 | 
						|
            it("sets position on click event", () => {
 | 
						|
                const { container } = getComponent({ shareType });
 | 
						|
                act(() => {
 | 
						|
                    // @ts-ignore
 | 
						|
                    mocked(mockMap).emit("click", mockClickEvent);
 | 
						|
                });
 | 
						|
 | 
						|
                // marker added
 | 
						|
                expect(maplibregl.Marker).toHaveBeenCalled();
 | 
						|
                expect(mockMarker.setLngLat).toHaveBeenCalledWith(new maplibregl.LngLat(12.4, 43.2));
 | 
						|
 | 
						|
                // marker is set, icon not avatar
 | 
						|
                expect(container.querySelector(".mx_Marker_icon")).toBeInTheDocument();
 | 
						|
            });
 | 
						|
 | 
						|
            it("submits location", () => {
 | 
						|
                const onChoose = jest.fn();
 | 
						|
                const { getByTestId } = getComponent({ onChoose, shareType });
 | 
						|
                act(() => {
 | 
						|
                    // @ts-ignore
 | 
						|
                    mocked(mockMap).emit("click", mockClickEvent);
 | 
						|
                });
 | 
						|
 | 
						|
                fireEvent.click(getByTestId("location-picker-submit-button"));
 | 
						|
 | 
						|
                // content of this call is tested in LocationShareMenu-test
 | 
						|
                expect(onChoose).toHaveBeenCalled();
 | 
						|
            });
 | 
						|
        });
 | 
						|
    });
 | 
						|
});
 |