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:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user