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
@@ -28,24 +28,16 @@ import {
type CryptoApi,
} from "matrix-js-sdk/src/crypto-api";
import UserInfo, {
disambiguateDevices,
getPowerLevels,
UserOptionsSection,
} from "../../../../../src/components/views/right_panel/UserInfo";
import dis from "../../../../../src/dispatcher/dispatcher";
import UserInfo, { disambiguateDevices } from "../../../../../src/components/views/right_panel/UserInfo";
import { getPowerLevels } from "../../../../../src/components/viewmodels/right_panel/user_info/UserInfoBasicViewModel";
import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import MultiInviter from "../../../../../src/utils/MultiInviter";
import Modal from "../../../../../src/Modal";
import { DirectoryMember, startDmOnFirstMessage } from "../../../../../src/utils/direct-messages";
import { clearAllModals, flushPromises } from "../../../../test-utils";
import ErrorDialog from "../../../../../src/components/views/dialogs/ErrorDialog";
import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents";
import { UIComponent } from "../../../../../src/settings/UIFeature";
import { Action } from "../../../../../src/dispatcher/actions";
import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog";
jest.mock("../../../../../src/utils/direct-messages", () => ({
...jest.requireActual("../../../../../src/utils/direct-messages"),
@@ -449,216 +441,6 @@ describe("<UserInfo />", () => {
});
});
describe("<UserOptionsSection />", () => {
const member = new RoomMember(defaultRoomId, defaultUserId);
const defaultProps = { member, canInvite: false, isSpace: false };
const renderComponent = (props = {}) => {
const Wrapper = (wrapperProps = {}) => {
return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
};
return render(<UserOptionsSection {...defaultProps} {...props} />, {
wrapper: Wrapper,
});
};
const inviteSpy = jest.spyOn(MultiInviter.prototype, "invite");
beforeEach(() => {
inviteSpy.mockReset();
mockClient.setIgnoredUsers.mockClear();
});
afterEach(async () => {
await clearAllModals();
});
afterAll(() => {
inviteSpy.mockRestore();
});
it("always shows share user button and clicking it should produce a ShareDialog", async () => {
const spy = jest.spyOn(Modal, "createDialog");
renderComponent();
await userEvent.click(screen.getByRole("button", { name: "Share profile" }));
expect(spy).toHaveBeenCalledWith(ShareDialog, { target: defaultProps.member });
});
it("does not show ignore or direct message buttons when member userId matches client userId", () => {
mockClient.getSafeUserId.mockReturnValueOnce(member.userId);
mockClient.getUserId.mockReturnValueOnce(member.userId);
renderComponent();
expect(screen.queryByRole("button", { name: /ignore/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /message/i })).not.toBeInTheDocument();
});
it("shows direct message and mention buttons when member userId does not match client userId", () => {
// call to client.getUserId returns undefined, which will not match member.userId
renderComponent();
expect(screen.getByRole("button", { name: "Send message" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Mention" })).toBeInTheDocument();
});
it("mention button fires ComposerInsert Action", async () => {
renderComponent();
const button = screen.getByRole("button", { name: "Mention" });
await userEvent.click(button);
expect(dis.dispatch).toHaveBeenCalledWith({
action: Action.ComposerInsert,
timelineRenderingType: "Room",
userId: "@user:example.com",
});
});
it("when call to client.getRoom is null, shows disabled read receipt button", () => {
mockClient.getRoom.mockReturnValueOnce(null);
renderComponent();
expect(screen.queryByRole("button", { name: "Jump to read receipt" })).toBeDisabled();
});
it("when call to client.getRoom is non-null and room.getEventReadUpTo is null, shows disabled read receipt button", () => {
mockRoom.getEventReadUpTo.mockReturnValueOnce(null);
mockClient.getRoom.mockReturnValueOnce(mockRoom);
renderComponent();
expect(screen.queryByRole("button", { name: "Jump to read receipt" })).toBeDisabled();
});
it("when calls to client.getRoom and room.getEventReadUpTo are non-null, shows read receipt button", () => {
mockRoom.getEventReadUpTo.mockReturnValueOnce("1234");
mockClient.getRoom.mockReturnValueOnce(mockRoom);
renderComponent();
expect(screen.getByRole("button", { name: "Jump to read receipt" })).toBeInTheDocument();
});
it("clicking the read receipt button calls dispatch with correct event_id", async () => {
const mockEventId = "1234";
mockRoom.getEventReadUpTo.mockReturnValue(mockEventId);
mockClient.getRoom.mockReturnValue(mockRoom);
renderComponent();
const readReceiptButton = screen.getByRole("button", { name: "Jump to read receipt" });
expect(readReceiptButton).toBeInTheDocument();
await userEvent.click(readReceiptButton);
expect(dis.dispatch).toHaveBeenCalledWith({
action: "view_room",
event_id: mockEventId,
highlighted: true,
metricsTrigger: undefined,
room_id: "!fkfk",
});
mockRoom.getEventReadUpTo.mockReset();
mockClient.getRoom.mockReset();
});
it("firing the read receipt event handler with a null event_id calls dispatch with undefined not null", async () => {
const mockEventId = "1234";
// the first call is the check to see if we should render the button, second call is
// when the button is clicked
mockRoom.getEventReadUpTo.mockReturnValueOnce(mockEventId).mockReturnValueOnce(null);
mockClient.getRoom.mockReturnValue(mockRoom);
renderComponent();
const readReceiptButton = screen.getByRole("button", { name: "Jump to read receipt" });
expect(readReceiptButton).toBeInTheDocument();
await userEvent.click(readReceiptButton);
expect(dis.dispatch).toHaveBeenCalledWith({
action: "view_room",
event_id: undefined,
highlighted: true,
metricsTrigger: undefined,
room_id: "!fkfk",
});
mockClient.getRoom.mockReset();
});
it("does not show the invite button when canInvite is false", () => {
renderComponent();
expect(screen.queryByRole("button", { name: /invite/i })).not.toBeInTheDocument();
});
it("shows the invite button when canInvite is true", () => {
renderComponent({ canInvite: true });
expect(screen.getByRole("button", { name: /invite/i })).toBeInTheDocument();
});
it("clicking the invite button will call MultiInviter.invite", async () => {
// to save mocking, we will reject the call to .invite
const mockErrorMessage = new Error("test error message");
inviteSpy.mockRejectedValue(mockErrorMessage);
// render the component and click the button
renderComponent({ canInvite: true });
const inviteButton = screen.getByRole("button", { name: /invite/i });
expect(inviteButton).toBeInTheDocument();
await userEvent.click(inviteButton);
// check that we have called .invite
expect(inviteSpy).toHaveBeenCalledWith([member.userId]);
// check that the test error message is displayed
await expect(screen.findByText(mockErrorMessage.message)).resolves.toBeInTheDocument();
});
it("if calling .invite throws something strange, show default error message", async () => {
inviteSpy.mockRejectedValue({ this: "could be anything" });
// render the component and click the button
renderComponent({ canInvite: true });
const inviteButton = screen.getByRole("button", { name: /invite/i });
expect(inviteButton).toBeInTheDocument();
await userEvent.click(inviteButton);
// check that the default test error message is displayed
await expect(screen.findByText(/operation failed/i)).resolves.toBeInTheDocument();
});
it.each([
["for a RoomMember", member, member.getMxcAvatarUrl()],
["for a User", defaultUser, defaultUser.avatarUrl],
])(
"clicking »message« %s should start a DM",
async (test: string, member: RoomMember | User, expectedAvatarUrl: string | undefined) => {
const deferred = Promise.withResolvers<string>();
mocked(startDmOnFirstMessage).mockReturnValue(deferred.promise);
renderComponent({ member });
await userEvent.click(screen.getByRole("button", { name: "Send message" }));
// Checking the attribute, because the button is a DIV and toBeDisabled() does not work.
expect(screen.getByRole("button", { name: "Send message" })).toBeDisabled();
expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockClient, [
new DirectoryMember({
user_id: member.userId,
display_name: member.rawDisplayName,
avatar_url: expectedAvatarUrl,
}),
]);
await act(async () => {
deferred.resolve("!dm:example.com");
await flushPromises();
});
// Checking the attribute, because the button is a DIV and toBeDisabled() does not work.
expect(screen.getByRole("button", { name: "Send message" })).not.toBeDisabled();
},
);
});
describe("disambiguateDevices", () => {
it("does not add ambiguous key to unique names", () => {
const initialDevices = [
@@ -10,16 +10,16 @@ import { render, screen, fireEvent } from "jest-matrix-react";
import { type Room, type RoomMember } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import { UserInfoAdminToolsContainer } from "../../../../../src/components/views/right_panel/user_info/UserInfoAdminToolsContainer";
import { useUserInfoAdminToolsContainerViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel";
import { useRoomKickButtonViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel";
import { useBanButtonViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel";
import { useMuteButtonViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel";
import { useRedactMessagesButtonViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel";
import { stubClient } from "../../../../test-utils";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { UserInfoAdminToolsContainer } from "../../../../../../src/components/views/right_panel/user_info/UserInfoAdminToolsContainer";
import { useUserInfoAdminToolsContainerViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel";
import { useRoomKickButtonViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel";
import { useBanButtonViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel";
import { useMuteButtonViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel";
import { useRedactMessagesButtonViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel";
import { stubClient } from "../../../../../test-utils";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
jest.mock("../../../../../src/utils/DMRoomMap", () => {
jest.mock("../../../../../../src/utils/DMRoomMap", () => {
const mock = {
getUserIdForRoomId: jest.fn(),
getDMRoomsForUserId: jest.fn(),
@@ -32,7 +32,7 @@ jest.mock("../../../../../src/utils/DMRoomMap", () => {
});
jest.mock(
"../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel",
"../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel",
() => ({
useUserInfoAdminToolsContainerViewModel: jest.fn().mockReturnValue({
isCurrentUserInTheRoom: true,
@@ -44,34 +44,43 @@ jest.mock(
}),
);
jest.mock("../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel", () => ({
useRoomKickButtonViewModel: jest.fn().mockReturnValue({
canUserBeKicked: true,
kickLabel: "Kick",
onKickClick: jest.fn(),
jest.mock(
"../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel",
() => ({
useRoomKickButtonViewModel: jest.fn().mockReturnValue({
canUserBeKicked: true,
kickLabel: "Kick",
onKickClick: jest.fn(),
}),
}),
}));
);
jest.mock("../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel", () => ({
jest.mock("../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel", () => ({
useBanButtonViewModel: jest.fn().mockReturnValue({
banLabel: "Ban",
onBanOrUnbanClick: jest.fn(),
}),
}));
jest.mock("../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel", () => ({
useMuteButtonViewModel: jest.fn().mockReturnValue({
isMemberInTheRoom: true,
muteLabel: "Mute",
onMuteButtonClick: jest.fn(),
jest.mock(
"../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel",
() => ({
useMuteButtonViewModel: jest.fn().mockReturnValue({
isMemberInTheRoom: true,
muteLabel: "Mute",
onMuteButtonClick: jest.fn(),
}),
}),
}));
);
jest.mock("../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel", () => ({
useRedactMessagesButtonViewModel: jest.fn().mockReturnValue({
onRedactAllMessagesClick: jest.fn(),
jest.mock(
"../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel",
() => ({
useRedactMessagesButtonViewModel: jest.fn().mockReturnValue({
onRedactAllMessagesClick: jest.fn(),
}),
}),
}));
);
const defaultRoomId = "!fkfk";
@@ -0,0 +1,112 @@
/*
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 { mocked } from "jest-mock";
import { type MatrixClient, type Room, RoomMember, type User } from "matrix-js-sdk/src/matrix";
import { logRoles, render, screen } from "jest-matrix-react";
import { createTestClient, mkStubRoom } from "../../../../../test-utils";
import {
type UserInfoBasicState,
useUserInfoBasicViewModel,
} from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoBasicViewModel";
import { UserInfoBasicView } from "../../../../../../src/components/views/right_panel/user_info/UserInfoBasicView";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
const defaultRoomPermissions = {
canEdit: true,
canInvite: true,
modifyLevelMax: -1,
};
jest.mock("../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoBasicViewModel", () => ({
useUserInfoBasicViewModel: jest.fn(),
useRoomPermissions: () => defaultRoomPermissions,
}));
describe("<UserInfoBasic />", () => {
const defaultValue: UserInfoBasicState = {
powerLevels: {},
roomPermissions: defaultRoomPermissions,
pendingUpdateCount: 0,
isMe: false,
isRoomDMForMember: false,
showDeactivateButton: true,
onSynapseDeactivate: jest.fn(),
startUpdating: jest.fn(),
stopUpdating: jest.fn(),
};
const defaultRoomId = "!fkfk";
const defaultUserId = "@user:example.com";
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
let defaultRoom: Room;
let defaultProps: { member: User | RoomMember; room: Room };
let matrixClient: MatrixClient;
const renderComponent = (props = defaultProps) => {
return render(
<MatrixClientContext.Provider value={matrixClient}>
<UserInfoBasicView {...props} />
</MatrixClientContext.Provider>,
);
};
beforeEach(() => {
matrixClient = createTestClient();
defaultRoom = mkStubRoom(defaultRoomId, defaultRoomId, matrixClient);
defaultProps = {
member: defaultMember,
room: defaultRoom,
};
});
it("should display the defaut values", () => {
mocked(useUserInfoBasicViewModel).mockReturnValue(defaultValue);
const { container } = renderComponent();
logRoles(container);
expect(container).toMatchSnapshot();
});
it("should not show ignore button if user is me", () => {
const state: UserInfoBasicState = { ...defaultValue, isMe: true };
mocked(useUserInfoBasicViewModel).mockReturnValue(state);
renderComponent();
const ignoreButton = screen.queryByRole("button", { name: "Ignore" });
expect(ignoreButton).not.toBeInTheDocument();
});
it("should not show deactivate button", () => {
const state: UserInfoBasicState = { ...defaultValue, showDeactivateButton: false };
mocked(useUserInfoBasicViewModel).mockReturnValue(state);
renderComponent();
const deactivateButton = screen.queryByRole("button", { name: "Deactivate user" });
expect(deactivateButton).not.toBeInTheDocument();
});
it("should not show powerlevels selector for dm", () => {
const state: UserInfoBasicState = { ...defaultValue, isRoomDMForMember: true };
mocked(useUserInfoBasicViewModel).mockReturnValue(state);
const { container } = renderComponent();
logRoles(container);
const powserlevel = screen.queryByRole("option", { name: "Default" });
expect(powserlevel).not.toBeInTheDocument();
});
it("should show spinner if pending update is > 0", () => {
const state: UserInfoBasicState = { ...defaultValue, pendingUpdateCount: 2 };
mocked(useUserInfoBasicViewModel).mockReturnValue(state);
renderComponent();
const spinner = screen.getByTestId("spinner");
expect(spinner).toBeInTheDocument();
});
});
@@ -0,0 +1,208 @@
/*
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 { mocked } from "jest-mock";
import { type Room, RoomMember, type User } from "matrix-js-sdk/src/matrix";
import { fireEvent, render, screen } from "jest-matrix-react";
import { mkStubRoom, stubClient } from "../../../../../test-utils";
import {
useUserInfoBasicOptionsViewModel,
type UserInfoBasicOptionsState,
} from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoBasicOptionsViewModel";
import { UserInfoBasicOptionsView } from "../../../../../../src/components/views/right_panel/user_info/UserInfoBasicOptionsView";
import { UIComponent } from "../../../../../../src/settings/UIFeature";
import { shouldShowComponent } from "../../../../../../src/customisations/helpers/UIComponents";
import { type Member } from "../../../../../../src/components/views/right_panel/UserInfo";
jest.mock("../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoBasicOptionsViewModel", () => ({
useUserInfoBasicOptionsViewModel: jest.fn(),
}));
jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => {
const original = jest.requireActual("../../../../../../src/customisations/helpers/UIComponents");
return {
shouldShowComponent: jest.fn().mockImplementation(original.shouldShowComponent),
};
});
describe("<UserOptionsSection />", () => {
const defaultValue: UserInfoBasicOptionsState = {
isMe: false,
showInviteButton: false,
showInsertPillButton: false,
readReceiptButtonDisabled: false,
onInsertPillButton: () => jest.fn(),
onReadReceiptButton: () => jest.fn(),
onShareUserClick: () => jest.fn(),
onInviteUserButton: (evt: Event) => Promise.resolve(),
onOpenDmForUser: (member: Member) => Promise.resolve(),
};
const defaultRoomId = "!fkfk";
const defaultUserId = "@user:example.com";
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
let defaultRoom: Room;
let defaultProps: { member: User | RoomMember; room: Room };
beforeEach(() => {
const matrixClient = stubClient();
defaultRoom = mkStubRoom(defaultRoomId, defaultRoomId, matrixClient);
defaultProps = {
member: defaultMember,
room: defaultRoom,
};
});
afterEach(() => {
jest.resetAllMocks();
});
it("should always display sharedButton when user is not me", () => {
// User is not me by default
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue({ ...defaultValue });
render(<UserInfoBasicOptionsView {...defaultProps} />);
const sharedButton = screen.getByRole("button", { name: "Share profile" });
expect(sharedButton).toBeInTheDocument();
});
it("should always display sharedButton when user is me", () => {
const propsWithMe = { ...defaultProps };
const onShareUserClick = jest.fn();
const state = { ...defaultValue, isMe: true, onShareUserClick };
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue(state);
render(<UserInfoBasicOptionsView {...propsWithMe} />);
const sharedButton2 = screen.getByRole("button", { name: "Share profile" });
expect(sharedButton2).toBeInTheDocument();
// clicking on the share profile button
fireEvent.click(sharedButton2);
expect(onShareUserClick).toHaveBeenCalled();
});
it("should show insert pill button when user is not me and showinsertpill is true", () => {
const onInsertPillButton = jest.fn();
const state = { ...defaultValue, showInsertPillButton: true, onInsertPillButton };
// User is not me and showInsertpill is true
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue(state);
render(<UserInfoBasicOptionsView {...defaultProps} />);
const insertPillButton = screen.getByRole("button", { name: "Mention" });
expect(insertPillButton).toBeInTheDocument();
// clicking on the insert pill button
fireEvent.click(insertPillButton);
expect(onInsertPillButton).toHaveBeenCalled();
});
it("should not show insert pill button when user is not me and showinsertpill is false", () => {
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue({ ...defaultValue, showInsertPillButton: false });
render(<UserInfoBasicOptionsView {...defaultProps} />);
const insertPillButton = screen.queryByRole("button", { name: "Mention" });
expect(insertPillButton).not.toBeInTheDocument();
});
it("should not show insert pill button when user is me", () => {
// User is me, should not see the insert button even when show insertpill is true
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue({
...defaultValue,
showInsertPillButton: true,
isMe: true,
});
const propsWithMe = { ...defaultProps };
render(<UserInfoBasicOptionsView {...propsWithMe} />);
const insertPillButton = screen.queryByRole("button", { name: "Mention" });
expect(insertPillButton).not.toBeInTheDocument();
});
it("should not show readreceiptbutton when user is me", () => {
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue({
...defaultValue,
readReceiptButtonDisabled: true,
isMe: true,
});
const propsWithMe = { ...defaultProps };
render(<UserInfoBasicOptionsView {...propsWithMe} />);
const readReceiptButton = screen.queryByRole("button", { name: "Jump to read receipt" });
expect(readReceiptButton).not.toBeInTheDocument();
});
it("should show disable readreceiptbutton when readReceiptButtonDisabled is true", () => {
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue({ ...defaultValue, readReceiptButtonDisabled: true });
render(<UserInfoBasicOptionsView {...defaultProps} />);
const readReceiptButton = screen.getByRole("button", { name: "Jump to read receipt" });
expect(readReceiptButton).toBeDisabled();
});
it("should not show disable readreceiptbutton when readReceiptButtonDisabled is false", () => {
const onReadReceiptButton = jest.fn();
const state = { ...defaultValue, readReceiptButtonDisabled: false, onReadReceiptButton };
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue(state);
render(<UserInfoBasicOptionsView {...defaultProps} />);
const readReceiptButton = screen.getByRole("button", { name: "Jump to read receipt" });
expect(readReceiptButton).not.toBeDisabled();
// clicking on the read receipt button
fireEvent.click(readReceiptButton);
expect(onReadReceiptButton).toHaveBeenCalled();
});
it("should show not show invite button if shouldShowComponent is false", () => {
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue({ ...defaultValue, showInviteButton: true });
mocked(shouldShowComponent).mockReturnValue(false);
render(<UserInfoBasicOptionsView {...defaultProps} />);
const inviteButton = screen.queryByRole("button", { name: "Invite" });
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.InviteUsers);
expect(inviteButton).not.toBeInTheDocument();
});
it("should show show invite button if shouldShowComponent is true", () => {
const onInviteUserButton = jest.fn();
const state = { ...defaultValue, showInviteButton: true, onInviteUserButton };
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue(state);
mocked(shouldShowComponent).mockReturnValue(true);
render(<UserInfoBasicOptionsView {...defaultProps} />);
const inviteButton = screen.getByRole("button", { name: "Invite" });
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.InviteUsers);
expect(inviteButton).toBeInTheDocument();
// clicking on the invite button
fireEvent.click(inviteButton);
expect(onInviteUserButton).toHaveBeenCalled();
});
it("should show directMessageButton when user is not me", () => {
// User is not me, direct message button should display
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue(defaultValue);
mocked(shouldShowComponent).mockReturnValue(true);
render(<UserInfoBasicOptionsView {...defaultProps} />);
const dmButton = screen.getByRole("button", { name: "Send message" });
expect(dmButton).toBeInTheDocument();
});
it("should not show directMessageButton when user is me", () => {
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue({ ...defaultValue, isMe: true });
mocked(shouldShowComponent).mockReturnValue(true);
const propsWithMe = { ...defaultProps };
render(<UserInfoBasicOptionsView {...propsWithMe} />);
const dmButton = screen.queryByRole("button", { name: "Send message" });
expect(dmButton).not.toBeInTheDocument();
});
});
@@ -12,10 +12,10 @@ import { Device, RoomMember } from "matrix-js-sdk/src/matrix";
import { render, waitFor, screen } from "jest-matrix-react";
import React from "react";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { UserInfoHeaderVerificationView } from "../../../../../src/components/views/right_panel/user_info/UserInfoHeaderVerificationView";
import { createTestClient } from "../../../../test-utils";
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import { UserInfoHeaderVerificationView } from "../../../../../../src/components/views/right_panel/user_info/UserInfoHeaderVerificationView";
import { createTestClient } from "../../../../../test-utils";
describe("<UserInfoHeaderVerificationView />", () => {
const defaultRoomId = "!fkfk";
@@ -12,14 +12,14 @@ import { Device, RoomMember } from "matrix-js-sdk/src/matrix";
import { fireEvent, render, screen } from "jest-matrix-react";
import React from "react";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { UserInfoHeaderView } from "../../../../../src/components/views/right_panel/user_info/UserInfoHeaderView";
import { createTestClient } from "../../../../test-utils";
import { useUserfoHeaderViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel";
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import { UserInfoHeaderView } from "../../../../../../src/components/views/right_panel/user_info/UserInfoHeaderView";
import { createTestClient } from "../../../../../test-utils";
import { useUserfoHeaderViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel";
// Mock the viewmodel hooks
jest.mock("../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel", () => ({
jest.mock("../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel", () => ({
useUserfoHeaderViewModel: jest.fn().mockReturnValue({
onMemberAvatarClick: jest.fn(),
precenseInfo: {
@@ -0,0 +1,315 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<UserInfoBasic /> should display the defaut values 1`] = `
<div>
<div
class="mx_UserInfo_container"
>
<div
class="mx_UserInfo_profileField"
>
<div
class="mx_PowerSelector"
>
<div
class="mx_Field mx_Field_select"
>
<select
data-testid="power-level-select-element"
id="mx_Field_1"
label="Power level"
placeholder="Power level"
type="text"
>
<option
data-testid="power-level-option-0"
value="0"
>
Default
</option>
<option
data-testid="power-level-option-SELECT_VALUE_CUSTOM"
value="SELECT_VALUE_CUSTOM"
>
Custom level
</option>
</select>
<label
for="mx_Field_1"
>
Power level
</label>
</div>
</div>
</div>
<button
class="_item_dyt4i_8 _interactive_dyt4i_26"
data-kind="primary"
role="button"
>
<svg
aria-hidden="true"
class="_icon_dyt4i_50"
fill="currentColor"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m1.5 21.25 1.45-4.95a10.2 10.2 0 0 1-.712-2.1A10.2 10.2 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22q-1.125 0-2.2-.238a10.2 10.2 0 0 1-2.1-.712L2.75 22.5a.94.94 0 0 1-1-.25.94.94 0 0 1-.25-1m2.45-1.2 3.2-.95a1 1 0 0 1 .275-.062q.15-.013.275-.013.225 0 .438.038.212.036.412.137a7.4 7.4 0 0 0 1.675.6Q11.1 20 12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12q0 .9.2 1.775t.6 1.675q.176.325.188.688t-.088.712z"
/>
</svg>
<span
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
>
Send message
</span>
<svg
aria-hidden="true"
class="_nav-hint_dyt4i_59"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
/>
</svg>
</button>
<button
class="_item_dyt4i_8 _interactive_dyt4i_26"
data-kind="primary"
role="button"
>
<svg
aria-hidden="true"
class="_icon_dyt4i_50"
fill="currentColor"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 12q-1.65 0-2.825-1.175T6 8t1.175-2.825T10 4t2.825 1.175T14 8t-1.175 2.825T10 12m-8 6v-.8q0-.85.438-1.562.437-.713 1.162-1.088a14.8 14.8 0 0 1 3.15-1.163A13.8 13.8 0 0 1 10 13q1.65 0 3.25.387 1.6.388 3.15 1.163.724.375 1.163 1.087Q18 16.35 18 17.2v.8q0 .824-.587 1.413A1.93 1.93 0 0 1 16 20H4q-.824 0-1.412-.587A1.93 1.93 0 0 1 2 18m2 0h12v-.8a.97.97 0 0 0-.5-.85q-1.35-.675-2.725-1.012a11.6 11.6 0 0 0-5.55 0Q5.85 15.675 4.5 16.35a.97.97 0 0 0-.5.85zm6-8q.825 0 1.412-.588Q12 8.826 12 8q0-.824-.588-1.412A1.93 1.93 0 0 0 10 6q-.825 0-1.412.588A1.93 1.93 0 0 0 8 8q0 .825.588 1.412Q9.175 10 10 10m7 1h2v2q0 .424.288.713.287.287.712.287.424 0 .712-.287A.97.97 0 0 0 21 13v-2h2q.424 0 .712-.287A.97.97 0 0 0 24 10a.97.97 0 0 0-.288-.713A.97.97 0 0 0 23 9h-2V7a.97.97 0 0 0-.288-.713A.97.97 0 0 0 20 6a.97.97 0 0 0-.712.287A.97.97 0 0 0 19 7v2h-2a.97.97 0 0 0-.712.287A.97.97 0 0 0 16 10q0 .424.288.713.287.287.712.287"
/>
</svg>
<span
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
>
Invite
</span>
<svg
aria-hidden="true"
class="_nav-hint_dyt4i_59"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
/>
</svg>
</button>
<button
class="_item_dyt4i_8 _interactive_dyt4i_26 _disabled_dyt4i_118"
data-kind="primary"
disabled=""
role="button"
>
<svg
aria-hidden="true"
class="_icon_dyt4i_50"
fill="currentColor"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
<span
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
>
Jump to read receipt
</span>
<svg
aria-hidden="true"
class="_nav-hint_dyt4i_59"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
/>
</svg>
</button>
<button
class="_item_dyt4i_8 _interactive_dyt4i_26"
data-kind="primary"
role="button"
>
<svg
aria-hidden="true"
class="_icon_dyt4i_50"
fill="currentColor"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 16a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 15V7.85L9.125 9.725q-.3.3-.7.3T7.7 9.7a.93.93 0 0 1-.288-.713A.98.98 0 0 1 7.7 8.3l3.6-3.6q.15-.15.325-.213.175-.062.375-.062t.375.062a.9.9 0 0 1 .325.213l3.6 3.6q.3.3.287.712a.98.98 0 0 1-.287.688q-.3.3-.713.313a.93.93 0 0 1-.712-.288L13 7.85V15q0 .424-.287.713A.97.97 0 0 1 12 16m-6 4q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 18v-2q0-.424.287-.713A.97.97 0 0 1 5 15q.424 0 .713.287Q6 15.576 6 16v2h12v-2q0-.424.288-.713A.97.97 0 0 1 19 15q.424 0 .712.287.288.288.288.713v2q0 .824-.587 1.413A1.93 1.93 0 0 1 18 20z"
/>
</svg>
<span
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
>
Share profile
</span>
<svg
aria-hidden="true"
class="_nav-hint_dyt4i_59"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
/>
</svg>
</button>
<button
class="_item_dyt4i_8 _interactive_dyt4i_26"
data-kind="primary"
role="button"
>
<svg
aria-hidden="true"
class="_icon_dyt4i_50"
fill="currentColor"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 4a8 8 0 1 0 0 16 1 1 0 1 1 0 2C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10v1.5a3.5 3.5 0 0 1-6.396 1.966A5 5 0 1 1 17 12v1.5a1.5 1.5 0 0 0 3 0V12a8 8 0 0 0-8-8m3 8a3 3 0 1 0-6 0 3 3 0 0 0 6 0"
/>
</svg>
<span
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
>
Mention
</span>
<svg
aria-hidden="true"
class="_nav-hint_dyt4i_59"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
/>
</svg>
</button>
</div>
<div
class="mx_UserInfo_container"
>
<button
class="_item_dyt4i_8 _interactive_dyt4i_26"
data-kind="critical"
role="button"
>
<svg
aria-hidden="true"
class="_icon_dyt4i_50"
fill="currentColor"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 21q-.824 0-1.412-.587A1.93 1.93 0 0 1 5 19V6a.97.97 0 0 1-.713-.287A.97.97 0 0 1 4 5q0-.424.287-.713A.97.97 0 0 1 5 4h4q0-.424.287-.712A.97.97 0 0 1 10 3h4q.424 0 .713.288Q15 3.575 15 4h4q.424 0 .712.287Q20 4.576 20 5t-.288.713A.97.97 0 0 1 19 6v13q0 .824-.587 1.413A1.93 1.93 0 0 1 17 21zM7 6v13h10V6zm2 10q0 .424.287.712Q9.576 17 10 17t.713-.288A.97.97 0 0 0 11 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 10 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 9 9zm4 0q0 .424.287.712.288.288.713.288.424 0 .713-.288A.97.97 0 0 0 15 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 9z"
/>
</svg>
<span
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
>
Deactivate user
</span>
<svg
aria-hidden="true"
class="_nav-hint_dyt4i_59"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
/>
</svg>
</button>
</div>
<div
class="mx_UserInfo_container"
>
<button
class="_item_dyt4i_8 _interactive_dyt4i_26"
data-kind="critical"
role="button"
>
<svg
aria-hidden="true"
class="_icon_dyt4i_50"
fill="currentColor"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 22a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12q0-1.35-.437-2.6A8 8 0 0 0 18.3 7.1L7.1 18.3q1.05.825 2.3 1.262T12 20m-6.3-3.1L16.9 5.7a8 8 0 0 0-2.3-1.263A7.8 7.8 0 0 0 12 4Q8.65 4 6.325 6.325T4 12q0 1.35.438 2.6A8 8 0 0 0 5.7 16.9"
/>
</svg>
<span
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
>
Ignore
</span>
<svg
aria-hidden="true"
class="_nav-hint_dyt4i_59"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
/>
</svg>
</button>
</div>
</div>
`;