Merge matrix-react-sdk into element-web

Merge remote-tracking branch 'repomerge/t3chguy/repomerge' into t3chguy/repo-merge

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2024-10-15 14:57:26 +01:00
3265 changed files with 484599 additions and 699 deletions

View File

@@ -0,0 +1,62 @@
/*
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 { render, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import LiveDurationDropdown, {
DEFAULT_DURATION_MS,
} from "../../../../../src/components/views/location/LiveDurationDropdown";
import { mockPlatformPeg } from "../../../../test-utils";
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });
describe("<LiveDurationDropdown />", () => {
const defaultProps = {
timeout: DEFAULT_DURATION_MS,
onChange: jest.fn(),
};
const renderComponent = (props = {}) => render(<LiveDurationDropdown {...defaultProps} {...props} />);
const getOption = (duration: string) => screen.getByRole("option", { name: `Share for ${duration}` });
const getSelectedOption = (duration: string) => screen.getByRole("button", { name: `Share for ${duration}` });
const openDropdown = async () => {
await userEvent.click(screen.getByRole("button"));
};
it("renders timeout as selected option", () => {
renderComponent();
expect(getSelectedOption("15m")).toBeInTheDocument();
});
it("renders non-default timeout as selected option", () => {
const timeout = 1234567;
renderComponent({ timeout });
expect(getSelectedOption("21m")).toBeInTheDocument();
});
it("renders a dropdown option for a non-default timeout value", async () => {
const timeout = 1234567;
renderComponent({ timeout });
await openDropdown();
expect(getOption("21m")).toBeInTheDocument();
});
it("updates value on option selection", async () => {
const onChange = jest.fn();
renderComponent({ onChange });
const ONE_HOUR = 3600000;
await openDropdown();
await userEvent.click(getOption("1h"));
expect(onChange).toHaveBeenCalledWith(ONE_HOUR);
});
});

View File

@@ -0,0 +1,326 @@
/*
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 "jest-matrix-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();
});
});
});
});

View File

@@ -0,0 +1,465 @@
/*
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 { mocked } from "jest-mock";
import { RoomMember, RelationType, MatrixClient, M_ASSET, LocationAssetType } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { act, fireEvent, render, RenderResult } from "jest-matrix-react";
import * as maplibregl from "maplibre-gl";
import LocationShareMenu from "../../../../../src/components/views/location/LocationShareMenu";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { ChevronFace } from "../../../../../src/components/structures/ContextMenu";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { LocationShareType } from "../../../../../src/components/views/location/shareLocation";
import {
flushPromisesWithFakeTimers,
getMockClientWithEventEmitter,
mockClientMethodsUser,
setupAsyncStoreWithClient,
} from "../../../../test-utils";
import Modal from "../../../../../src/Modal";
import { DEFAULT_DURATION_MS } from "../../../../../src/components/views/location/LiveDurationDropdown";
import { OwnBeaconStore } from "../../../../../src/stores/OwnBeaconStore";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
import QuestionDialog from "../../../../../src/components/views/dialogs/QuestionDialog";
jest.useFakeTimers();
jest.mock("../../../../../src/utils/location/findMapStyleUrl", () => ({
findMapStyleUrl: jest.fn().mockReturnValue("test"),
}));
jest.mock("../../../../../src/settings/SettingsStore", () => ({
getValue: jest.fn(),
setValue: jest.fn(),
monitorSetting: jest.fn(),
watchSetting: jest.fn(),
unwatchSetting: jest.fn(),
}));
jest.mock("../../../../../src/stores/OwnProfileStore", () => ({
OwnProfileStore: {
instance: {
displayName: "Ernie",
getHttpAvatarUrl: jest.fn().mockReturnValue("image.com/img"),
},
},
}));
jest.mock("../../../../../src/Modal", () => ({
createDialog: jest.fn(),
on: jest.fn(),
off: jest.fn(),
ModalManagerEvent: { Opened: "opened" },
}));
describe("<LocationShareMenu />", () => {
const userId = "@ernie:server.org";
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
getClientWellKnown: jest.fn().mockResolvedValue({
map_style_url: "maps.com",
}),
sendMessage: jest.fn(),
unstable_createLiveBeacon: jest.fn().mockResolvedValue({ event_id: "1" }),
unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: "1" }),
getVisibleRooms: jest.fn().mockReturnValue([]),
});
const defaultProps = {
menuPosition: {
top: 1,
left: 1,
chevronFace: ChevronFace.Bottom,
},
onFinished: jest.fn(),
openMenu: jest.fn(),
roomId: "!room:server.org",
sender: new RoomMember("!room:server.org", userId),
};
const mockGeolocate = new maplibregl.GeolocateControl({});
jest.spyOn(mockGeolocate, "on");
const mapOptions = { container: {} as unknown as HTMLElement, style: "" };
const mockMap = new maplibregl.Map(mapOptions);
jest.spyOn(mockMap, "on");
const position = {
coords: {
latitude: -36.24484561954707,
longitude: 175.46884959563613,
accuracy: 10,
},
timestamp: 1646305006802,
type: "geolocate",
};
const makeOwnBeaconStore = async () => {
const store = OwnBeaconStore.instance;
await setupAsyncStoreWithClient(store, mockClient);
return store;
};
const getComponent = (props = {}): RenderResult =>
render(<LocationShareMenu {...defaultProps} {...props} />, {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={mockClient}>{children}</MatrixClientContext.Provider>
),
});
beforeEach(async () => {
jest.spyOn(logger, "error").mockRestore();
mocked(SettingsStore).getValue.mockReturnValue(false);
mockClient.sendMessage.mockClear();
mockClient.unstable_createLiveBeacon.mockClear().mockResolvedValue({ event_id: "1" });
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient as unknown as MatrixClient);
mocked(Modal).createDialog.mockClear();
jest.clearAllMocks();
await makeOwnBeaconStore();
});
const getBackButton = (getByLabelText: RenderResult["getByLabelText"]) => getByLabelText("Back");
const getCancelButton = (getByLabelText: RenderResult["getByLabelText"]) => getByLabelText("Close");
const setLocationGeolocate = () => {
// get the callback LocationShareMenu registered for geolocate
expect(mocked(mockGeolocate.on)).toHaveBeenCalledWith("geolocate", expect.any(Function));
const [, onGeolocateCallback] = mocked(mockGeolocate.on).mock.calls.find(([event]) => event === "geolocate")!;
// set the location
onGeolocateCallback(position);
};
const setLocationClick = () => {
// get the callback LocationShareMenu registered for geolocate
expect(mocked(mockMap.on)).toHaveBeenCalledWith("click", expect.any(Function));
const [, onMapClickCallback] = mocked(mockMap.on).mock.calls.find(([event]) => event === "click")!;
const event = {
lngLat: { lng: position.coords.longitude, lat: position.coords.latitude },
} as unknown as maplibregl.MapMouseEvent;
// set the location
onMapClickCallback(event);
};
const shareTypeLabels: Record<LocationShareType, string> = {
[LocationShareType.Own]: "My current location",
[LocationShareType.Live]: "My live location",
[LocationShareType.Pin]: "Drop a Pin",
};
const setShareType = (getByText: RenderResult["getByText"], shareType: LocationShareType) => {
fireEvent.click(getByText(shareTypeLabels[shareType]));
};
describe("when only Own share type is enabled", () => {
beforeEach(() => enableSettings([]));
it("renders own and live location options", () => {
const { getByText } = getComponent();
expect(getByText(shareTypeLabels[LocationShareType.Own])).toBeInTheDocument();
expect(getByText(shareTypeLabels[LocationShareType.Live])).toBeInTheDocument();
});
it("renders back button from location picker screen", () => {
const { getByText, getByLabelText } = getComponent();
setShareType(getByText, LocationShareType.Own);
expect(getBackButton(getByLabelText)).toBeInTheDocument();
});
it("clicking cancel button from location picker closes dialog", () => {
const onFinished = jest.fn();
const { getByLabelText } = getComponent({ onFinished });
fireEvent.click(getCancelButton(getByLabelText));
expect(onFinished).toHaveBeenCalled();
});
it("creates static own location share event on submission", () => {
const onFinished = jest.fn();
const { getByText } = getComponent({ onFinished });
setShareType(getByText, LocationShareType.Own);
setLocationGeolocate();
fireEvent.click(getByText("Share location"));
expect(onFinished).toHaveBeenCalled();
const [messageRoomId, relation, messageBody] = mockClient.sendMessage.mock.calls[0];
expect(messageRoomId).toEqual(defaultProps.roomId);
expect(relation).toEqual(null);
expect(messageBody).toEqual(
expect.objectContaining({
[M_ASSET.name]: {
type: LocationAssetType.Self,
},
}),
);
});
});
describe("with pin drop share type enabled", () => {
it("renders share type switch with own and pin drop options", () => {
const { getByText } = getComponent();
expect(document.querySelector(".mx_LocationPicker")).not.toBeInTheDocument();
expect(getByText(shareTypeLabels[LocationShareType.Own])).toBeInTheDocument();
expect(getByText(shareTypeLabels[LocationShareType.Pin])).toBeInTheDocument();
});
it("does not render back button on share type screen", () => {
const { queryByLabelText } = getComponent();
expect(queryByLabelText("Back")).not.toBeInTheDocument();
});
it("clicking cancel button from share type screen closes dialog", () => {
const onFinished = jest.fn();
const { getByLabelText } = getComponent({ onFinished });
fireEvent.click(getCancelButton(getByLabelText));
expect(onFinished).toHaveBeenCalled();
});
it("selecting own location share type advances to location picker", () => {
const { getByText } = getComponent();
setShareType(getByText, LocationShareType.Own);
expect(document.querySelector(".mx_LocationPicker")).toBeInTheDocument();
});
it("clicking back button from location picker screen goes back to share screen", () => {
const onFinished = jest.fn();
const { getByText, getByLabelText } = getComponent({ onFinished });
// advance to location picker
setShareType(getByText, LocationShareType.Own);
expect(document.querySelector(".mx_LocationPicker")).toBeInTheDocument();
fireEvent.click(getBackButton(getByLabelText));
// back to share type
expect(getByText("What location type do you want to share?")).toBeInTheDocument();
});
it("creates pin drop location share event on submission", () => {
const onFinished = jest.fn();
const { getByText } = getComponent({ onFinished });
// advance to location picker
setShareType(getByText, LocationShareType.Pin);
setLocationClick();
fireEvent.click(getByText("Share location"));
expect(onFinished).toHaveBeenCalled();
const [messageRoomId, relation, messageBody] = mockClient.sendMessage.mock.calls[0];
expect(messageRoomId).toEqual(defaultProps.roomId);
expect(relation).toEqual(null);
expect(messageBody).toEqual(
expect.objectContaining({
[M_ASSET.name]: {
type: LocationAssetType.Pin,
},
}),
);
});
});
describe("with live location disabled", () => {
beforeEach(() => enableSettings([]));
it("goes to labs flag screen after live options is clicked", () => {
const onFinished = jest.fn();
const { getByText, getByTestId } = getComponent({ onFinished });
setShareType(getByText, LocationShareType.Live);
expect(getByTestId("location-picker-enable-live-share")).toMatchSnapshot();
});
it("disables OK button when labs flag is not enabled", () => {
const { getByText } = getComponent();
setShareType(getByText, LocationShareType.Live);
expect(getByText("OK").hasAttribute("disabled")).toBeTruthy();
});
it("enables OK button when labs flag is toggled to enabled", () => {
const { getByText, getByLabelText } = getComponent();
setShareType(getByText, LocationShareType.Live);
fireEvent.click(getByLabelText("Enable live location sharing"));
expect(getByText("OK").hasAttribute("disabled")).toBeFalsy();
});
it("enables live share setting on ok button submit", () => {
const { getByText, getByLabelText } = getComponent();
setShareType(getByText, LocationShareType.Live);
fireEvent.click(getByLabelText("Enable live location sharing"));
fireEvent.click(getByText("OK"));
expect(SettingsStore.setValue).toHaveBeenCalledWith(
"feature_location_share_live",
null,
SettingLevel.DEVICE,
true,
);
});
it("navigates to location picker when live share is enabled in settings store", () => {
// @ts-ignore
mocked(SettingsStore.watchSetting).mockImplementation((featureName, roomId, callback) => {
callback(featureName, roomId, SettingLevel.DEVICE, "", "");
window.setTimeout(() => {
callback(featureName, roomId, SettingLevel.DEVICE, "", "");
}, 1000);
});
mocked(SettingsStore.getValue).mockReturnValue(false);
const { getByText, getByLabelText } = getComponent();
setShareType(getByText, LocationShareType.Live);
// we're on enable live share screen
expect(getByLabelText("Enable live location sharing")).toBeInTheDocument();
act(() => {
mocked(SettingsStore.getValue).mockReturnValue(true);
// advance so watchSetting will update the value
jest.runAllTimers();
});
// advanced to location picker
expect(document.querySelector(".mx_LocationPicker")).toBeInTheDocument();
});
});
describe("Live location share", () => {
beforeEach(() => enableSettings(["feature_location_share_live"]));
it("does not display live location share option when composer has a relation", () => {
const relation = {
rel_type: RelationType.Thread,
event_id: "12345",
};
const { queryByText } = getComponent({ relation });
expect(queryByText(shareTypeLabels[LocationShareType.Live])).not.toBeInTheDocument();
});
it("creates beacon info event on submission", async () => {
const onFinished = jest.fn();
const { getByText } = getComponent({ onFinished });
// advance to location picker
setShareType(getByText, LocationShareType.Live);
setLocationGeolocate();
fireEvent.click(getByText("Share location"));
// flush stopping existing beacons promises
await flushPromisesWithFakeTimers();
expect(onFinished).toHaveBeenCalled();
const [eventRoomId, eventContent] = mockClient.unstable_createLiveBeacon.mock.calls[0];
expect(eventRoomId).toEqual(defaultProps.roomId);
expect(eventContent).toEqual(
expect.objectContaining({
// default timeout
timeout: DEFAULT_DURATION_MS,
description: `Ernie's live location`,
live: true,
[M_ASSET.name]: {
type: LocationAssetType.Self,
},
}),
);
});
it("opens error dialog when beacon creation fails", async () => {
// stub logger to keep console clean from expected error
const logSpy = jest.spyOn(logger, "error").mockReturnValue(undefined);
const error = new Error("oh no");
mockClient.unstable_createLiveBeacon.mockRejectedValue(error);
const { getByText } = getComponent();
// advance to location picker
setShareType(getByText, LocationShareType.Live);
setLocationGeolocate();
fireEvent.click(getByText("Share location"));
await flushPromisesWithFakeTimers();
await flushPromisesWithFakeTimers();
await flushPromisesWithFakeTimers();
expect(logSpy).toHaveBeenCalledWith("We couldn't start sharing your live location", error);
expect(mocked(Modal).createDialog).toHaveBeenCalledWith(
QuestionDialog,
expect.objectContaining({
button: "Try again",
description: "Element could not send your location. Please try again later.",
title: `We couldn't send your location`,
cancelButton: "Cancel",
}),
);
});
it("opens error dialog when beacon creation fails with permission error", async () => {
// stub logger to keep console clean from expected error
const logSpy = jest.spyOn(logger, "error").mockReturnValue(undefined);
const error = { errcode: "M_FORBIDDEN" } as unknown as Error;
mockClient.unstable_createLiveBeacon.mockRejectedValue(error);
const { getByText } = getComponent();
// advance to location picker
setShareType(getByText, LocationShareType.Live);
setLocationGeolocate();
fireEvent.click(getByText("Share location"));
await flushPromisesWithFakeTimers();
await flushPromisesWithFakeTimers();
await flushPromisesWithFakeTimers();
expect(logSpy).toHaveBeenCalledWith("Insufficient permissions to start sharing your live location", error);
expect(mocked(Modal).createDialog).toHaveBeenCalledWith(
QuestionDialog,
expect.objectContaining({
button: "OK",
description: "You need to have the right permissions in order to share locations in this room.",
title: `You don't have permission to share locations`,
hasCancelButton: false,
}),
);
});
});
});
function enableSettings(settings: string[]) {
mocked(SettingsStore).getValue.mockReturnValue(false);
mocked(SettingsStore).getValue.mockImplementation((settingName: string): any => settings.includes(settingName));
}

View File

@@ -0,0 +1,47 @@
/*
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 { render, RenderResult } from "jest-matrix-react";
import { RoomMember, LocationAssetType } from "matrix-js-sdk/src/matrix";
import LocationViewDialog from "../../../../../src/components/views/location/LocationViewDialog";
import { TILE_SERVER_WK_KEY } from "../../../../../src/utils/WellKnownUtils";
import { getMockClientWithEventEmitter, makeLocationEvent } from "../../../../test-utils";
describe("<LocationViewDialog />", () => {
const roomId = "!room:server";
const userId = "@user:server";
const mockClient = getMockClientWithEventEmitter({
getClientWellKnown: jest.fn().mockReturnValue({
[TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" },
}),
isGuest: jest.fn().mockReturnValue(false),
});
const defaultEvent = makeLocationEvent("geo:51.5076,-0.1276", LocationAssetType.Pin);
const defaultProps = {
matrixClient: mockClient,
mxEvent: defaultEvent,
onFinished: jest.fn(),
};
const getComponent = (props = {}): RenderResult => render(<LocationViewDialog {...defaultProps} {...props} />);
it("renders map correctly", () => {
const { container } = getComponent();
expect(container.querySelector(".mx_Map")).toMatchSnapshot();
});
it("renders marker correctly for self share", () => {
const selfShareEvent = makeLocationEvent("geo:51.5076,-0.1276", LocationAssetType.Self);
const member = new RoomMember(roomId, userId);
// @ts-ignore cheat assignment to property
selfShareEvent.sender = member;
const { container } = getComponent({ mxEvent: selfShareEvent });
expect(container.querySelector(".mx_BaseAvatar")?.getAttribute("title")).toEqual(userId);
});
});

View File

@@ -0,0 +1,269 @@
/*
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 "jest-matrix-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();
});
});
});

View File

@@ -0,0 +1,51 @@
/*
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 { render, RenderResult } from "jest-matrix-react";
import { MapError, MapErrorProps } from "../../../../../src/components/views/location/MapError";
import { LocationShareError } from "../../../../../src/utils/location";
describe("<MapError />", () => {
const defaultProps = {
onFinished: jest.fn(),
error: LocationShareError.MapStyleUrlNotConfigured,
className: "test",
};
const getComponent = (props: Partial<MapErrorProps> = {}): RenderResult =>
render(<MapError {...defaultProps} {...props} />);
it("renders correctly for MapStyleUrlNotConfigured", () => {
const { container } = getComponent();
expect(container).toMatchSnapshot();
});
it("renders correctly for MapStyleUrlNotReachable", () => {
const { container } = getComponent({
error: LocationShareError.MapStyleUrlNotReachable,
});
expect(container).toMatchSnapshot();
});
it("does not render button when onFinished falsy", () => {
const { queryByText } = getComponent({
error: LocationShareError.MapStyleUrlNotReachable,
onFinished: undefined,
});
// no button
expect(queryByText("OK")).toBeFalsy();
});
it("applies class when isMinimised is truthy", () => {
const { container } = getComponent({
isMinimised: true,
});
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,42 @@
/*
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 { RoomMember } from "matrix-js-sdk/src/matrix";
import { getByTestId, render } from "jest-matrix-react";
import Marker from "../../../../../src/components/views/location/Marker";
describe("<Marker />", () => {
const defaultProps = {
id: "abc123",
};
const getComponent = (props = {}) => render(<Marker {...defaultProps} {...props} />);
it("renders with location icon when no room member", () => {
const { asFragment } = getComponent();
expect(asFragment()).toMatchSnapshot();
});
it("does not try to use member color without room member", () => {
const { container } = getComponent({ useMemberColor: true });
expect(container.querySelector(".mx_Marker.mx_Marker_defaultColor")).toBeInTheDocument();
});
it("uses member color class", () => {
const member = new RoomMember("!room:server", "@user:server");
const { container } = getComponent({ useMemberColor: true, roomMember: member });
expect(container.querySelector(".mx_Marker.mx_Username_color3")).toBeInTheDocument();
});
it("renders member avatar when roomMember is truthy", () => {
const member = new RoomMember("!room:server", "@user:server");
const { container } = getComponent({ roomMember: member });
expect(getByTestId(container, "avatar-img")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,68 @@
/*
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 { render } from "jest-matrix-react";
import { mocked } from "jest-mock";
import * as maplibregl from "maplibre-gl";
import SmartMarker from "../../../../../src/components/views/location/SmartMarker";
jest.mock("../../../../../src/utils/location/findMapStyleUrl", () => ({
findMapStyleUrl: jest.fn().mockReturnValue("tileserver.com"),
}));
describe("<SmartMarker />", () => {
const mapOptions = { container: {} as unknown as HTMLElement, style: "" };
const mockMap = new maplibregl.Map(mapOptions);
const mockMarker = new maplibregl.Marker();
const defaultProps = {
map: mockMap,
geoUri: "geo:43.2,54.6",
};
const getComponent = (props = {}): JSX.Element => <SmartMarker {...defaultProps} {...props} />;
beforeEach(() => {
jest.clearAllMocks();
});
it("creates a marker on mount", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
// marker added only once
expect(maplibregl.Marker).toHaveBeenCalledTimes(1);
// set to correct position
expect(mockMarker.setLngLat).toHaveBeenCalledWith({ lon: 54.6, lat: 43.2 });
// added to map
expect(mockMarker.addTo).toHaveBeenCalledWith(mockMap);
});
it("updates marker position on change", () => {
const { rerender } = render(getComponent({ geoUri: "geo:40,50" }));
rerender(getComponent({ geoUri: "geo:41,51" }));
rerender(getComponent({ geoUri: "geo:42,52" }));
// marker added only once
expect(maplibregl.Marker).toHaveBeenCalledTimes(1);
// set positions
expect(mocked(mockMarker.setLngLat)).toHaveBeenCalledWith({ lat: 40, lon: 50 });
expect(mocked(mockMarker.setLngLat)).toHaveBeenCalledWith({ lat: 41, lon: 51 });
expect(mocked(mockMarker.setLngLat)).toHaveBeenCalledWith({ lat: 42, lon: 52 });
});
it("removes marker on unmount", () => {
const { unmount, container } = render(getComponent());
expect(container).toMatchSnapshot();
unmount();
expect(mockMarker.remove).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,47 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022, 2023 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 * as maplibregl from "maplibre-gl";
import { render, screen } from "jest-matrix-react";
import ZoomButtons from "../../../../../src/components/views/location/ZoomButtons";
describe("<ZoomButtons />", () => {
const mapOptions = { container: {} as unknown as HTMLElement, style: "" };
const mockMap = new maplibregl.Map(mapOptions);
const defaultProps = {
map: mockMap,
};
const getComponent = (props = {}) => render(<ZoomButtons {...defaultProps} {...props} />);
beforeEach(() => {
jest.clearAllMocks();
});
it("renders buttons", () => {
const component = getComponent();
expect(component.asFragment()).toMatchSnapshot();
});
it("calls map zoom in on zoom in click", () => {
const component = getComponent();
screen.getByTestId("map-zoom-in-button").click();
expect(mockMap.zoomIn).toHaveBeenCalled();
expect(component).toBeTruthy();
});
it("calls map zoom out on zoom out click", () => {
const component = getComponent();
screen.getByTestId("map-zoom-out-button").click();
expect(mockMap.zoomOut).toHaveBeenCalled();
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LocationPicker <LocationPicker /> for Live location share type updates selected duration 1`] = `
<div
data-testid="live-duration-option-3600000"
>
Share for 1h
</div>
`;

View File

@@ -0,0 +1,58 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<LocationShareMenu /> with live location disabled goes to labs flag screen after live options is clicked 1`] = `
<div
class="mx_EnableLiveShare"
data-testid="location-picker-enable-live-share"
>
<div
class="mx_StyledLiveBeaconIcon mx_EnableLiveShare_icon"
/>
<h3
class="mx_Heading_h3 mx_EnableLiveShare_heading"
>
Live location sharing
</h3>
<p
class="mx_EnableLiveShare_description"
>
Please note: this is a labs feature using a temporary implementation. This means you will not be able to delete your location history, and advanced users will be able to see your location history even after you stop sharing your live location with this room.
</p>
<div
class="mx_SettingsFlag"
data-testid="enable-live-share-toggle"
>
<span
class="mx_SettingsFlag_label"
>
<div
id="mx_LabelledToggleSwitch_vY7Q4uEh9K38"
>
Enable live location sharing
</div>
</span>
<div
aria-checked="false"
aria-disabled="false"
aria-labelledby="mx_LabelledToggleSwitch_vY7Q4uEh9K38"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<button
aria-disabled="true"
class="mx_AccessibleButton mx_EnableLiveShare_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled"
data-testid="enable-live-share-submit"
disabled=""
role="button"
tabindex="0"
>
OK
</button>
</div>
`;

View File

@@ -0,0 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<LocationViewDialog /> renders map correctly 1`] = `
<div
class="mx_Map mx_LocationViewDialog_map"
id="mx_Map_mx_LocationViewDialog_$2"
>
<span>
<div
class="mx_Marker mx_Marker_defaultColor"
id="mx_LocationViewDialog_$2-marker"
>
<div
class="mx_Marker_border"
>
<div
class="mx_Marker_icon"
/>
</div>
</div>
</span>
<div
class="mx_ZoomButtons"
>
<div
aria-describedby="floating-ui-6"
aria-label="Zoom in"
class="mx_AccessibleButton mx_ZoomButtons_button"
data-testid="map-zoom-in-button"
role="button"
tabindex="0"
>
<div
class="mx_ZoomButtons_icon"
/>
</div>
<div
aria-label="Zoom out"
class="mx_AccessibleButton mx_ZoomButtons_button"
data-testid="map-zoom-out-button"
role="button"
tabindex="0"
>
<div
class="mx_ZoomButtons_icon"
/>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,91 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<MapError /> applies class when isMinimised is truthy 1`] = `
<div>
<div
class="mx_MapError test mx_MapError_isMinimised"
data-testid="map-rendering-error"
>
<div
class="mx_MapError_icon"
/>
<h3
class="mx_Heading_h3 mx_MapError_heading"
>
Unable to load map
</h3>
<p
class="mx_MapError_message"
>
This homeserver is not configured to display maps.
</p>
<button
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
OK
</button>
</div>
</div>
`;
exports[`<MapError /> renders correctly for MapStyleUrlNotConfigured 1`] = `
<div>
<div
class="mx_MapError test"
data-testid="map-rendering-error"
>
<div
class="mx_MapError_icon"
/>
<h3
class="mx_Heading_h3 mx_MapError_heading"
>
Unable to load map
</h3>
<p
class="mx_MapError_message"
>
This homeserver is not configured to display maps.
</p>
<button
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
OK
</button>
</div>
</div>
`;
exports[`<MapError /> renders correctly for MapStyleUrlNotReachable 1`] = `
<div>
<div
class="mx_MapError test"
data-testid="map-rendering-error"
>
<div
class="mx_MapError_icon"
/>
<h3
class="mx_Heading_h3 mx_MapError_heading"
>
Unable to load map
</h3>
<p
class="mx_MapError_message"
>
This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.
</p>
<button
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
OK
</button>
</div>
</div>
`;

View File

@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Marker /> renders with location icon when no room member 1`] = `
<DocumentFragment>
<div
class="mx_Marker mx_Marker_defaultColor"
id="abc123"
>
<div
class="mx_Marker_border"
>
<div
class="mx_Marker_icon"
/>
</div>
</div>
</DocumentFragment>
`;

View File

@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SmartMarker /> creates a marker on mount 1`] = `
<div>
<span>
<div
class="mx_Marker mx_Marker_defaultColor"
>
<div
class="mx_Marker_border"
>
<div
class="mx_Marker_icon"
/>
</div>
</div>
</span>
</div>
`;
exports[`<SmartMarker /> removes marker on unmount 1`] = `
<div>
<span>
<div
class="mx_Marker mx_Marker_defaultColor"
>
<div
class="mx_Marker_border"
>
<div
class="mx_Marker_icon"
/>
</div>
</div>
</span>
</div>
`;

View File

@@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ZoomButtons /> renders buttons 1`] = `
<DocumentFragment>
<div
class="mx_ZoomButtons"
>
<div
aria-label="Zoom in"
class="mx_AccessibleButton mx_ZoomButtons_button"
data-testid="map-zoom-in-button"
role="button"
tabindex="0"
>
<div
class="mx_ZoomButtons_icon"
/>
</div>
<div
aria-label="Zoom out"
class="mx_AccessibleButton mx_ZoomButtons_button"
data-testid="map-zoom-out-button"
role="button"
tabindex="0"
>
<div
class="mx_ZoomButtons_icon"
/>
</div>
</div>
</DocumentFragment>
`;

View File

@@ -0,0 +1,55 @@
/*
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 { mocked } from "jest-mock";
import {
ContentHelpers,
MatrixClient,
LegacyLocationEventContent,
MLocationEventContent,
} from "matrix-js-sdk/src/matrix";
import { doMaybeLocalRoomAction } from "../../../../../src/utils/local-room";
import {
LocationShareType,
shareLocation,
ShareLocationFn,
} from "../../../../../src/components/views/location/shareLocation";
jest.mock("../../../../../src/utils/local-room", () => ({
doMaybeLocalRoomAction: jest.fn(),
}));
describe("shareLocation", () => {
const roomId = "!room:example.com";
const shareType = LocationShareType.Pin;
const content = { test: "location content" } as unknown as LegacyLocationEventContent & MLocationEventContent;
let client: MatrixClient;
let shareLocationFn: ShareLocationFn;
beforeEach(() => {
const makeLocationContent = jest.spyOn(ContentHelpers, "makeLocationContent");
client = {
sendMessage: jest.fn(),
} as unknown as MatrixClient;
mocked(makeLocationContent).mockReturnValue(content);
mocked(doMaybeLocalRoomAction).mockImplementation(
<T>(roomId: string, fn: (actualRoomId: string) => Promise<T>, client?: MatrixClient) => {
return fn(roomId);
},
);
shareLocationFn = shareLocation(client, roomId, shareType, undefined, () => {});
});
it("should forward the call to doMaybeLocalRoomAction", () => {
shareLocationFn({ uri: "https://example.com/" });
expect(client.sendMessage).toHaveBeenCalledWith(roomId, null, content);
});
});