element-web/apps/web/test/viewmodels/right-panel/WidgetContextMenuViewModel-test.tsx

297 lines
11 KiB
TypeScript

/*
* Copyright 2025 New Vector Ltd.
*
* 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 { MatrixWidgetType } from "matrix-widget-api";
import { type MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import {
WidgetContextMenuViewModel,
type WidgetContextMenuViewModelProps,
} from "../../../src/viewmodels/right-panel/WidgetContextMenuViewModel";
import { stubClient } from "../../test-utils";
import WidgetUtils from "../../../src/utils/WidgetUtils";
import { type IApp } from "../../../src/utils/WidgetUtils-types";
import { Container, WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
import * as livestream from "../../../src/Livestream";
import Modal from "../../../src/Modal";
import SettingsStore from "../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import * as widgetStore from "../../../src/stores/WidgetStore";
import { WidgetMessagingStore } from "../../../src/stores/widgets/WidgetMessagingStore";
import { type WidgetMessaging } from "../../../src/stores/widgets/WidgetMessaging";
describe("WidgetContextMenuViewModel", () => {
const widgetId = "w1";
const eventId = "e1";
const roomId = "r1";
const userId = "@user-id:server";
const app: IApp = {
id: widgetId,
eventId,
roomId,
type: MatrixWidgetType.Custom,
url: "https://example.com",
name: "Example 1",
creatorUserId: userId,
avatar_url: undefined,
};
let client: MatrixClient;
const defaultProps: WidgetContextMenuViewModelProps = {
menuDisplayed: true,
room: undefined,
roomId,
cli: stubClient(),
app,
showUnpin: true,
userWidget: true,
trigger: <></>,
onEditClick: jest.fn(),
onDeleteClick: jest.fn(),
onFinished: jest.fn(),
};
beforeEach(() => {
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
jest.spyOn(WidgetUtils, "isManagedByManager").mockReturnValue(true);
jest.spyOn(WidgetUtils, "editWidget").mockReturnValue();
const mockMessaging = {
on: () => {},
off: () => {},
stop: () => {},
widgetApi: {
hasCapability: jest.fn(),
},
} as unknown as WidgetMessaging;
jest.spyOn(WidgetMessagingStore.instance, "getMessagingForUid").mockReturnValue(mockMessaging);
client = stubClient();
});
afterEach(() => {
jest.clearAllMocks();
});
it("should return the snapshot", () => {
const vm = new WidgetContextMenuViewModel(defaultProps);
expect(vm.getSnapshot()).toMatchObject({
showStreamAudioStreamButton: false, // because widget type is custom and not jitsi
showEditButton: true, // because default mock return true on canUserModifyWidgets and isManagedByManager
showRevokeButton: false,
showDeleteButton: true,
showSnapshotButton: false, // because no default value for sdkconfig "enableWidgetScreenshots"
showMoveButtons: [false, false],
canModify: true,
isMenuOpened: true,
trigger: <></>,
});
});
it("should call edit widget no custom edit function passed and room exist", () => {
const props = {
...defaultProps,
room: new Room(roomId, client, userId),
onEditClick: undefined,
};
const vm = new WidgetContextMenuViewModel(props);
vm.onEditClick();
expect(WidgetUtils.editWidget).toHaveBeenCalled();
expect(props.onFinished).toHaveBeenCalled();
});
it("should call custom onEditClick if passed as props and room exist", () => {
const props = {
...defaultProps,
room: new Room(roomId, client, userId),
};
const vm = new WidgetContextMenuViewModel(props);
vm.onEditClick();
expect(props.onEditClick).toHaveBeenCalled();
expect(props.onFinished).toHaveBeenCalled();
});
it("should just call finish if no custom onEditClick is passed as props and does not room exist", () => {
const props = {
...defaultProps,
room: undefined,
onEditClick: undefined,
};
const vm = new WidgetContextMenuViewModel(props);
vm.onEditClick();
expect(WidgetUtils.editWidget).not.toHaveBeenCalled();
expect(props.onFinished).toHaveBeenCalled();
});
it("should move widget position when onmovebutton is called", () => {
jest.spyOn(WidgetLayoutStore.instance, "moveWithinContainer").mockReturnValue();
const props = {
...defaultProps,
room: new Room(roomId, client, userId),
};
const vm = new WidgetContextMenuViewModel(props);
vm.onMoveButton(1);
expect(WidgetLayoutStore.instance.moveWithinContainer).toHaveBeenCalledWith(
props.room,
Container.Top,
props.app,
1,
);
expect(props.onFinished).toHaveBeenCalled();
});
it("should throw error when onmovebutton is called and no room is given", () => {
const props = {
...defaultProps,
room: undefined,
};
const vm = new WidgetContextMenuViewModel(props);
expect(() => vm.onMoveButton(1)).toThrow();
});
it("should startJitsiAudioLivestream when onStreamAudioClick button is clicked", async () => {
jest.spyOn(livestream, "startJitsiAudioLivestream").mockImplementation(jest.fn());
jest.spyOn(livestream, "getConfigLivestreamUrl").mockReturnValue("https://url");
const props = {
...defaultProps,
room: new Room(roomId, client, userId),
};
const vm = new WidgetContextMenuViewModel(props);
vm.onStreamAudioClick();
await expect(livestream.startJitsiAudioLivestream).toHaveBeenCalled();
expect(props.onFinished).toHaveBeenCalled();
});
it("should show modal when startJitsiAudioLivestream is on error and onStreamAudioClick button is clicked", async () => {
jest.spyOn(livestream, "startJitsiAudioLivestream").mockImplementation(() => {
console.log("failllllled");
throw new Error("Failed");
});
jest.spyOn(livestream, "getConfigLivestreamUrl").mockReturnValue("https://url");
jest.spyOn(Modal, "createDialog").mockReturnValue({
finished: Promise.resolve([true, true, false]),
close: jest.fn(),
});
const props = {
...defaultProps,
room: new Room(roomId, client, userId),
};
const vm = new WidgetContextMenuViewModel(props);
await vm.onStreamAudioClick();
expect(Modal.createDialog).toHaveBeenCalled();
});
it("should throw when no room is given and onStreamAudioClick button is clicked", async () => {
jest.spyOn(livestream, "startJitsiAudioLivestream").mockImplementation(jest.fn());
jest.spyOn(livestream, "getConfigLivestreamUrl").mockReturnValue("https://url");
const props = {
...defaultProps,
room: new Room(roomId, client, userId),
};
const vm = new WidgetContextMenuViewModel(props);
await vm.onStreamAudioClick();
// nothing happened
expect(props.onFinished).toHaveBeenCalled();
});
it("should call custom delete function when it is given in props", () => {
const props = {
...defaultProps,
};
const vm = new WidgetContextMenuViewModel(props);
vm.onDeleteClick();
expect(props.onDeleteClick).toHaveBeenCalled();
expect(props.onFinished).toHaveBeenCalled();
});
it("should display modal when no custom function is provided and a room is given", () => {
jest.spyOn(Modal, "createDialog").mockReturnValue({
finished: Promise.resolve([true, true, false]),
close: jest.fn(),
});
const props = {
...defaultProps,
room: new Room(roomId, client, userId),
onDeleteClick: undefined,
};
const vm = new WidgetContextMenuViewModel(props);
vm.onDeleteClick();
expect(Modal.createDialog).toHaveBeenCalled();
expect(props.onFinished).toHaveBeenCalled();
});
it("should do nothing when onDeleteClick and no custom function and no room is provided", () => {
const props = {
...defaultProps,
room: undefined,
onDeleteClick: undefined,
};
const vm = new WidgetContextMenuViewModel(props);
vm.onDeleteClick();
expect(props.onFinished).toHaveBeenCalled();
});
it("should set new level for allowedwidget when onrevoke button is clicked", () => {
const current = { [eventId]: true };
jest.spyOn(SettingsStore, "getValue").mockReturnValue(current);
jest.spyOn(SettingsStore, "firstSupportedLevel").mockReturnValue(SettingLevel.DEFAULT);
jest.spyOn(SettingsStore, "setValue").mockResolvedValue();
jest.spyOn(widgetStore, "isAppWidget").mockReturnValue(true);
const props = {
...defaultProps,
room: new Room(roomId, client, userId),
};
const vm = new WidgetContextMenuViewModel(props);
vm.onRevokeClick();
expect(SettingsStore.setValue).toHaveBeenCalledWith(
"allowedWidgets",
props.roomId,
SettingLevel.DEFAULT,
current,
);
const current2 = { [eventId]: false };
jest.spyOn(SettingsStore, "getValue").mockReturnValue(current2);
jest.spyOn(SettingsStore, "firstSupportedLevel").mockReturnValue(SettingLevel.DEFAULT);
jest.spyOn(SettingsStore, "setValue").mockResolvedValue();
jest.spyOn(widgetStore, "isAppWidget").mockReturnValue(false);
vm.onRevokeClick();
expect(SettingsStore.setValue).toHaveBeenCalledWith(
"allowedWidgets",
props.roomId,
SettingLevel.DEFAULT,
current2,
);
});
it("should throw an error when first supported level is not set", () => {
jest.spyOn(SettingsStore, "firstSupportedLevel").mockReturnValue(null);
const props = {
...defaultProps,
room: undefined,
onDeleteClick: undefined,
};
const vm = new WidgetContextMenuViewModel(props);
expect(() => vm.onRevokeClick()).toThrow();
});
});