/*
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("", () => {
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(, {
wrapper: ({ children }) => (
{children}
),
});
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();
});
});
});
});