MVVM userinfo basic component (#30305)

* feat: mvvm userinfo basic component

* test: mvvm userinfobasic component

* chore: apply review. rename views, add comment and move some codes

* chore(review): move openDM method into viewmodel
This commit is contained in:
Marc
2025-10-20 06:13:20 +00:00
committed by GitHub
parent cf51b256ce
commit e6e6f87d01
18 changed files with 1745 additions and 741 deletions
@@ -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(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",
});
});
});
});
@@ -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);
});
});