mv element.io @types __mocks__/ debian docker module_system/ playwright res src test webapp Dockerfile .dockerignore .eslintignore .stylelintrc.cjs babel.config.cjs recorder-worklet-loader.cjs .modernizr.json components.json config.json config.sample.json package.json project.json tsconfig.json tsconfig.module_system.json jest.config.ts playwright.config.ts webpack.config.ts build_config.sample.yaml apps/web/
mkdir apps/web/scripts
mv scripts/{cleanup.sh,ci_package.sh,copy-res.ts,deploy.py,package.sh} apps/web/scripts
And a couple of gitignore tweaks
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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 { renderHook, waitFor } from "jest-matrix-react";
|
||||
import { JoinRule, type MatrixClient, type Room, RoomMember, User } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
AvatarBadgeDecoration,
|
||||
useRoomAvatarViewModel,
|
||||
} from "../../../../../src/components/viewmodels/avatars/RoomAvatarViewModel";
|
||||
import { createTestClient, mkStubRoom } from "../../../../test-utils";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import * as PresenceIndicatorModule from "../../../../../src/components/views/avatars/WithPresenceIndicator";
|
||||
import { DefaultTagID } from "../../../../../src/stores/room-list/models";
|
||||
|
||||
jest.mock("../../../../../src/utils/room/getJoinedNonFunctionalMembers", () => ({
|
||||
getJoinedNonFunctionalMembers: jest.fn().mockReturnValue([]),
|
||||
}));
|
||||
|
||||
describe("RoomAvatarViewModel", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = createTestClient();
|
||||
room = mkStubRoom("roomId", "roomName", matrixClient);
|
||||
|
||||
DMRoomMap.makeShared(matrixClient);
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(undefined);
|
||||
|
||||
jest.spyOn(PresenceIndicatorModule, "useDmMember").mockReturnValue(null);
|
||||
jest.spyOn(PresenceIndicatorModule, "usePresence").mockReturnValue(null);
|
||||
});
|
||||
|
||||
it("should have badgeDecoration set to LowPriority", () => {
|
||||
room.tags[DefaultTagID.LowPriority] = {};
|
||||
const { result: vm } = renderHook(() => useRoomAvatarViewModel(room));
|
||||
expect(vm.current.badgeDecoration).toBe(AvatarBadgeDecoration.LowPriority);
|
||||
});
|
||||
|
||||
it("should have badgeDecoration set to VideoRoom", () => {
|
||||
jest.spyOn(room, "isCallRoom").mockReturnValue(true);
|
||||
const { result: vm } = renderHook(() => useRoomAvatarViewModel(room));
|
||||
expect(vm.current.badgeDecoration).toBe(AvatarBadgeDecoration.VideoRoom);
|
||||
});
|
||||
|
||||
it("should have badgeDecoration set to PublicRoom", () => {
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
const { result: vm } = renderHook(() => useRoomAvatarViewModel(room));
|
||||
expect(vm.current.badgeDecoration).toBe(AvatarBadgeDecoration.PublicRoom);
|
||||
});
|
||||
|
||||
it("should set badgeDecoration based on priority", () => {
|
||||
// 1. Presence has the least priority
|
||||
const user = User.createUser("userId", matrixClient);
|
||||
const roomMember = new RoomMember(room.roomId, "userId");
|
||||
roomMember.user = user;
|
||||
jest.spyOn(PresenceIndicatorModule, "useDmMember").mockReturnValue(roomMember);
|
||||
jest.spyOn(PresenceIndicatorModule, "usePresence").mockReturnValue(PresenceIndicatorModule.Presence.Online);
|
||||
|
||||
const { result: vm1 } = renderHook(() => useRoomAvatarViewModel(room));
|
||||
expect(vm1.current.badgeDecoration).toBe(AvatarBadgeDecoration.Presence);
|
||||
|
||||
// 2. With presence and public room, presence takes precedence
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
// Render again, it's easier than mocking the event emitter.
|
||||
const { result: vm, rerender } = renderHook(() => useRoomAvatarViewModel(room));
|
||||
expect(vm.current.badgeDecoration).toBe(AvatarBadgeDecoration.PublicRoom);
|
||||
|
||||
// 3. With presence, public-room and video room, video room takes precedence
|
||||
jest.spyOn(room, "isCallRoom").mockReturnValue(true);
|
||||
rerender(room);
|
||||
expect(vm.current.badgeDecoration).toBe(AvatarBadgeDecoration.VideoRoom);
|
||||
|
||||
// 4. With presence, public room, video room and low priority, low priority takes precedence
|
||||
room.tags[DefaultTagID.LowPriority] = {};
|
||||
rerender(room);
|
||||
expect(vm.current.badgeDecoration).toBe(AvatarBadgeDecoration.LowPriority);
|
||||
});
|
||||
|
||||
it("should recompute isPublic when room changed", async () => {
|
||||
const { result: vm, rerender } = renderHook((props) => useRoomAvatarViewModel(props), { initialProps: room });
|
||||
expect(vm.current.badgeDecoration).not.toBe(AvatarBadgeDecoration.PublicRoom);
|
||||
|
||||
const publicRoom = mkStubRoom("roomId2", "roomName2", matrixClient);
|
||||
jest.spyOn(publicRoom, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
rerender(publicRoom);
|
||||
|
||||
await waitFor(() => expect(vm.current.badgeDecoration).toBe(AvatarBadgeDecoration.PublicRoom));
|
||||
});
|
||||
|
||||
it("should return presence", async () => {
|
||||
const user = User.createUser("userId", matrixClient);
|
||||
const roomMember = new RoomMember(room.roomId, "userId");
|
||||
roomMember.user = user;
|
||||
jest.spyOn(PresenceIndicatorModule, "useDmMember").mockReturnValue(roomMember);
|
||||
jest.spyOn(PresenceIndicatorModule, "usePresence").mockReturnValue(PresenceIndicatorModule.Presence.Online);
|
||||
|
||||
const { result: vm } = renderHook(() => useRoomAvatarViewModel(room));
|
||||
expect(vm.current.presence).toBe(PresenceIndicatorModule.Presence.Online);
|
||||
});
|
||||
});
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { renderHook } from "jest-matrix-react";
|
||||
import { act, type SyntheticEvent } from "react";
|
||||
|
||||
import { useRoomTopicViewModel } from "../../../../../src/components/viewmodels/right_panel/RoomSummaryCardTopicViewModel";
|
||||
import { createTestClient, mkStubRoom } from "../../../../test-utils";
|
||||
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import { onRoomTopicLinkClick } from "../../../../../src/components/views/elements/RoomTopic";
|
||||
|
||||
jest.mock("../../../../../src/components/views/elements/RoomTopic");
|
||||
|
||||
describe("RoomSummaryCardTopicViewModel", () => {
|
||||
const client = createTestClient();
|
||||
const mockRoom = mkStubRoom("!room:example.com", "Test Room", client);
|
||||
const mockUserId = "@user:example.com";
|
||||
const mockEvent = { preventDefault: jest.fn(), stopPropagation: jest.fn() } as unknown as SyntheticEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock room client's getSafeUserId
|
||||
mockRoom.client.getSafeUserId = jest.fn().mockReturnValue(mockUserId);
|
||||
jest.spyOn(defaultDispatcher, "dispatch");
|
||||
|
||||
(onRoomTopicLinkClick as jest.Mock).mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function render() {
|
||||
return renderHook(() => useRoomTopicViewModel(mockRoom));
|
||||
}
|
||||
|
||||
it("should initialize with expanded state", () => {
|
||||
const { result } = render();
|
||||
expect(result.current.expanded).toBe(true);
|
||||
});
|
||||
|
||||
it("should toggle expanded state on click", async () => {
|
||||
const { result } = render();
|
||||
|
||||
await act(() => {
|
||||
result.current.onExpandedClick(mockEvent);
|
||||
});
|
||||
|
||||
expect(result.current.expanded).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle edit click", () => {
|
||||
const { result } = render();
|
||||
result.current.onEditClick(mockEvent);
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "open_room_settings" });
|
||||
});
|
||||
|
||||
it("should handle topic link clicks when the target is an anchor element", () => {
|
||||
const { result } = render();
|
||||
const mockAnchorEvent = { target: document.createElement("a") } as unknown as React.MouseEvent<HTMLElement>;
|
||||
|
||||
result.current.onTopicLinkClick(mockAnchorEvent);
|
||||
expect(onRoomTopicLinkClick).toHaveBeenCalledWith(mockAnchorEvent);
|
||||
});
|
||||
|
||||
it("should handle topic link clicks when the target is not an anchor element", () => {
|
||||
const { result } = render();
|
||||
const mockNonAnchorEvent = {
|
||||
target: document.createElement("div"),
|
||||
} as unknown as React.MouseEvent<HTMLElement>;
|
||||
|
||||
result.current.onTopicLinkClick(mockNonAnchorEvent);
|
||||
expect(onRoomTopicLinkClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("Topic editing permissions", () => {
|
||||
it("should allow editing when user has permission", () => {
|
||||
mockRoom.currentState.maySendStateEvent = jest.fn().mockReturnValue(true);
|
||||
const { result } = render();
|
||||
expect(result.current.canEditTopic).toBe(true);
|
||||
});
|
||||
|
||||
it("should not allow editing when user lacks permission", () => {
|
||||
mockRoom.currentState.maySendStateEvent = jest.fn().mockReturnValue(false);
|
||||
const { result } = render();
|
||||
expect(result.current.canEditTopic).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* 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 { act, renderHook, waitFor } from "jest-matrix-react";
|
||||
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { useRoomSummaryCardViewModel } from "../../../../../src/components/viewmodels/right_panel/RoomSummaryCardViewModel";
|
||||
import { mkStubRoom, stubClient, withClientContextRenderOptions } from "../../../../test-utils";
|
||||
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import RoomListStore from "../../../../../src/stores/room-list/RoomListStore";
|
||||
import { DefaultTagID } from "../../../../../src/stores/room-list/models";
|
||||
import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelStore";
|
||||
import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases";
|
||||
import Modal from "../../../../../src/Modal";
|
||||
import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog";
|
||||
import ExportDialog from "../../../../../src/components/views/dialogs/ExportDialog";
|
||||
import { PollHistoryDialog } from "../../../../../src/components/views/dialogs/PollHistoryDialog";
|
||||
import { ReportRoomDialog } from "../../../../../src/components/views/dialogs/ReportRoomDialog";
|
||||
import { inviteToRoom } from "../../../../../src/utils/room/inviteToRoom";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import * as hooks from "../../../../../src/hooks/useAccountData";
|
||||
|
||||
jest.mock("../../../../../src/utils/room/inviteToRoom", () => ({
|
||||
inviteToRoom: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("useRoomSummaryCardViewModel", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
let room: Room;
|
||||
let permalinkCreator: any;
|
||||
const onSearchCancel = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = stubClient();
|
||||
room = mkStubRoom("roomId", "roomName", matrixClient);
|
||||
permalinkCreator = {};
|
||||
|
||||
DMRoomMap.setShared({
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
} as unknown as DMRoomMap);
|
||||
|
||||
jest.spyOn(RoomListStore.instance, "getTagsForRoom").mockReturnValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
function render() {
|
||||
return renderHook(
|
||||
() => useRoomSummaryCardViewModel(room, permalinkCreator, onSearchCancel),
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
}
|
||||
|
||||
it("should return correct initial state", () => {
|
||||
const { result } = render();
|
||||
|
||||
expect(result.current.isDirectMessage).toBe(false);
|
||||
expect(result.current.isRoomEncrypted).toBe(false);
|
||||
expect(result.current.isVideoRoom).toBe(false);
|
||||
expect(result.current.isFavorite).toBe(false);
|
||||
expect(result.current.pinCount).toBe(0);
|
||||
expect(result.current.searchInputRef.current).toBe(null);
|
||||
});
|
||||
|
||||
it("should handle room members click", () => {
|
||||
const spy = jest.spyOn(RightPanelStore.instance, "pushCard");
|
||||
const { result } = render();
|
||||
|
||||
result.current.onRoomMembersClick();
|
||||
expect(spy).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList }, true);
|
||||
});
|
||||
|
||||
it("should handle room settings click", () => {
|
||||
const spy = jest.spyOn(defaultDispatcher, "dispatch");
|
||||
const { result } = render();
|
||||
|
||||
result.current.onRoomSettingsClick(new Event("click"));
|
||||
expect(spy).toHaveBeenCalledWith({ action: "open_room_settings" });
|
||||
});
|
||||
|
||||
it("should handle leave room click", () => {
|
||||
const spy = jest.spyOn(defaultDispatcher, "dispatch");
|
||||
const { result } = render();
|
||||
|
||||
result.current.onLeaveRoomClick();
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
action: "leave_room",
|
||||
room_id: room.roomId,
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle room threads click", () => {
|
||||
const spy = jest.spyOn(RightPanelStore.instance, "pushCard");
|
||||
const { result } = render();
|
||||
|
||||
result.current.onRoomThreadsClick();
|
||||
expect(spy).toHaveBeenCalledWith({ phase: RightPanelPhases.ThreadPanel }, true);
|
||||
});
|
||||
|
||||
it("should handle room files click", () => {
|
||||
const spy = jest.spyOn(RightPanelStore.instance, "pushCard");
|
||||
const { result } = render();
|
||||
|
||||
result.current.onRoomFilesClick();
|
||||
expect(spy).toHaveBeenCalledWith({ phase: RightPanelPhases.FilePanel }, true);
|
||||
});
|
||||
|
||||
it("should handle room extensions click", () => {
|
||||
const spy = jest.spyOn(RightPanelStore.instance, "pushCard");
|
||||
const { result } = render();
|
||||
|
||||
result.current.onRoomExtensionsClick();
|
||||
expect(spy).toHaveBeenCalledWith({ phase: RightPanelPhases.Extensions }, true);
|
||||
});
|
||||
|
||||
it("should handle room pins click", () => {
|
||||
const spy = jest.spyOn(RightPanelStore.instance, "pushCard");
|
||||
const { result } = render();
|
||||
|
||||
result.current.onRoomPinsClick();
|
||||
expect(spy).toHaveBeenCalledWith({ phase: RightPanelPhases.PinnedMessages }, true);
|
||||
});
|
||||
|
||||
it("should handle room invite click", () => {
|
||||
const { result } = render();
|
||||
|
||||
result.current.onInviteToRoomClick();
|
||||
expect(inviteToRoom).toHaveBeenCalledWith(room);
|
||||
});
|
||||
|
||||
describe("action that trigger a dialog", () => {
|
||||
let createDialogSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
createDialogSpy = jest.spyOn(Modal, "createDialog").mockImplementation(() => ({
|
||||
finished: Promise.resolve([false]),
|
||||
close: jest.fn(),
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
createDialogSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should handle room export click", async () => {
|
||||
const { result } = render();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.onRoomExportClick();
|
||||
});
|
||||
expect(createDialogSpy).toHaveBeenCalledWith(ExportDialog, { room });
|
||||
});
|
||||
|
||||
it("should handle room poll history click", async () => {
|
||||
const { result } = render();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.onRoomPollHistoryClick();
|
||||
});
|
||||
expect(createDialogSpy).toHaveBeenCalledWith(PollHistoryDialog, {
|
||||
room,
|
||||
matrixClient,
|
||||
permalinkCreator,
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle room report click", async () => {
|
||||
const { result } = render();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.onReportRoomClick();
|
||||
});
|
||||
|
||||
expect(createDialogSpy).toHaveBeenCalledWith(ReportRoomDialog, { roomId: room.roomId });
|
||||
});
|
||||
|
||||
it("should handle share room click", async () => {
|
||||
const { result } = render();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.onShareRoomClick();
|
||||
});
|
||||
|
||||
expect(createDialogSpy).toHaveBeenCalledWith(ShareDialog, {
|
||||
target: room,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("favorite room state", () => {
|
||||
it("should identify favorite rooms", () => {
|
||||
jest.spyOn(RoomListStore.instance, "getTagsForRoom").mockReturnValue([DefaultTagID.Favourite]);
|
||||
const { result } = render();
|
||||
|
||||
expect(result.current.isFavorite).toBe(true);
|
||||
});
|
||||
|
||||
it("should identify non-favorite rooms", () => {
|
||||
jest.spyOn(RoomListStore.instance, "getTagsForRoom").mockReturnValue([]);
|
||||
const { result } = render();
|
||||
|
||||
expect(result.current.isFavorite).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("direct message state", () => {
|
||||
it("should identify direct message rooms", async () => {
|
||||
// Mock the direct rooms account data
|
||||
const directRoomsList = {
|
||||
"@user:domain.com": [room.roomId],
|
||||
};
|
||||
// Mock the useAccountData hook result
|
||||
jest.spyOn(hooks, "useAccountData").mockReturnValue(directRoomsList);
|
||||
|
||||
const { result } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isDirectMessage).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("should identify non-direct message rooms", async () => {
|
||||
// Mock the direct rooms account data
|
||||
const directRoomsList = {};
|
||||
// Mock the useAccountData hook result
|
||||
jest.spyOn(hooks, "useAccountData").mockReturnValue(directRoomsList);
|
||||
|
||||
const { result } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isDirectMessage).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("search input", () => {
|
||||
it("should handle search input escape key", () => {
|
||||
const directRoomsList = {};
|
||||
jest.spyOn(hooks, "useAccountData").mockReturnValue(directRoomsList);
|
||||
const { result } = render();
|
||||
// Create a mock input element and set it as the current ref value
|
||||
const mockInputElement = document.createElement("input");
|
||||
mockInputElement.value = "some search text";
|
||||
|
||||
result.current.searchInputRef.current = mockInputElement;
|
||||
|
||||
const event = {
|
||||
key: "Escape",
|
||||
preventDefault: jest.fn(),
|
||||
stopPropagation: jest.fn(),
|
||||
};
|
||||
|
||||
result.current.onUpdateSearchInput(event as any);
|
||||
|
||||
expect(onSearchCancel).toHaveBeenCalled();
|
||||
expect(mockInputElement?.value).toBe("");
|
||||
});
|
||||
});
|
||||
});
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
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 {
|
||||
EventType,
|
||||
KnownMembership,
|
||||
type MatrixClient,
|
||||
MatrixEvent,
|
||||
type Room,
|
||||
RoomMember,
|
||||
type User,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { renderHook, waitFor } from "jest-matrix-react";
|
||||
|
||||
import { Action } from "../../../../../../src/dispatcher/actions";
|
||||
import Modal from "../../../../../../src/Modal";
|
||||
import MultiInviter from "../../../../../../src/utils/MultiInviter";
|
||||
import { createTestClient, mkRoom, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import dis from "../../../../../../src/dispatcher/dispatcher";
|
||||
import { useUserInfoBasicOptionsViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoBasicOptionsViewModel";
|
||||
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
||||
import ErrorDialog from "../../../../../../src/components/views/dialogs/ErrorDialog";
|
||||
|
||||
jest.mock("../../../../../../src/dispatcher/dispatcher");
|
||||
|
||||
describe("<UserOptionsSection />", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
const meUserId = "@me:example.com";
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
let defaultProps: { room: Room; member: User | RoomMember };
|
||||
let mockClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = createTestClient();
|
||||
room = mkRoom(mockClient, defaultRoomId);
|
||||
defaultProps = {
|
||||
member: defaultMember,
|
||||
room,
|
||||
};
|
||||
DMRoomMap.makeShared(mockClient);
|
||||
});
|
||||
|
||||
const renderUserInfoBasicOptionsViewModelHook = (
|
||||
props: {
|
||||
member: User | RoomMember;
|
||||
room: Room;
|
||||
} = defaultProps,
|
||||
) => {
|
||||
return renderHook(
|
||||
() => useUserInfoBasicOptionsViewModel(props.room, props.member),
|
||||
withClientContextRenderOptions(mockClient),
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Mock the current user account id. Which is different to the defaultMember which is the selected one
|
||||
// When we want to mock the current user, needs to override this value
|
||||
jest.spyOn(mockClient, "getUserId").mockReturnValue(meUserId);
|
||||
jest.spyOn(mockClient, "getRoom").mockReturnValue(room);
|
||||
});
|
||||
|
||||
it("should showInviteButton if current user can invite and selected user membership is LEAVE", () => {
|
||||
// cant use mkRoomMember because instanceof check will failed in this case
|
||||
const member: RoomMember = new RoomMember(defaultMember.userId, defaultMember.roomId);
|
||||
const me: RoomMember = new RoomMember(meUserId, defaultMember.roomId);
|
||||
|
||||
console.log("member instanceof RoomMember", member instanceof RoomMember);
|
||||
|
||||
member.powerLevel = 1;
|
||||
member.membership = KnownMembership.Leave;
|
||||
me.powerLevel = 50;
|
||||
me.membership = KnownMembership.Join;
|
||||
const powerLevelEvents = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: {
|
||||
invite: 50,
|
||||
state_default: 0,
|
||||
},
|
||||
});
|
||||
jest.spyOn(room.currentState, "getStateEvents").mockReturnValue(powerLevelEvents);
|
||||
// used to get the current me user
|
||||
jest.spyOn(room, "getMember").mockReturnValue(me);
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook({ ...defaultProps, member });
|
||||
|
||||
expect(result.current.showInviteButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not showInviteButton if current cannot invite", () => {
|
||||
const member: RoomMember = new RoomMember(defaultMember.userId, defaultMember.roomId);
|
||||
const me: RoomMember = new RoomMember(meUserId, defaultMember.roomId);
|
||||
member.powerLevel = 50;
|
||||
member.membership = KnownMembership.Leave;
|
||||
me.powerLevel = 0;
|
||||
me.membership = KnownMembership.Join;
|
||||
const powerLevelEvents = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: {
|
||||
invite: 50,
|
||||
state_default: 0,
|
||||
},
|
||||
});
|
||||
jest.spyOn(room.currentState, "getStateEvents").mockReturnValue(powerLevelEvents);
|
||||
// used to get the current me user
|
||||
jest.spyOn(room, "getMember").mockReturnValue(me);
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook({ ...defaultProps, member });
|
||||
|
||||
expect(result.current.showInviteButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not showInviteButton if selected user membership is not LEAVE", () => {
|
||||
const member: RoomMember = new RoomMember(defaultMember.userId, defaultMember.roomId);
|
||||
const me: RoomMember = new RoomMember(meUserId, defaultMember.roomId);
|
||||
member.powerLevel = 50;
|
||||
member.membership = KnownMembership.Join;
|
||||
me.powerLevel = 50;
|
||||
me.membership = KnownMembership.Join;
|
||||
const powerLevelEvents = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: {
|
||||
invite: 50,
|
||||
state_default: 0,
|
||||
},
|
||||
});
|
||||
jest.spyOn(room.currentState, "getStateEvents").mockReturnValue(powerLevelEvents);
|
||||
jest.spyOn(room, "getMember").mockReturnValue(me);
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook({ ...defaultProps, member });
|
||||
|
||||
expect(result.current.showInviteButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should showInsertPillButton if room is not a space", () => {
|
||||
jest.spyOn(room, "isSpaceRoom").mockReturnValue(false);
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||
expect(result.current.showInsertPillButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not showInsertPillButton if room is a space", () => {
|
||||
jest.spyOn(room, "isSpaceRoom").mockReturnValue(true);
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||
expect(result.current.showInsertPillButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should readReceiptButtonDisabled be true if all messages where read", () => {
|
||||
jest.spyOn(room, "getEventReadUpTo").mockReturnValue(null);
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||
expect(result.current.readReceiptButtonDisabled).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should readReceiptButtonDisabled be false if some messages are available", () => {
|
||||
jest.spyOn(room, "getEventReadUpTo").mockReturnValue("aneventId");
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||
expect(result.current.readReceiptButtonDisabled).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should readReceiptButtonDisabled be true if room is a space", () => {
|
||||
jest.spyOn(room, "getEventReadUpTo").mockReturnValue("aneventId");
|
||||
jest.spyOn(room, "isSpaceRoom").mockReturnValue(true);
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||
expect(result.current.readReceiptButtonDisabled).toBeTruthy();
|
||||
});
|
||||
|
||||
it("firing onReadReceiptButton calls dispatch with correct event_id", () => {
|
||||
const eventId = "aneventId";
|
||||
jest.spyOn(room, "getEventReadUpTo").mockReturnValue(eventId);
|
||||
jest.spyOn(room, "isSpaceRoom").mockReturnValue(false);
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||
|
||||
result.current.onReadReceiptButton();
|
||||
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: "view_room",
|
||||
event_id: eventId,
|
||||
highlighted: true,
|
||||
metricsTrigger: undefined,
|
||||
room_id: defaultRoomId,
|
||||
});
|
||||
});
|
||||
|
||||
it("calling onInsertPillButton should calls dispatch", () => {
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||
|
||||
result.current.onInsertPillButton();
|
||||
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ComposerInsert,
|
||||
userId: defaultMember.userId,
|
||||
timelineRenderingType: "Room",
|
||||
});
|
||||
});
|
||||
|
||||
it("calling onInviteUserButton will call MultiInviter.invite", async () => {
|
||||
// to save mocking, we will reject the call to .invite
|
||||
const mockErrorMessage = new Error("test error message");
|
||||
const spy = jest.spyOn(MultiInviter.prototype, "invite");
|
||||
spy.mockRejectedValue(mockErrorMessage);
|
||||
jest.spyOn(Modal, "createDialog");
|
||||
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||
result.current.onInviteUserButton("roomId", new Event("click"));
|
||||
|
||||
// check that we have called .invite
|
||||
expect(spy).toHaveBeenCalledWith([defaultMember.userId]);
|
||||
|
||||
await waitFor(() => {
|
||||
// check that the test error message is displayed
|
||||
expect(Modal.createDialog).toHaveBeenCalledWith(ErrorDialog, {
|
||||
description: "test error message",
|
||||
title: "Failed to invite",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
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 { EventType, type MatrixClient, MatrixEvent, type Room, RoomMember, type User } from "matrix-js-sdk/src/matrix";
|
||||
import { renderHook, waitFor } from "jest-matrix-react";
|
||||
|
||||
import { createTestClient, mkRoom, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import { useUserInfoBasicViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoBasicViewModel";
|
||||
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
||||
import Modal from "../../../../../../src/Modal";
|
||||
import QuestionDialog from "../../../../../../src/components/views/dialogs/QuestionDialog";
|
||||
|
||||
jest.mock("../../../../../../src/customisations/UserIdentifier", () => {
|
||||
return {
|
||||
getDisplayUserIdentifier: jest.fn().mockReturnValue("customUserIdentifier"),
|
||||
};
|
||||
});
|
||||
|
||||
describe("useUserInfoHeaderViewModel", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
let defaultProps: {
|
||||
member: User | RoomMember;
|
||||
room: Room;
|
||||
};
|
||||
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = createTestClient();
|
||||
mockClient.isSynapseAdministrator = jest.fn().mockResolvedValue(true);
|
||||
mockClient.deactivateSynapseUser = jest.fn().mockResolvedValue({
|
||||
id_server_unbind_result: "success",
|
||||
});
|
||||
|
||||
room = mkRoom(mockClient, defaultRoomId);
|
||||
defaultProps = {
|
||||
member: defaultMember,
|
||||
room,
|
||||
};
|
||||
DMRoomMap.makeShared(mockClient);
|
||||
jest.spyOn(mockClient, "getRoom").mockReturnValue(room);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderUserInfoBasicViewModelHook = (
|
||||
props: {
|
||||
member: User | RoomMember;
|
||||
room: Room;
|
||||
} = defaultProps,
|
||||
) => {
|
||||
return renderHook(
|
||||
() => useUserInfoBasicViewModel(props.room, props.member),
|
||||
withClientContextRenderOptions(mockClient),
|
||||
);
|
||||
};
|
||||
|
||||
it("should set showDeactivateButton value to true", async () => {
|
||||
jest.spyOn(mockClient, "getDomain").mockReturnValue("example.com");
|
||||
const { result } = renderUserInfoBasicViewModelHook();
|
||||
// checking the synpase admin is an async operation, that is why we wait for it
|
||||
await waitFor(() => {
|
||||
expect(result.current.showDeactivateButton).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set showDeactivateButton value to false because domain is not the same", async () => {
|
||||
jest.spyOn(mockClient, "getDomain").mockReturnValue("toto.com");
|
||||
const { result } = renderUserInfoBasicViewModelHook();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.showDeactivateButton).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("should give powerlevels values", () => {
|
||||
const powerLevelEvents = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: {
|
||||
invite: 1,
|
||||
state_default: 1,
|
||||
},
|
||||
});
|
||||
jest.spyOn(room.currentState, "getStateEvents").mockReturnValue(powerLevelEvents);
|
||||
const { result } = renderUserInfoBasicViewModelHook();
|
||||
expect(result.current.powerLevels).toStrictEqual({
|
||||
invite: 1,
|
||||
state_default: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("should set isRoomDMForMember to true if found in dmroommap", () => {
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue("id");
|
||||
const { result } = renderUserInfoBasicViewModelHook();
|
||||
expect(result.current.isRoomDMForMember).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should set isRoomDMForMember to false if not found in dmroommap", () => {
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(undefined);
|
||||
const { result } = renderUserInfoBasicViewModelHook();
|
||||
expect(result.current.isRoomDMForMember).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should display modal and call deactivateSynapseUser when calling onSynapaseDeactivate", async () => {
|
||||
const powerLevelEvents = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: {
|
||||
invite: 1,
|
||||
state_default: 1,
|
||||
},
|
||||
});
|
||||
jest.spyOn(room.currentState, "getStateEvents").mockReturnValue(powerLevelEvents);
|
||||
jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
finished: Promise.resolve([true, true, false]),
|
||||
close: jest.fn(),
|
||||
});
|
||||
|
||||
const { result } = renderUserInfoBasicViewModelHook();
|
||||
|
||||
await waitFor(() => result.current.onSynapseDeactivate());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(Modal.createDialog).toHaveBeenLastCalledWith(QuestionDialog, {
|
||||
button: "Deactivate user",
|
||||
danger: true,
|
||||
description: (
|
||||
<div>
|
||||
Deactivating this user will log them out and prevent them from logging back in. Additionally,
|
||||
they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want
|
||||
to deactivate this user?
|
||||
</div>
|
||||
),
|
||||
title: "Deactivate user?",
|
||||
});
|
||||
});
|
||||
expect(mockClient.deactivateSynapseUser).toHaveBeenCalledWith(defaultMember.userId);
|
||||
});
|
||||
});
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
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 { Device, type MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { type Mocked } from "jest-mock";
|
||||
import { UserVerificationStatus, type CryptoApi } from "matrix-js-sdk/src/crypto-api";
|
||||
import { renderHook, waitFor } from "jest-matrix-react";
|
||||
|
||||
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||
import { useUserInfoVerificationViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderVerificationViewModel";
|
||||
|
||||
describe("useUserInfoVerificationHeaderViewModel", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
|
||||
const defaultProps = {
|
||||
devices: [] as Device[],
|
||||
member: defaultMember,
|
||||
};
|
||||
let mockClient: MatrixClient;
|
||||
let mockCrypto: Mocked<CryptoApi>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCrypto = {
|
||||
bootstrapSecretStorage: jest.fn(),
|
||||
bootstrapCrossSigning: jest.fn(),
|
||||
getCrossSigningKeyId: jest.fn(),
|
||||
getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]),
|
||||
getUserDeviceInfo: jest.fn(),
|
||||
getDeviceVerificationStatus: jest.fn(),
|
||||
getUserVerificationStatus: jest.fn(),
|
||||
isDehydrationSupported: jest.fn().mockResolvedValue(false),
|
||||
startDehydration: jest.fn(),
|
||||
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
|
||||
userHasCrossSigningKeys: jest.fn().mockResolvedValue(false),
|
||||
} as unknown as Mocked<CryptoApi>;
|
||||
|
||||
mockClient = createTestClient();
|
||||
jest.spyOn(mockClient, "doesServerSupportUnstableFeature").mockResolvedValue(true);
|
||||
jest.spyOn(mockClient.secretStorage, "hasKey").mockResolvedValue(true);
|
||||
jest.spyOn(mockClient, "getCrypto").mockReturnValue(mockCrypto);
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const renderUserInfoHeaderVerificationHook = (props = defaultProps) => {
|
||||
return renderHook(
|
||||
() => useUserInfoVerificationViewModel(props.member, props.devices),
|
||||
withClientContextRenderOptions(mockClient),
|
||||
);
|
||||
};
|
||||
|
||||
it("should be able to verify user", async () => {
|
||||
const notMeId = "@notMe";
|
||||
const notMetMember = new RoomMember(defaultRoomId, notMeId);
|
||||
const device1 = new Device({
|
||||
deviceId: "d1",
|
||||
userId: notMeId,
|
||||
displayName: "my device",
|
||||
algorithms: [],
|
||||
keys: new Map(),
|
||||
});
|
||||
|
||||
// mock the user as not verified
|
||||
jest.spyOn(mockCrypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false),
|
||||
);
|
||||
|
||||
jest.spyOn(mockClient, "getUserId").mockReturnValue(defaultMember.userId);
|
||||
|
||||
// the selected user is not the default user, so he can make user verification
|
||||
const { result } = renderUserInfoHeaderVerificationHook({ member: notMetMember, devices: [device1] });
|
||||
await waitFor(() => {
|
||||
const canVerify = result.current.canVerify;
|
||||
|
||||
expect(canVerify).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not be able to verify user if user is not me", async () => {
|
||||
const device1 = new Device({
|
||||
deviceId: "d1",
|
||||
userId: defaultMember.userId,
|
||||
displayName: "my device",
|
||||
algorithms: [],
|
||||
keys: new Map(),
|
||||
});
|
||||
|
||||
// mock the user as not verified
|
||||
jest.spyOn(mockCrypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false),
|
||||
);
|
||||
|
||||
jest.spyOn(mockClient, "getUserId").mockReturnValue(defaultMember.userId);
|
||||
|
||||
const { result } = renderUserInfoHeaderVerificationHook({ member: defaultMember, devices: [device1] });
|
||||
await waitFor(() => {
|
||||
const canVerify = result.current.canVerify;
|
||||
|
||||
expect(canVerify).toBeFalsy();
|
||||
// if we cant verify the user the hasCrossSigningKeys value should also be undefined
|
||||
expect(result.current.hasCrossSigningKeys).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not be able to verify user if im already verified", async () => {
|
||||
const notMeId = "@notMe";
|
||||
const notMetMember = new RoomMember(defaultRoomId, notMeId);
|
||||
const device1 = new Device({
|
||||
deviceId: "d1",
|
||||
userId: notMeId,
|
||||
displayName: "my device",
|
||||
algorithms: [],
|
||||
keys: new Map(),
|
||||
});
|
||||
|
||||
// mock the user as already verified
|
||||
jest.spyOn(mockCrypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(true, true, false),
|
||||
);
|
||||
|
||||
jest.spyOn(mockClient, "getUserId").mockReturnValue(defaultMember.userId);
|
||||
|
||||
// the selected user is not the default user, so he can make user verification
|
||||
const { result } = renderUserInfoHeaderVerificationHook({ member: notMetMember, devices: [device1] });
|
||||
await waitFor(() => {
|
||||
const canVerify = result.current.canVerify;
|
||||
|
||||
expect(canVerify).toBeFalsy();
|
||||
// if we cant verify the user the hasCrossSigningKeys value should also be undefined
|
||||
expect(result.current.hasCrossSigningKeys).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not be able to verify user there is no devices", async () => {
|
||||
const notMeId = "@notMe";
|
||||
const notMetMember = new RoomMember(defaultRoomId, notMeId);
|
||||
|
||||
// mock the user as not verified
|
||||
jest.spyOn(mockCrypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false),
|
||||
);
|
||||
|
||||
jest.spyOn(mockClient, "getUserId").mockReturnValue(defaultMember.userId);
|
||||
|
||||
// the selected user is not the default user, so he can make user verification
|
||||
const { result } = renderUserInfoHeaderVerificationHook({ member: notMetMember, devices: [] });
|
||||
await waitFor(() => {
|
||||
const canVerify = result.current.canVerify;
|
||||
|
||||
expect(canVerify).toBeFalsy();
|
||||
// if we cant verify the user the hasCrossSigningKeys value should also be undefined
|
||||
expect(result.current.hasCrossSigningKeys).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get correct hasCrossSigningKeys values", async () => {
|
||||
const notMeId = "@notMe";
|
||||
const notMetMember = new RoomMember(defaultRoomId, notMeId);
|
||||
const device1 = new Device({
|
||||
deviceId: "d1",
|
||||
userId: notMeId,
|
||||
displayName: "my device",
|
||||
algorithms: [],
|
||||
keys: new Map(),
|
||||
});
|
||||
|
||||
// mock the user as not verified
|
||||
jest.spyOn(mockCrypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false),
|
||||
);
|
||||
|
||||
jest.spyOn(mockClient, "getUserId").mockReturnValue(defaultMember.userId);
|
||||
|
||||
jest.spyOn(mockCrypto, "userHasCrossSigningKeys").mockResolvedValue(true);
|
||||
const { result } = renderUserInfoHeaderVerificationHook({ member: notMetMember, devices: [device1] });
|
||||
await waitFor(() => {
|
||||
const hasCrossSigningKeys = result.current.hasCrossSigningKeys;
|
||||
|
||||
expect(hasCrossSigningKeys).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
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 { type MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked, type Mocked } from "jest-mock";
|
||||
import { type CryptoApi } from "matrix-js-sdk/src/crypto-api";
|
||||
import { renderHook } from "jest-matrix-react";
|
||||
|
||||
import { withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||
import { useUserfoHeaderViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel";
|
||||
import * as UseTimezone from "../../../../../../src/hooks/useUserTimezone";
|
||||
import SdkConfig from "../../../../../../src/SdkConfig";
|
||||
import Modal from "../../../../../../src/Modal";
|
||||
import ImageView from "../../../../../../src/components/views/elements/ImageView";
|
||||
import * as Media from "../../../../../../src/customisations/Media";
|
||||
import { type IConfigOptions } from "../../../../../../src/IConfigOptions";
|
||||
|
||||
jest.mock("../../../../../../src/customisations/UserIdentifier", () => {
|
||||
return {
|
||||
getDisplayUserIdentifier: jest.fn().mockReturnValue("customUserIdentifier"),
|
||||
};
|
||||
});
|
||||
|
||||
describe("useUserInfoHeaderViewModel", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
|
||||
const defaultProps = {
|
||||
member: defaultMember,
|
||||
roomId: defaultRoomId,
|
||||
};
|
||||
|
||||
let mockClient: Mocked<MatrixClient>;
|
||||
let mockCrypto: Mocked<CryptoApi>;
|
||||
|
||||
const mockAvatarUrl = "mock-avatar-url";
|
||||
const oldGet = SdkConfig.get;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCrypto = mocked({
|
||||
getDeviceVerificationStatus: jest.fn(),
|
||||
getUserDeviceInfo: jest.fn(),
|
||||
userHasCrossSigningKeys: jest.fn().mockResolvedValue(false),
|
||||
getUserVerificationStatus: jest.fn(),
|
||||
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false),
|
||||
} as unknown as CryptoApi);
|
||||
|
||||
mockClient = mocked({
|
||||
getUser: jest.fn(),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
isUserIgnored: jest.fn(),
|
||||
getIgnoredUsers: jest.fn(),
|
||||
setIgnoredUsers: jest.fn(),
|
||||
getUserId: jest.fn(),
|
||||
getSafeUserId: jest.fn(),
|
||||
getDomain: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false),
|
||||
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false),
|
||||
getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue(mockAvatarUrl),
|
||||
removeListener: jest.fn(),
|
||||
currentState: {
|
||||
on: jest.fn(),
|
||||
},
|
||||
getRoom: jest.fn(),
|
||||
credentials: {},
|
||||
setPowerLevel: jest.fn(),
|
||||
getCrypto: jest.fn().mockReturnValue(mockCrypto),
|
||||
baseUrl: "homeserver.url",
|
||||
} as unknown as MatrixClient);
|
||||
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const renderUserInfoHeaderViewModelHook = (props = defaultProps) => {
|
||||
return renderHook(() => useUserfoHeaderViewModel(props), withClientContextRenderOptions(mockClient));
|
||||
};
|
||||
it("should give user timezone info", () => {
|
||||
const defaultTZ = { timezone: "FR", friendly: "fr" };
|
||||
jest.spyOn(UseTimezone, "useUserTimezone").mockReturnValue(defaultTZ);
|
||||
|
||||
const { result } = renderUserInfoHeaderViewModelHook();
|
||||
const timezone = result.current.timezoneInfo;
|
||||
|
||||
expect(UseTimezone.useUserTimezone).toHaveBeenCalledWith(mockClient, defaultMember.userId);
|
||||
expect(timezone).toEqual(defaultTZ);
|
||||
});
|
||||
|
||||
it("should give correct showPresence value based on enablePresenceByHsUrl", () => {
|
||||
jest.spyOn(SdkConfig, "get").mockImplementation((key: string) => {
|
||||
if (key === "enable_presence_by_hs_url") {
|
||||
return {
|
||||
[mockClient.baseUrl]: false,
|
||||
};
|
||||
}
|
||||
return oldGet(key as keyof IConfigOptions);
|
||||
});
|
||||
const { result } = renderUserInfoHeaderViewModelHook();
|
||||
const showPresence = result.current.showPresence;
|
||||
expect(showPresence).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should have default value true for showPresence", () => {
|
||||
jest.spyOn(SdkConfig, "get").mockImplementation(() => false);
|
||||
const { result } = renderUserInfoHeaderViewModelHook();
|
||||
const showPresence = result.current.showPresence;
|
||||
expect(showPresence).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should open image dialog when avatar is clicked", () => {
|
||||
const props = Object.assign({}, defaultProps);
|
||||
const spyModale = jest.spyOn(Modal, "createDialog");
|
||||
const spyMedia = jest.spyOn(Media, "mediaFromMxc");
|
||||
jest.spyOn(props.member, "getMxcAvatarUrl").mockReturnValue(mockAvatarUrl);
|
||||
|
||||
const { result } = renderUserInfoHeaderViewModelHook(props);
|
||||
|
||||
result.current.onMemberAvatarClick();
|
||||
|
||||
expect(spyModale).toHaveBeenCalledWith(
|
||||
ImageView,
|
||||
{
|
||||
src: mockAvatarUrl,
|
||||
name: defaultMember.name,
|
||||
},
|
||||
"mx_Dialog_lightbox",
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
expect(spyMedia).toHaveBeenCalledWith(mockAvatarUrl);
|
||||
});
|
||||
|
||||
it("should not open image dialog when avatar url is null", () => {
|
||||
const props = Object.assign({}, defaultProps);
|
||||
const spyModale = jest.spyOn(Modal, "createDialog");
|
||||
jest.spyOn(props.member, "getMxcAvatarUrl").mockReturnValue(mockAvatarUrl);
|
||||
jest.spyOn(Media, "mediaFromMxc").mockReturnValue({
|
||||
srcHttp: null,
|
||||
isEncrypted: false,
|
||||
srcMxc: "",
|
||||
thumbnailMxc: undefined,
|
||||
hasThumbnail: false,
|
||||
thumbnailHttp: null,
|
||||
getThumbnailHttp: function (width: number, height: number, mode?: "scale" | "crop"): string | null {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
getThumbnailOfSourceHttp: function (width: number, height: number, mode?: "scale" | "crop"): string | null {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
getSquareThumbnailHttp: function (dim: number): string | null {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
downloadSource: function (): Promise<Response> {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderUserInfoHeaderViewModelHook(props);
|
||||
|
||||
result.current.onMemberAvatarClick();
|
||||
|
||||
expect(spyModale).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { renderHook } from "jest-matrix-react";
|
||||
import { type Mocked, mocked } from "jest-mock";
|
||||
import { RoomMember, MatrixEvent, type Room, EventType, type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||
import { useUserInfoPowerlevelViewModel } from "../../../../../../src/components/viewmodels/right_panel/UserInfoPowerlevelViewModel";
|
||||
import { withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import { type IRoomPermissions } from "../../../../../../src/components/views/right_panel/UserInfo";
|
||||
import Modal from "../../../../../../src/Modal";
|
||||
import { warnSelfDemote } from "../../../../../../src/components/views/right_panel/UserInfo";
|
||||
|
||||
jest.mock("../../../../../../src/Modal", () => ({
|
||||
createDialog: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../../src/components/views/right_panel/UserInfo", () => ({
|
||||
warnSelfDemote: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("UserInfoAdminPowerlevelViewModel", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
const defaultMeId = "@me:example.com";
|
||||
const selfUser = new RoomMember(defaultRoomId, defaultMeId);
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
const startPowerLevel = 50;
|
||||
const changedPowerLevel = 100;
|
||||
|
||||
let mockClient: Mocked<MatrixClient>;
|
||||
let mockRoom: Mocked<Room>;
|
||||
let defaultProps: {
|
||||
user: RoomMember;
|
||||
room: Room;
|
||||
roomPermissions: IRoomPermissions;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
defaultProps = {
|
||||
user: defaultMember,
|
||||
room: mockRoom,
|
||||
roomPermissions: {
|
||||
modifyLevelMax: 100,
|
||||
canEdit: false,
|
||||
canInvite: false,
|
||||
},
|
||||
};
|
||||
|
||||
mockRoom = mocked({
|
||||
roomId: defaultRoomId,
|
||||
getType: jest.fn().mockReturnValue(undefined),
|
||||
isSpaceRoom: jest.fn().mockReturnValue(false),
|
||||
getMember: jest.fn().mockReturnValue(undefined),
|
||||
getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"),
|
||||
name: "test room",
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
currentState: {
|
||||
getStateEvents: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
},
|
||||
getEventReadUpTo: jest.fn(),
|
||||
} as unknown as Room);
|
||||
|
||||
mockClient = mocked({
|
||||
getUser: jest.fn(),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
isUserIgnored: jest.fn(),
|
||||
getIgnoredUsers: jest.fn(),
|
||||
setIgnoredUsers: jest.fn(),
|
||||
getUserId: jest.fn(),
|
||||
getSafeUserId: jest.fn(),
|
||||
getDomain: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false),
|
||||
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false),
|
||||
getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
removeListener: jest.fn(),
|
||||
currentState: {
|
||||
on: jest.fn(),
|
||||
},
|
||||
getRoom: jest.fn(),
|
||||
credentials: {},
|
||||
setPowerLevel: jest.fn().mockResolvedValueOnce({ event_id: "123" }),
|
||||
} as unknown as MatrixClient);
|
||||
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
|
||||
|
||||
(Modal.createDialog as jest.Mock).mockImplementation(() => ({
|
||||
finished: Promise.resolve([true]),
|
||||
}));
|
||||
(warnSelfDemote as jest.Mock).mockResolvedValue(true);
|
||||
});
|
||||
|
||||
const renderComponentHook = (props = defaultProps, client = mockClient) => {
|
||||
return renderHook(
|
||||
() => useUserInfoPowerlevelViewModel(props.user, props.room),
|
||||
withClientContextRenderOptions(client),
|
||||
);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should give default power level", () => {
|
||||
const defaultPowerLevel = 1;
|
||||
const powerLevelEvent = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: { users: { [defaultUserId]: defaultPowerLevel }, users_default: defaultPowerLevel },
|
||||
});
|
||||
mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent);
|
||||
|
||||
const { result } = renderComponentHook({ ...defaultProps, room: mockRoom });
|
||||
|
||||
expect(result.current.powerLevelUsersDefault).toBe(defaultPowerLevel);
|
||||
});
|
||||
|
||||
it("handles successful power level change", async () => {
|
||||
const powerLevelEvent = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: { users: { [defaultUserId]: startPowerLevel }, users_default: 1 },
|
||||
});
|
||||
mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent);
|
||||
mockClient.getSafeUserId.mockReturnValueOnce(defaultUserId);
|
||||
mockClient.getUserId.mockReturnValueOnce(defaultUserId);
|
||||
|
||||
const { result } = renderComponentHook({ ...defaultProps, room: mockRoom }, mockClient);
|
||||
|
||||
await result.current.onPowerChange(changedPowerLevel);
|
||||
|
||||
expect(mockClient.setPowerLevel).toHaveBeenCalledTimes(1);
|
||||
expect(mockClient.setPowerLevel).toHaveBeenCalledWith(mockRoom.roomId, defaultMember.userId, changedPowerLevel);
|
||||
});
|
||||
|
||||
it("shows warning when promoting user to higher power level", async () => {
|
||||
const powerLevelEvent = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: {
|
||||
users: {
|
||||
[defaultUserId]: startPowerLevel,
|
||||
[defaultMeId]: startPowerLevel,
|
||||
},
|
||||
users_default: 1,
|
||||
},
|
||||
});
|
||||
mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent);
|
||||
mockClient.getUserId.mockReturnValue(defaultMeId);
|
||||
|
||||
const { result } = renderComponentHook({ ...defaultProps, room: mockRoom }, mockClient);
|
||||
|
||||
await result.current.onPowerChange(changedPowerLevel);
|
||||
|
||||
expect(Modal.createDialog).toHaveBeenCalled();
|
||||
expect(mockClient.setPowerLevel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows warning when self-demoting", async () => {
|
||||
const powerLevelEvent = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: {
|
||||
users: { [defaultMeId]: changedPowerLevel },
|
||||
users_default: 1,
|
||||
},
|
||||
});
|
||||
mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent);
|
||||
mockClient.getUserId.mockReturnValue(defaultMeId);
|
||||
|
||||
const { result } = renderComponentHook({ ...defaultProps, room: mockRoom, user: selfUser }, mockClient);
|
||||
|
||||
await result.current.onPowerChange(startPowerLevel);
|
||||
|
||||
expect(warnSelfDemote).toHaveBeenCalled();
|
||||
expect(mockClient.setPowerLevel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cancels power level change when user declines warning", async () => {
|
||||
(Modal.createDialog as jest.Mock).mockImplementation(() => ({
|
||||
finished: Promise.resolve([false]),
|
||||
}));
|
||||
|
||||
const powerLevelEvent = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: {
|
||||
users: {
|
||||
[defaultUserId]: startPowerLevel,
|
||||
"@me:example.com": startPowerLevel,
|
||||
},
|
||||
users_default: 1,
|
||||
},
|
||||
});
|
||||
mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent);
|
||||
mockClient.getUserId.mockReturnValue(defaultMeId);
|
||||
|
||||
const { result } = renderComponentHook({ ...defaultProps, room: mockRoom }, mockClient);
|
||||
|
||||
await result.current.onPowerChange(changedPowerLevel);
|
||||
|
||||
expect(Modal.createDialog).toHaveBeenCalled();
|
||||
expect(mockClient.setPowerLevel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles missing power level event", async () => {
|
||||
mockRoom.currentState.getStateEvents.mockReturnValue(null);
|
||||
|
||||
const { result } = renderComponentHook({ ...defaultProps, room: mockRoom }, mockClient);
|
||||
|
||||
await result.current.onPowerChange(changedPowerLevel);
|
||||
|
||||
expect(mockClient.setPowerLevel).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
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 { renderHook } from "jest-matrix-react";
|
||||
import { type Mocked, mocked } from "jest-mock";
|
||||
import { type Room, type MatrixClient, RoomMember, type IPowerLevelsContent } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
|
||||
import {
|
||||
type RoomAdminToolsContainerProps,
|
||||
useUserInfoAdminToolsContainerViewModel,
|
||||
} from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel";
|
||||
import { withClientContextRenderOptions } from "../../../../../../test-utils";
|
||||
|
||||
describe("UserInfoAdminToolsContainerViewModel", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
|
||||
let mockRoom: Mocked<Room>;
|
||||
let mockClient: Mocked<MatrixClient>;
|
||||
let mockPowerLevels: IPowerLevelsContent;
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
|
||||
let defaultContainerProps: RoomAdminToolsContainerProps;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRoom = mocked({
|
||||
roomId: defaultRoomId,
|
||||
getType: jest.fn().mockReturnValue(undefined),
|
||||
isSpaceRoom: jest.fn().mockReturnValue(false),
|
||||
getMember: jest.fn().mockReturnValue(undefined),
|
||||
getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"),
|
||||
name: "test room",
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
currentState: {
|
||||
getStateEvents: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
},
|
||||
getEventReadUpTo: jest.fn(),
|
||||
} as unknown as Room);
|
||||
|
||||
mockPowerLevels = {
|
||||
users: {
|
||||
"@currentuser:example.com": 100,
|
||||
},
|
||||
events: {},
|
||||
state_default: 50,
|
||||
ban: 50,
|
||||
kick: 50,
|
||||
redact: 50,
|
||||
};
|
||||
|
||||
defaultContainerProps = {
|
||||
room: mockRoom,
|
||||
member: defaultMember,
|
||||
powerLevels: mockPowerLevels,
|
||||
};
|
||||
|
||||
mockClient = mocked({
|
||||
getUser: jest.fn(),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
isUserIgnored: jest.fn(),
|
||||
getIgnoredUsers: jest.fn(),
|
||||
setIgnoredUsers: jest.fn(),
|
||||
getUserId: jest.fn().mockReturnValue(defaultUserId),
|
||||
getSafeUserId: jest.fn(),
|
||||
getDomain: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false),
|
||||
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false),
|
||||
getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
removeListener: jest.fn(),
|
||||
currentState: {
|
||||
on: jest.fn(),
|
||||
},
|
||||
getRoom: jest.fn(),
|
||||
credentials: {},
|
||||
setPowerLevel: jest.fn(),
|
||||
} as unknown as MatrixClient);
|
||||
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
|
||||
});
|
||||
|
||||
const renderAdminToolsContainerHook = (props = defaultContainerProps) => {
|
||||
return renderHook(
|
||||
() => useUserInfoAdminToolsContainerViewModel(props),
|
||||
withClientContextRenderOptions(mockClient),
|
||||
);
|
||||
};
|
||||
|
||||
describe("useUserInfoAdminToolsContainerViewModel", () => {
|
||||
it("should return false when user is not in the room", () => {
|
||||
mockRoom.getMember.mockReturnValue(null);
|
||||
const { result } = renderAdminToolsContainerHook();
|
||||
expect(result.current).toEqual({
|
||||
isCurrentUserInTheRoom: false,
|
||||
shouldShowKickButton: false,
|
||||
shouldShowBanButton: false,
|
||||
shouldShowMuteButton: false,
|
||||
shouldShowRedactButton: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should not show kick, ban and mute buttons if user is me", () => {
|
||||
const mockMeMember = new RoomMember(mockRoom.roomId, "arbitraryId");
|
||||
mockMeMember.powerLevel = 51; // defaults to 50
|
||||
mockRoom.getMember.mockReturnValueOnce(mockMeMember);
|
||||
|
||||
const props = {
|
||||
...defaultContainerProps,
|
||||
room: mockRoom,
|
||||
member: mockMeMember,
|
||||
powerLevels: mockPowerLevels,
|
||||
};
|
||||
const { result } = renderAdminToolsContainerHook(props);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
isCurrentUserInTheRoom: true,
|
||||
shouldShowKickButton: false,
|
||||
shouldShowBanButton: false,
|
||||
shouldShowMuteButton: false,
|
||||
shouldShowRedactButton: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns mute toggle button if conditions met", () => {
|
||||
const mockMeMember = new RoomMember(mockRoom.roomId, "arbitraryId");
|
||||
mockMeMember.powerLevel = 51; // defaults to 50
|
||||
mockRoom.getMember.mockReturnValueOnce(mockMeMember);
|
||||
|
||||
const defaultMemberWithPowerLevelAndJoinMembership = {
|
||||
...defaultMember,
|
||||
powerLevel: 0,
|
||||
membership: KnownMembership.Join,
|
||||
} as RoomMember;
|
||||
|
||||
const { result } = renderAdminToolsContainerHook({
|
||||
...defaultContainerProps,
|
||||
member: defaultMemberWithPowerLevelAndJoinMembership,
|
||||
powerLevels: { events: { "m.room.power_levels": 1 } },
|
||||
});
|
||||
|
||||
expect(result.current.shouldShowMuteButton).toBe(true);
|
||||
});
|
||||
|
||||
it("should not show mute button for one's own member", () => {
|
||||
const mockMeMember = new RoomMember(mockRoom.roomId, mockClient.getSafeUserId());
|
||||
mockMeMember.powerLevel = 51; // defaults to 50
|
||||
mockRoom.getMember.mockReturnValueOnce(mockMeMember);
|
||||
mockClient.getUserId.mockReturnValueOnce(mockMeMember.userId);
|
||||
|
||||
const { result } = renderAdminToolsContainerHook({
|
||||
...defaultContainerProps,
|
||||
member: mockMeMember,
|
||||
powerLevels: { events: { "m.room.power_levels": 100 } },
|
||||
});
|
||||
|
||||
expect(result.current.shouldShowMuteButton).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
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 { cleanup, renderHook } from "jest-matrix-react";
|
||||
import { type Mocked, mocked } from "jest-mock";
|
||||
import { type Room, type MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
|
||||
import { type RoomAdminToolsProps } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel";
|
||||
import { useBanButtonViewModel } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel";
|
||||
import Modal from "../../../../../../../src/Modal";
|
||||
import { withClientContextRenderOptions } from "../../../../../../test-utils";
|
||||
|
||||
describe("useBanButtonViewModel", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
|
||||
let mockRoom: Mocked<Room>;
|
||||
let mockSpace: Mocked<Room>;
|
||||
let mockClient: Mocked<MatrixClient>;
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
|
||||
const memberWithBanMembership = { ...defaultMember, membership: KnownMembership.Ban } as RoomMember;
|
||||
|
||||
let defaultAdminToolsProps: RoomAdminToolsProps;
|
||||
const createDialogSpy: jest.SpyInstance = jest.spyOn(Modal, "createDialog");
|
||||
|
||||
beforeEach(() => {
|
||||
mockRoom = mocked({
|
||||
roomId: defaultRoomId,
|
||||
getType: jest.fn().mockReturnValue(undefined),
|
||||
isSpaceRoom: jest.fn().mockReturnValue(false),
|
||||
getMember: jest.fn().mockReturnValue(undefined),
|
||||
getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"),
|
||||
name: "test room",
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
currentState: {
|
||||
getStateEvents: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
},
|
||||
getEventReadUpTo: jest.fn(),
|
||||
} as unknown as Room);
|
||||
|
||||
mockSpace = mocked({
|
||||
roomId: defaultRoomId,
|
||||
getType: jest.fn().mockReturnValue("m.space"),
|
||||
isSpaceRoom: jest.fn().mockReturnValue(true),
|
||||
getMember: jest.fn().mockReturnValue(undefined),
|
||||
getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"),
|
||||
name: "test room",
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
currentState: {
|
||||
getStateEvents: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
},
|
||||
getEventReadUpTo: jest.fn(),
|
||||
} as unknown as Room);
|
||||
|
||||
defaultAdminToolsProps = {
|
||||
room: mockRoom,
|
||||
member: defaultMember,
|
||||
isUpdating: false,
|
||||
startUpdating: jest.fn(),
|
||||
stopUpdating: jest.fn(),
|
||||
};
|
||||
|
||||
mockClient = mocked({
|
||||
getUser: jest.fn(),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
isUserIgnored: jest.fn(),
|
||||
getIgnoredUsers: jest.fn(),
|
||||
setIgnoredUsers: jest.fn(),
|
||||
getUserId: jest.fn().mockReturnValue(defaultUserId),
|
||||
getSafeUserId: jest.fn(),
|
||||
getDomain: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false),
|
||||
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false),
|
||||
getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
removeListener: jest.fn(),
|
||||
currentState: {
|
||||
on: jest.fn(),
|
||||
},
|
||||
getRoom: jest.fn(),
|
||||
credentials: {},
|
||||
setPowerLevel: jest.fn(),
|
||||
} as unknown as MatrixClient);
|
||||
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
|
||||
mockRoom.getMember.mockReturnValue(defaultMember);
|
||||
});
|
||||
|
||||
const renderBanButtonHook = (props = defaultAdminToolsProps) => {
|
||||
return renderHook(() => useBanButtonViewModel(props), withClientContextRenderOptions(mockClient));
|
||||
};
|
||||
|
||||
it("renders the correct labels for banned and unbanned members", () => {
|
||||
// test for room
|
||||
const propsWithBanMembership = {
|
||||
...defaultAdminToolsProps,
|
||||
member: memberWithBanMembership,
|
||||
};
|
||||
|
||||
// defaultMember is not banned
|
||||
const { result } = renderBanButtonHook();
|
||||
expect(result.current.banLabel).toBe("Ban from room");
|
||||
cleanup();
|
||||
|
||||
const { result: result2 } = renderBanButtonHook(propsWithBanMembership);
|
||||
expect(result2.current.banLabel).toBe("Unban from room");
|
||||
cleanup();
|
||||
|
||||
// test for space
|
||||
const { result: result3 } = renderBanButtonHook({ ...defaultAdminToolsProps, room: mockSpace });
|
||||
expect(result3.current.banLabel).toBe("Ban from space");
|
||||
cleanup();
|
||||
|
||||
const { result: result4 } = renderBanButtonHook({
|
||||
...propsWithBanMembership,
|
||||
room: mockSpace,
|
||||
});
|
||||
expect(result4.current.banLabel).toBe("Unban from space");
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("clicking the ban or unban button calls Modal.createDialog with the correct arguments if user is not banned", async () => {
|
||||
createDialogSpy.mockReturnValueOnce({ finished: Promise.resolve([]), close: jest.fn() });
|
||||
|
||||
const propsWithSpace = {
|
||||
...defaultAdminToolsProps,
|
||||
room: mockSpace,
|
||||
};
|
||||
const { result } = renderBanButtonHook(propsWithSpace);
|
||||
await result.current.onBanOrUnbanClick();
|
||||
|
||||
// check the last call arguments and the presence of the spaceChildFilter callback
|
||||
expect(createDialogSpy).toHaveBeenLastCalledWith(
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ spaceChildFilter: expect.any(Function) }),
|
||||
"mx_ConfirmSpaceUserActionDialog_wrapper",
|
||||
);
|
||||
|
||||
// test the spaceChildFilter callback
|
||||
const callback = createDialogSpy.mock.lastCall[1].spaceChildFilter;
|
||||
|
||||
// make dummy values for myMember and theirMember, then we will test
|
||||
// null vs their member followed by
|
||||
// truthy my member vs their member
|
||||
const mockMyMember = { powerLevel: 1 };
|
||||
const mockTheirMember = { membership: "is not ban", powerLevel: 0 };
|
||||
|
||||
const mockRoom = {
|
||||
getMember: jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(null)
|
||||
.mockReturnValueOnce(mockTheirMember)
|
||||
.mockReturnValueOnce(mockMyMember)
|
||||
.mockReturnValueOnce(mockTheirMember),
|
||||
currentState: {
|
||||
hasSufficientPowerLevelFor: jest.fn().mockReturnValue(true),
|
||||
},
|
||||
};
|
||||
|
||||
expect(callback(mockRoom)).toBe(false);
|
||||
expect(callback(mockRoom)).toBe(true);
|
||||
});
|
||||
|
||||
it("clicking the ban or unban button calls Modal.createDialog with the correct arguments if user _is_ banned", async () => {
|
||||
createDialogSpy.mockReturnValueOnce({ finished: Promise.resolve([]), close: jest.fn() });
|
||||
|
||||
const propsWithBanMembership = {
|
||||
...defaultAdminToolsProps,
|
||||
member: memberWithBanMembership,
|
||||
room: mockSpace,
|
||||
};
|
||||
const { result } = renderBanButtonHook(propsWithBanMembership);
|
||||
await result.current.onBanOrUnbanClick();
|
||||
|
||||
// check the last call arguments and the presence of the spaceChildFilter callback
|
||||
expect(createDialogSpy).toHaveBeenLastCalledWith(
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ spaceChildFilter: expect.any(Function) }),
|
||||
"mx_ConfirmSpaceUserActionDialog_wrapper",
|
||||
);
|
||||
|
||||
// test the spaceChildFilter callback
|
||||
const callback = createDialogSpy.mock.lastCall[1].spaceChildFilter;
|
||||
|
||||
// make dummy values for myMember and theirMember, then we will test
|
||||
// null vs their member followed by
|
||||
// my member vs their member
|
||||
const mockMyMember = { powerLevel: 1 };
|
||||
const mockTheirMember = { membership: KnownMembership.Ban, powerLevel: 0 };
|
||||
|
||||
const mockRoom = {
|
||||
getMember: jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(null)
|
||||
.mockReturnValueOnce(mockTheirMember)
|
||||
.mockReturnValueOnce(mockMyMember)
|
||||
.mockReturnValueOnce(mockTheirMember),
|
||||
currentState: {
|
||||
hasSufficientPowerLevelFor: jest.fn().mockReturnValue(true),
|
||||
},
|
||||
};
|
||||
|
||||
expect(callback(mockRoom)).toBe(false);
|
||||
expect(callback(mockRoom)).toBe(true);
|
||||
});
|
||||
});
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
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 { cleanup, renderHook } from "jest-matrix-react";
|
||||
import { type Mocked, mocked } from "jest-mock";
|
||||
import { type Room, type MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
|
||||
import { type RoomAdminToolsProps } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel";
|
||||
import { useRoomKickButtonViewModel } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel";
|
||||
import Modal from "../../../../../../../src/Modal";
|
||||
import { withClientContextRenderOptions } from "../../../../../../test-utils";
|
||||
|
||||
describe("useRoomKickButtonViewModel", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
|
||||
let mockRoom: Mocked<Room>;
|
||||
let mockSpace: Mocked<Room>;
|
||||
let mockClient: Mocked<MatrixClient>;
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
const memberWithInviteMembership = { ...defaultMember, membership: KnownMembership.Invite } as RoomMember;
|
||||
const memberWithJoinMembership = { ...defaultMember, membership: KnownMembership.Join } as RoomMember;
|
||||
|
||||
const createDialogSpy: jest.SpyInstance = jest.spyOn(Modal, "createDialog");
|
||||
|
||||
let defaultAdminToolsProps: RoomAdminToolsProps;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRoom = mocked({
|
||||
roomId: defaultRoomId,
|
||||
getType: jest.fn().mockReturnValue(undefined),
|
||||
isSpaceRoom: jest.fn().mockReturnValue(false),
|
||||
getMember: jest.fn().mockReturnValue(undefined),
|
||||
getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"),
|
||||
name: "test room",
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
currentState: {
|
||||
getStateEvents: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
},
|
||||
getEventReadUpTo: jest.fn(),
|
||||
} as unknown as Room);
|
||||
|
||||
mockSpace = mocked({
|
||||
roomId: defaultRoomId,
|
||||
getType: jest.fn().mockReturnValue("m.space"),
|
||||
isSpaceRoom: jest.fn().mockReturnValue(true),
|
||||
getMember: jest.fn().mockReturnValue(undefined),
|
||||
getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"),
|
||||
name: "test room",
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
currentState: {
|
||||
getStateEvents: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
},
|
||||
getEventReadUpTo: jest.fn(),
|
||||
} as unknown as Room);
|
||||
|
||||
defaultAdminToolsProps = {
|
||||
room: mockRoom,
|
||||
member: defaultMember,
|
||||
isUpdating: false,
|
||||
startUpdating: jest.fn(),
|
||||
stopUpdating: jest.fn(),
|
||||
};
|
||||
|
||||
mockClient = mocked({
|
||||
getUser: jest.fn(),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
isUserIgnored: jest.fn(),
|
||||
getIgnoredUsers: jest.fn(),
|
||||
setIgnoredUsers: jest.fn(),
|
||||
getUserId: jest.fn().mockReturnValue(defaultUserId),
|
||||
getSafeUserId: jest.fn(),
|
||||
getDomain: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false),
|
||||
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false),
|
||||
getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
removeListener: jest.fn(),
|
||||
currentState: {
|
||||
on: jest.fn(),
|
||||
},
|
||||
getRoom: jest.fn(),
|
||||
credentials: {},
|
||||
setPowerLevel: jest.fn(),
|
||||
} as unknown as MatrixClient);
|
||||
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
|
||||
// mock useContext to return mockClient
|
||||
// jest.spyOn(React, "useContext").mockReturnValue(mockClient);
|
||||
|
||||
mockRoom.getMember.mockReturnValue(defaultMember);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
createDialogSpy.mockReset();
|
||||
});
|
||||
|
||||
const renderKickButtonHook = (props = defaultAdminToolsProps) => {
|
||||
return renderHook(() => useRoomKickButtonViewModel(props), withClientContextRenderOptions(mockClient));
|
||||
};
|
||||
|
||||
it("renders nothing if member.membership is undefined", () => {
|
||||
// .membership is undefined in our member by default
|
||||
const { result } = renderKickButtonHook();
|
||||
expect(result.current.canUserBeKicked).toBe(false);
|
||||
});
|
||||
|
||||
it("renders something if member.membership is 'invite' or 'join'", () => {
|
||||
let props = {
|
||||
...defaultAdminToolsProps,
|
||||
member: memberWithInviteMembership,
|
||||
};
|
||||
const { result } = renderKickButtonHook(props);
|
||||
expect(result.current.canUserBeKicked).toBe(true);
|
||||
|
||||
cleanup();
|
||||
|
||||
props = {
|
||||
...defaultAdminToolsProps,
|
||||
member: memberWithJoinMembership,
|
||||
};
|
||||
const { result: result2 } = renderKickButtonHook(props);
|
||||
expect(result2.current.canUserBeKicked).toBe(true);
|
||||
});
|
||||
|
||||
it("renders the correct label", () => {
|
||||
// test for room
|
||||
const propsWithJoinMembership = {
|
||||
...defaultAdminToolsProps,
|
||||
member: memberWithJoinMembership,
|
||||
};
|
||||
|
||||
const { result } = renderKickButtonHook(propsWithJoinMembership);
|
||||
expect(result.current.kickLabel).toBe("Remove from room");
|
||||
cleanup();
|
||||
|
||||
const propsWithInviteMembership = {
|
||||
...defaultAdminToolsProps,
|
||||
member: memberWithInviteMembership,
|
||||
};
|
||||
|
||||
const { result: result2 } = renderKickButtonHook(propsWithInviteMembership);
|
||||
expect(result2.current.kickLabel).toBe("Disinvite from room");
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("renders the correct label for space", () => {
|
||||
const propsWithInviteMembership = {
|
||||
...defaultAdminToolsProps,
|
||||
room: mockSpace,
|
||||
member: memberWithInviteMembership,
|
||||
};
|
||||
|
||||
const propsWithJoinMembership = {
|
||||
...defaultAdminToolsProps,
|
||||
room: mockSpace,
|
||||
member: memberWithJoinMembership,
|
||||
};
|
||||
|
||||
const { result: result3 } = renderKickButtonHook(propsWithJoinMembership);
|
||||
expect(result3.current.kickLabel).toBe("Remove from space");
|
||||
cleanup();
|
||||
|
||||
const { result: result4 } = renderKickButtonHook(propsWithInviteMembership);
|
||||
expect(result4.current.kickLabel).toBe("Disinvite from space");
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("clicking the kick button calls Modal.createDialog with the correct arguments when room is a space", async () => {
|
||||
createDialogSpy.mockReturnValueOnce({ finished: Promise.resolve([]), close: jest.fn() });
|
||||
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
|
||||
|
||||
const propsWithInviteMembership = {
|
||||
...defaultAdminToolsProps,
|
||||
room: mockSpace,
|
||||
member: memberWithInviteMembership,
|
||||
};
|
||||
const { result } = renderKickButtonHook(propsWithInviteMembership);
|
||||
|
||||
await result.current.onKickClick();
|
||||
|
||||
// check the last call arguments and the presence of the spaceChildFilter callback
|
||||
expect(createDialogSpy).toHaveBeenLastCalledWith(
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ spaceChildFilter: expect.any(Function) }),
|
||||
"mx_ConfirmSpaceUserActionDialog_wrapper",
|
||||
);
|
||||
|
||||
// test the spaceChildFilter callback
|
||||
const callback = createDialogSpy.mock.lastCall[1].spaceChildFilter;
|
||||
|
||||
// make dummy values for myMember and theirMember, then we will test
|
||||
// null vs their member followed by
|
||||
// my member vs their member
|
||||
const mockMyMember = { powerLevel: 1 };
|
||||
const mockTheirMember = { membership: KnownMembership.Invite, powerLevel: 0 };
|
||||
|
||||
const mockRoom = {
|
||||
getMember: jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(null)
|
||||
.mockReturnValueOnce(mockTheirMember)
|
||||
.mockReturnValueOnce(mockMyMember)
|
||||
.mockReturnValueOnce(mockTheirMember),
|
||||
currentState: {
|
||||
hasSufficientPowerLevelFor: jest.fn().mockReturnValue(true),
|
||||
},
|
||||
};
|
||||
|
||||
expect(callback(mockRoom)).toBe(false);
|
||||
expect(callback(mockRoom)).toBe(true);
|
||||
});
|
||||
});
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
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 { renderHook } from "jest-matrix-react";
|
||||
import { type Mocked, mocked } from "jest-mock";
|
||||
import {
|
||||
type Room,
|
||||
type MatrixClient,
|
||||
RoomMember,
|
||||
type MatrixEvent,
|
||||
type ISendEventResponse,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
|
||||
import { type RoomAdminToolsProps } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel";
|
||||
import { useMuteButtonViewModel } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel";
|
||||
import { isMuted } from "../../../../../../../src/components/views/right_panel/UserInfo";
|
||||
import { withClientContextRenderOptions } from "../../../../../../test-utils";
|
||||
|
||||
describe("useMuteButtonViewModel", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
|
||||
let mockRoom: Mocked<Room>;
|
||||
let mockClient: Mocked<MatrixClient>;
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
|
||||
let defaultAdminToolsProps: RoomAdminToolsProps;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRoom = mocked({
|
||||
roomId: defaultRoomId,
|
||||
getType: jest.fn().mockReturnValue(undefined),
|
||||
isSpaceRoom: jest.fn().mockReturnValue(false),
|
||||
getMember: jest.fn().mockReturnValue(undefined),
|
||||
getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"),
|
||||
name: "test room",
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
currentState: {
|
||||
getStateEvents: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
},
|
||||
getEventReadUpTo: jest.fn(),
|
||||
} as unknown as Room);
|
||||
|
||||
defaultAdminToolsProps = {
|
||||
room: mockRoom,
|
||||
member: defaultMember,
|
||||
isUpdating: false,
|
||||
startUpdating: jest.fn(),
|
||||
stopUpdating: jest.fn(),
|
||||
};
|
||||
|
||||
mockClient = mocked({
|
||||
getUser: jest.fn(),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
isUserIgnored: jest.fn(),
|
||||
getIgnoredUsers: jest.fn(),
|
||||
setIgnoredUsers: jest.fn(),
|
||||
getUserId: jest.fn().mockReturnValue(defaultUserId),
|
||||
getSafeUserId: jest.fn(),
|
||||
getDomain: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false),
|
||||
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false),
|
||||
getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
removeListener: jest.fn(),
|
||||
currentState: {
|
||||
on: jest.fn(),
|
||||
},
|
||||
getRoom: jest.fn(),
|
||||
credentials: {},
|
||||
setPowerLevel: jest.fn(),
|
||||
} as unknown as MatrixClient);
|
||||
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
|
||||
|
||||
mockClient.setPowerLevel.mockImplementation(() => Promise.resolve({} as ISendEventResponse));
|
||||
|
||||
mockRoom.currentState.getStateEvents.mockReturnValueOnce({
|
||||
getContent: jest.fn().mockReturnValue({
|
||||
events: {
|
||||
"m.room.message": 0,
|
||||
},
|
||||
events_default: 0,
|
||||
}),
|
||||
} as unknown as MatrixEvent);
|
||||
|
||||
jest.spyOn(mockClient, "setPowerLevel").mockImplementation(() => Promise.resolve({} as ISendEventResponse));
|
||||
jest.spyOn(mockRoom.currentState, "getStateEvents").mockReturnValue({
|
||||
getContent: jest.fn().mockReturnValue({
|
||||
events: {
|
||||
"m.room.message": 0,
|
||||
},
|
||||
events_default: 0,
|
||||
}),
|
||||
} as unknown as MatrixEvent);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderMuteButtonHook = (props = defaultAdminToolsProps) => {
|
||||
return renderHook(() => useMuteButtonViewModel(props), withClientContextRenderOptions(mockClient));
|
||||
};
|
||||
|
||||
it("should early return when isUpdating=true", async () => {
|
||||
const defaultMemberWithPowerLevelAndJoinMembership = {
|
||||
...defaultMember,
|
||||
powerLevel: 0,
|
||||
membership: KnownMembership.Join,
|
||||
} as RoomMember;
|
||||
|
||||
const { result } = renderMuteButtonHook({
|
||||
...defaultAdminToolsProps,
|
||||
member: defaultMemberWithPowerLevelAndJoinMembership,
|
||||
isUpdating: true,
|
||||
});
|
||||
|
||||
const resultClick = await result.current.onMuteButtonClick();
|
||||
|
||||
expect(resultClick).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should stop updating when level is NaN", async () => {
|
||||
const { result } = renderMuteButtonHook({
|
||||
...defaultAdminToolsProps,
|
||||
member: defaultMember,
|
||||
isUpdating: false,
|
||||
});
|
||||
|
||||
jest.spyOn(mockRoom.currentState, "getStateEvents").mockReturnValueOnce({
|
||||
getContent: jest.fn().mockReturnValue({
|
||||
events: {
|
||||
"m.room.message": NaN,
|
||||
},
|
||||
events_default: NaN,
|
||||
}),
|
||||
} as unknown as MatrixEvent);
|
||||
|
||||
await result.current.onMuteButtonClick();
|
||||
|
||||
expect(defaultAdminToolsProps.stopUpdating).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should set powerlevel to default when user is muted", async () => {
|
||||
const defaultMutedMember = {
|
||||
...defaultMember,
|
||||
powerLevel: -1,
|
||||
membership: KnownMembership.Join,
|
||||
} as RoomMember;
|
||||
|
||||
const { result } = renderMuteButtonHook({
|
||||
...defaultAdminToolsProps,
|
||||
member: defaultMutedMember,
|
||||
isUpdating: false,
|
||||
});
|
||||
|
||||
await result.current.onMuteButtonClick();
|
||||
|
||||
expect(mockClient.setPowerLevel).toHaveBeenCalledWith(mockRoom.roomId, defaultMember.userId, 0);
|
||||
});
|
||||
|
||||
it("should set powerlevel - 1 when user is unmuted", async () => {
|
||||
const defaultUnmutedMember = {
|
||||
...defaultMember,
|
||||
powerLevel: 0,
|
||||
membership: KnownMembership.Join,
|
||||
} as RoomMember;
|
||||
|
||||
const { result } = renderMuteButtonHook({
|
||||
...defaultAdminToolsProps,
|
||||
member: defaultUnmutedMember,
|
||||
isUpdating: false,
|
||||
});
|
||||
|
||||
await result.current.onMuteButtonClick();
|
||||
|
||||
expect(mockClient.setPowerLevel).toHaveBeenCalledWith(mockRoom.roomId, defaultMember.userId, -1);
|
||||
});
|
||||
|
||||
it("returns false if either argument is falsy", () => {
|
||||
// @ts-ignore to let us purposely pass incorrect args
|
||||
expect(isMuted(defaultMember, null)).toBe(false);
|
||||
// @ts-ignore to let us purposely pass incorrect args
|
||||
expect(isMuted(null, {})).toBe(false);
|
||||
});
|
||||
|
||||
it("when powerLevelContent.events and .events_default are undefined, returns false", () => {
|
||||
const powerLevelContents = {};
|
||||
expect(isMuted(defaultMember, powerLevelContents)).toBe(false);
|
||||
});
|
||||
|
||||
it("when powerLevelContent.events is undefined, uses .events_default", () => {
|
||||
const higherPowerLevelContents = { events_default: 10 };
|
||||
expect(isMuted(defaultMember, higherPowerLevelContents)).toBe(true);
|
||||
|
||||
const lowerPowerLevelContents = { events_default: -10 };
|
||||
expect(isMuted(defaultMember, lowerPowerLevelContents)).toBe(false);
|
||||
});
|
||||
|
||||
it("when powerLevelContent.events is defined but '.m.room.message' isn't, uses .events_default", () => {
|
||||
const higherPowerLevelContents = { events: {}, events_default: 10 };
|
||||
expect(isMuted(defaultMember, higherPowerLevelContents)).toBe(true);
|
||||
|
||||
const lowerPowerLevelContents = { events: {}, events_default: -10 };
|
||||
expect(isMuted(defaultMember, lowerPowerLevelContents)).toBe(false);
|
||||
});
|
||||
|
||||
it("when powerLevelContent.events and '.m.room.message' are defined, uses the value", () => {
|
||||
const higherPowerLevelContents = { events: { "m.room.message": -10 }, events_default: 10 };
|
||||
expect(isMuted(defaultMember, higherPowerLevelContents)).toBe(false);
|
||||
|
||||
const lowerPowerLevelContents = { events: { "m.room.message": 10 }, events_default: -10 };
|
||||
expect(isMuted(defaultMember, lowerPowerLevelContents)).toBe(true);
|
||||
});
|
||||
});
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
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 { renderHook } from "jest-matrix-react";
|
||||
import { type Mocked, mocked } from "jest-mock";
|
||||
import { type Room, type MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
|
||||
import { useRedactMessagesButtonViewModel } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel";
|
||||
import Modal from "../../../../../../../src/Modal";
|
||||
import BulkRedactDialog from "../../../../../../../src/components/views/dialogs/BulkRedactDialog";
|
||||
import { withClientContextRenderOptions } from "../../../../../../test-utils";
|
||||
|
||||
describe("useRedactMessagesButtonViewModel", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
|
||||
let mockRoom: Mocked<Room>;
|
||||
let mockClient: Mocked<MatrixClient>;
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
|
||||
beforeEach(() => {
|
||||
mockRoom = mocked({
|
||||
roomId: defaultRoomId,
|
||||
getType: jest.fn().mockReturnValue(undefined),
|
||||
isSpaceRoom: jest.fn().mockReturnValue(false),
|
||||
getMember: jest.fn().mockReturnValue(undefined),
|
||||
getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"),
|
||||
name: "test room",
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
currentState: {
|
||||
getStateEvents: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
},
|
||||
getEventReadUpTo: jest.fn(),
|
||||
} as unknown as Room);
|
||||
|
||||
mockClient = mocked({
|
||||
getUser: jest.fn(),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
isUserIgnored: jest.fn(),
|
||||
getIgnoredUsers: jest.fn(),
|
||||
setIgnoredUsers: jest.fn(),
|
||||
getUserId: jest.fn().mockReturnValue(defaultUserId),
|
||||
getSafeUserId: jest.fn(),
|
||||
getDomain: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false),
|
||||
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false),
|
||||
getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
removeListener: jest.fn(),
|
||||
currentState: {
|
||||
on: jest.fn(),
|
||||
},
|
||||
getRoom: jest.fn(),
|
||||
credentials: {},
|
||||
setPowerLevel: jest.fn(),
|
||||
} as unknown as MatrixClient);
|
||||
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
|
||||
});
|
||||
|
||||
const renderRedactButtonHook = (props = defaultMember) => {
|
||||
return renderHook(() => useRedactMessagesButtonViewModel(props), withClientContextRenderOptions(mockClient));
|
||||
};
|
||||
|
||||
it("should show BulkRedactDialog upon clicking the Remove messages button", async () => {
|
||||
const spy = jest.spyOn(Modal, "createDialog");
|
||||
|
||||
mockClient.getRoom.mockReturnValue(mockRoom);
|
||||
mockClient.getUserId.mockReturnValue("@arbitraryId:server");
|
||||
const mockMeMember = new RoomMember(mockRoom.roomId, mockClient.getUserId()!);
|
||||
mockMeMember.powerLevel = 51; // defaults to 50
|
||||
const defaultMemberWithPowerLevel = { ...defaultMember, powerLevel: 0 } as RoomMember;
|
||||
mockRoom.getMember.mockImplementation((userId) =>
|
||||
userId === mockClient.getUserId() ? mockMeMember : defaultMemberWithPowerLevel,
|
||||
);
|
||||
|
||||
const { result } = renderRedactButtonHook();
|
||||
await result.current.onRedactAllMessagesClick();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
BulkRedactDialog,
|
||||
expect.objectContaining({ member: defaultMemberWithPowerLevel }),
|
||||
);
|
||||
});
|
||||
});
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
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 { renderHook, waitFor } from "jest-matrix-react";
|
||||
import { act } from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import type { BackupTrustInfo, KeyBackupCheck, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import { useKeyStoragePanelViewModel } from "../../../../../../src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel";
|
||||
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
|
||||
describe("KeyStoragePanelViewModel", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = createTestClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should update the pending value immediately", async () => {
|
||||
const { result } = renderHook(
|
||||
() => useKeyStoragePanelViewModel(),
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
act(() => {
|
||||
result.current.setEnabled(true);
|
||||
});
|
||||
expect(result.current.isEnabled).toBe(true);
|
||||
expect(result.current.busy).toBe(true);
|
||||
});
|
||||
|
||||
it("should update if a KeyBackupStatus event is received", async () => {
|
||||
const { result } = renderHook(
|
||||
() => useKeyStoragePanelViewModel(),
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
await waitFor(() => expect(result.current.isEnabled).toBe(false));
|
||||
|
||||
const mock = mocked(matrixClient.getCrypto()!.getActiveSessionBackupVersion);
|
||||
mock.mockResolvedValue("1");
|
||||
matrixClient.emit(CryptoEvent.KeyBackupStatus, true);
|
||||
await waitFor(() => expect(result.current.isEnabled).toBe(true));
|
||||
|
||||
mock.mockResolvedValue(null);
|
||||
matrixClient.emit(CryptoEvent.KeyBackupStatus, false);
|
||||
await waitFor(() => expect(result.current.isEnabled).toBe(false));
|
||||
});
|
||||
|
||||
it("should call resetKeyBackup if there is no backup currently", async () => {
|
||||
mocked(matrixClient.getCrypto()!.checkKeyBackupAndEnable).mockResolvedValue(null);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useKeyStoragePanelViewModel(),
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
|
||||
await result.current.setEnabled(true);
|
||||
expect(mocked(matrixClient.getCrypto()!.resetKeyBackup)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each<BackupTrustInfo>([
|
||||
{ trusted: true, matchesDecryptionKey: false },
|
||||
{ trusted: false, matchesDecryptionKey: true },
|
||||
{ trusted: true, matchesDecryptionKey: true },
|
||||
])("should not call resetKeyBackup if there is a backup currently and it is trusted", async (trustInfo) => {
|
||||
mocked(matrixClient.getCrypto()!.checkKeyBackupAndEnable).mockResolvedValue({
|
||||
backupInfo: {
|
||||
version: "1",
|
||||
algorithm: "foobar",
|
||||
auth_data: {
|
||||
public_key: "foobar",
|
||||
},
|
||||
},
|
||||
trustInfo,
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useKeyStoragePanelViewModel(),
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
|
||||
await result.current.setEnabled(true);
|
||||
expect(mocked(matrixClient.getCrypto()!.resetKeyBackup)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call resetKeyBackup if there is a backup currently but it is not trusted", async () => {
|
||||
mocked(matrixClient.getCrypto()!.checkKeyBackupAndEnable).mockResolvedValue({
|
||||
backupInfo: {
|
||||
version: "1",
|
||||
algorithm: "foobar",
|
||||
auth_data: {
|
||||
public_key: "foobar",
|
||||
},
|
||||
},
|
||||
trustInfo: {
|
||||
trusted: false,
|
||||
matchesDecryptionKey: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useKeyStoragePanelViewModel(),
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
|
||||
await result.current.setEnabled(true);
|
||||
expect(mocked(matrixClient.getCrypto()!.resetKeyBackup)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should set account data flag when enabling", async () => {
|
||||
mocked(matrixClient.getCrypto()!.checkKeyBackupAndEnable).mockResolvedValue(null);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useKeyStoragePanelViewModel(),
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
|
||||
await result.current.setEnabled(true);
|
||||
expect(mocked(matrixClient.setAccountData)).toHaveBeenCalledWith("m.org.matrix.custom.backup_disabled", {
|
||||
disabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should delete key storage when disabling", async () => {
|
||||
mocked(matrixClient.getCrypto()!.checkKeyBackupAndEnable).mockResolvedValue({} as KeyBackupCheck);
|
||||
mocked(matrixClient.getCrypto()!.getKeyBackupInfo).mockResolvedValue({ version: "99" } as KeyBackupInfo);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useKeyStoragePanelViewModel(),
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
|
||||
await result.current.setEnabled(false);
|
||||
|
||||
expect(mocked(matrixClient.getCrypto()!.disableKeyStorage)).toHaveBeenCalled();
|
||||
expect(mocked(matrixClient.setAccountData)).toHaveBeenCalledWith("m.org.matrix.custom.backup_disabled", {
|
||||
disabled: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user