MVVM WidgetContextMenu component to shared component (#31190)
* Create WidgetContextMenu component in shared-components * Modify WidgetMenuContext call (apptile, extensioncard, widgetcard), test and stories * Correctly use new widgetcontextmenu component * WidgetContextMenuViewModel unit test * Lint and add comments * Finalize widgetcontextmenuviewmodel test * fix lint errors * Fix test error * Update playwright screenshots * add userWidget in widgetcontexstmenu props * Fix some a11y issues on playwright * fix linter error widget card * Use new i18n way for share component widget context menu * Add i18n context provider for widget context menu * chore: lint and update snapshot widgetcontextmenu
This commit is contained in:
296
test/viewmodels/right-panel/WidgetContextMenuViewModel-test.tsx
Normal file
296
test/viewmodels/right-panel/WidgetContextMenuViewModel-test.tsx
Normal file
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user