Mvvm split user info, create userinfoadmintools container component (#29808)

* feat: mvvm split user info, create userinfoadmintools container component

* test: mvvm userinfoadmintools and view

* feat: user info admin components more split and comments

* test: mvvm user admin info mute view models more coverage

* chore: rename user-info folder to user_info
This commit is contained in:
Marc
2025-06-13 09:08:29 +02:00
committed by GitHub
parent d89afe83a8
commit 0f0f904cb0
14 changed files with 2024 additions and 901 deletions

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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 }),
);
});
});