* Use EditInPlace for identity server picker. * Update test * Add a test for setting an ID server. * fix tests * Reformat other :not sections * forgot a comma * Update Apperance settings to use toggle switches. * Remove unused checkbox setting. * Remove unused import. * Update tests * lint * update apperance screenshot * Begin replacing settings * Refactor RoomPublishSetting * Remove LabelledToggleSwitch * Refactor SettingsFlag to use SettingsToggleInput * Refactor CreateRoomDialog to use SettingsToggleInput * Refactor DeclineAndBlockInviteDialog to use SettingsToggleInput * Update DevtoolsDialog * Refactor ReportRoomDialog to use SettingsToggle * Update RoomUpgradeWarningDialog to use SettingsToggleInput * Update WidgetCapabilitiesPromptDialog to use SettingsToggleInput * Update trivial switchovers * Update Notifications settings to use SettingsFlag where possible * Update RoomPublishSetting and SpaceSettingVisibilityTab to use SettingsToggleInput with a warning * revert changes to field * Updated screenshots * Prevent accidental submits * Replace test ID tests * Create new snapshot tests * Add screenshot test for DeclineAndBlockDialog * Add screenshot for create room dialog. * Add devtools test * Add upgrade rooms test * Add widget capabilites prompt test * Fix spec * Add a test for the live location sharing prompt. * fix copyright * Add tests for notification settings * Add tests for user security tab. * Add test for room security tab. * Add test for video settings tab. * remove .only * Test creating a video room * Mask the IM name in the header. * Add spaces vis tab test. * Fixup unit tests to check correct attributes. * Various fixes to components for tests. * lint * Update compound * update setting names * Cleanup tests prettier Updates some more playwright tests Update more snapshots Update switch more fixes drop .only last screenshot round fix video room flake Remove console.logs Remove roomId from devtools view. lint final screenshot * Add playwright tests * import pages/ remove duplicate create-room * Update screenshots * Fix accessibility for devtools * Disable region test * Fixup headers * remove extra test * Fix permissions dialog * fixup tests * update snapshot * Update jest tests * Clear up playwright tests * update widget screenshot * Fix wrong snaps from using wrong compound version * Revert mistaken s/checkbox/switch/ * lint lint * Update headings * fix snap * remove unused * update snapshot * update tab screenshot * Update snapshots * Fix margins * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update snapshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update snapshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
467 lines
18 KiB
TypeScript
467 lines
18 KiB
TypeScript
/*
|
|
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 OR LicenseRef-Element-Commercial
|
|
Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import React from "react";
|
|
import { mocked } from "jest-mock";
|
|
import { RoomMember, RelationType, type MatrixClient, M_ASSET, LocationAssetType } from "matrix-js-sdk/src/matrix";
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
|
import { act, fireEvent, render, type 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: {
|
|
on: jest.fn(),
|
|
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
|
|
act(() => 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
|
|
act(() => 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")).toHaveAttribute("aria-disabled", "true");
|
|
});
|
|
|
|
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")).not.toHaveAttribute("aria-disabled", "true");
|
|
});
|
|
|
|
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));
|
|
}
|