1554 lines
		
	
	
		
			60 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			1554 lines
		
	
	
		
			60 KiB
		
	
	
	
		
			TypeScript
		
	
	
| /*
 | |
| Copyright 2022 The Matrix.org Foundation C.I.C.
 | |
| 
 | |
| Licensed under the Apache License, Version 2.0 (the "License");
 | |
| you may not use this file except in compliance with the License.
 | |
| You may obtain a copy of the License at
 | |
| 
 | |
|     http://www.apache.org/licenses/LICENSE-2.0
 | |
| 
 | |
| Unless required by applicable law or agreed to in writing, software
 | |
| distributed under the License is distributed on an "AS IS" BASIS,
 | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| See the License for the specific language governing permissions and
 | |
| limitations under the License.
 | |
| */
 | |
| 
 | |
| import React from "react";
 | |
| import { fireEvent, render, screen, waitFor, cleanup, act, within } from "@testing-library/react";
 | |
| import userEvent from "@testing-library/user-event";
 | |
| import { Mocked, mocked } from "jest-mock";
 | |
| import {
 | |
|     Room,
 | |
|     User,
 | |
|     MatrixClient,
 | |
|     RoomMember,
 | |
|     MatrixEvent,
 | |
|     EventType,
 | |
|     CryptoApi,
 | |
|     DeviceVerificationStatus,
 | |
|     Device,
 | |
| } from "matrix-js-sdk/src/matrix";
 | |
| import { KnownMembership } from "matrix-js-sdk/src/types";
 | |
| import { defer } from "matrix-js-sdk/src/utils";
 | |
| import { EventEmitter } from "events";
 | |
| import {
 | |
|     UserVerificationStatus,
 | |
|     VerificationRequest,
 | |
|     VerificationPhase as Phase,
 | |
|     VerificationRequestEvent,
 | |
| } from "matrix-js-sdk/src/crypto-api";
 | |
| 
 | |
| import UserInfo, {
 | |
|     BanToggleButton,
 | |
|     DeviceItem,
 | |
|     disambiguateDevices,
 | |
|     getPowerLevels,
 | |
|     isMuted,
 | |
|     PowerLevelEditor,
 | |
|     RoomAdminToolsContainer,
 | |
|     RoomKickButton,
 | |
|     UserInfoHeader,
 | |
|     UserOptionsSection,
 | |
| } from "../../../../src/components/views/right_panel/UserInfo";
 | |
| import dis from "../../../../src/dispatcher/dispatcher";
 | |
| 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 * as mockVerification from "../../../../src/verification";
 | |
| import Modal from "../../../../src/Modal";
 | |
| import { E2EStatus } from "../../../../src/utils/ShieldUtils";
 | |
| 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";
 | |
| 
 | |
| jest.mock("../../../../src/utils/direct-messages", () => ({
 | |
|     ...jest.requireActual("../../../../src/utils/direct-messages"),
 | |
|     startDmOnFirstMessage: jest.fn(),
 | |
| }));
 | |
| 
 | |
| jest.mock("../../../../src/dispatcher/dispatcher");
 | |
| 
 | |
| jest.mock("../../../../src/customisations/UserIdentifier", () => {
 | |
|     return {
 | |
|         getDisplayUserIdentifier: jest.fn().mockReturnValue("customUserIdentifier"),
 | |
|     };
 | |
| });
 | |
| 
 | |
| jest.mock("../../../../src/utils/DMRoomMap", () => {
 | |
|     const mock = {
 | |
|         getUserIdForRoomId: jest.fn(),
 | |
|         getDMRoomsForUserId: jest.fn(),
 | |
|     };
 | |
| 
 | |
|     return {
 | |
|         shared: jest.fn().mockReturnValue(mock),
 | |
|         sharedInstance: mock,
 | |
|     };
 | |
| });
 | |
| 
 | |
| jest.mock("../../../../src/customisations/helpers/UIComponents", () => {
 | |
|     const original = jest.requireActual("../../../../src/customisations/helpers/UIComponents");
 | |
|     return {
 | |
|         shouldShowComponent: jest.fn().mockImplementation(original.shouldShowComponent),
 | |
|     };
 | |
| });
 | |
| 
 | |
| const defaultRoomId = "!fkfk";
 | |
| const defaultUserId = "@user:example.com";
 | |
| const defaultUser = new User(defaultUserId);
 | |
| 
 | |
| let mockRoom: Mocked<Room>;
 | |
| let mockSpace: Mocked<Room>;
 | |
| let mockClient: Mocked<MatrixClient>;
 | |
| let mockCrypto: Mocked<CryptoApi>;
 | |
| 
 | |
| 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);
 | |
| 
 | |
|     mockCrypto = mocked({
 | |
|         getDeviceVerificationStatus: jest.fn(),
 | |
|         getUserDeviceInfo: jest.fn(),
 | |
|         userHasCrossSigningKeys: jest.fn().mockResolvedValue(false),
 | |
|         getUserVerificationStatus: jest.fn(),
 | |
|     } as unknown as CryptoApi);
 | |
| 
 | |
|     mockClient = mocked({
 | |
|         getUser: jest.fn(),
 | |
|         isGuest: jest.fn().mockReturnValue(false),
 | |
|         isUserIgnored: jest.fn(),
 | |
|         getIgnoredUsers: jest.fn(),
 | |
|         setIgnoredUsers: jest.fn(),
 | |
|         isCryptoEnabled: jest.fn(),
 | |
|         getUserId: jest.fn(),
 | |
|         getSafeUserId: jest.fn(),
 | |
|         on: jest.fn(),
 | |
|         off: jest.fn(),
 | |
|         isSynapseAdministrator: jest.fn().mockResolvedValue(false),
 | |
|         isRoomEncrypted: jest.fn().mockReturnValue(false),
 | |
|         doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false),
 | |
|         mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
 | |
|         removeListener: jest.fn(),
 | |
|         currentState: {
 | |
|             on: jest.fn(),
 | |
|         },
 | |
|         getRoom: jest.fn(),
 | |
|         credentials: {},
 | |
|         setPowerLevel: jest.fn(),
 | |
|         downloadKeys: jest.fn(),
 | |
|         getCrypto: jest.fn().mockReturnValue(mockCrypto),
 | |
|     } as unknown as MatrixClient);
 | |
| 
 | |
|     jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
 | |
|     jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
 | |
| });
 | |
| 
 | |
| describe("<UserInfo />", () => {
 | |
|     class MockVerificationRequest extends EventEmitter {
 | |
|         pending = true;
 | |
|         phase: Phase = Phase.Ready;
 | |
|         cancellationCode: string | null = null;
 | |
| 
 | |
|         constructor(opts: Partial<VerificationRequest>) {
 | |
|             super();
 | |
|             Object.assign(this, {
 | |
|                 channel: { transactionId: 1 },
 | |
|                 otherPartySupportsMethod: jest.fn(),
 | |
|                 generateQRCode: jest.fn().mockReturnValue(new Promise(() => {})),
 | |
|                 ...opts,
 | |
|             });
 | |
|         }
 | |
|     }
 | |
|     let verificationRequest: MockVerificationRequest;
 | |
| 
 | |
|     const defaultProps = {
 | |
|         user: defaultUser,
 | |
|         // idk what is wrong with this type
 | |
|         phase: RightPanelPhases.RoomMemberInfo as RightPanelPhases.RoomMemberInfo,
 | |
|         onClose: jest.fn(),
 | |
|     };
 | |
| 
 | |
|     const renderComponent = (props = {}) => {
 | |
|         const Wrapper = (wrapperProps = {}) => {
 | |
|             return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
 | |
|         };
 | |
| 
 | |
|         return render(<UserInfo {...defaultProps} {...props} />, {
 | |
|             wrapper: Wrapper,
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     beforeEach(() => {
 | |
|         verificationRequest = new MockVerificationRequest({});
 | |
|     });
 | |
| 
 | |
|     afterEach(async () => {
 | |
|         await clearAllModals();
 | |
|         jest.clearAllMocks();
 | |
|     });
 | |
| 
 | |
|     it("closes on close button click", async () => {
 | |
|         renderComponent();
 | |
| 
 | |
|         await userEvent.click(screen.getByTestId("base-card-close-button"));
 | |
| 
 | |
|         expect(defaultProps.onClose).toHaveBeenCalled();
 | |
|     });
 | |
| 
 | |
|     describe("without a room", () => {
 | |
|         it("does not render space header", () => {
 | |
|             renderComponent();
 | |
|             expect(screen.queryByTestId("space-header")).not.toBeInTheDocument();
 | |
|         });
 | |
| 
 | |
|         it("renders user info", () => {
 | |
|             renderComponent();
 | |
|             expect(screen.getByRole("heading", { name: defaultUserId })).toBeInTheDocument();
 | |
|         });
 | |
| 
 | |
|         it("renders encryption info panel without pending verification", () => {
 | |
|             renderComponent({ phase: RightPanelPhases.EncryptionPanel });
 | |
|             expect(screen.getByRole("heading", { name: /encryption/i })).toBeInTheDocument();
 | |
|         });
 | |
| 
 | |
|         it("renders encryption verification panel with pending verification", () => {
 | |
|             renderComponent({ phase: RightPanelPhases.EncryptionPanel, verificationRequest });
 | |
| 
 | |
|             expect(screen.queryByRole("heading", { name: /encryption/i })).not.toBeInTheDocument();
 | |
|             // the verificationRequest has phase of Phase.Ready but .otherPartySupportsMethod
 | |
|             // will not return true, so we expect to see the noCommonMethod error from VerificationPanel
 | |
|             expect(screen.getByText(/try with a different client/i)).toBeInTheDocument();
 | |
|         });
 | |
| 
 | |
|         it("should show error modal when the verification request is cancelled with a mismatch", () => {
 | |
|             renderComponent({ phase: RightPanelPhases.EncryptionPanel, verificationRequest });
 | |
| 
 | |
|             const spy = jest.spyOn(Modal, "createDialog");
 | |
|             act(() => {
 | |
|                 verificationRequest.phase = Phase.Cancelled;
 | |
|                 verificationRequest.cancellationCode = "m.key_mismatch";
 | |
|                 verificationRequest.emit(VerificationRequestEvent.Change);
 | |
|             });
 | |
|             expect(spy).toHaveBeenCalledWith(
 | |
|                 ErrorDialog,
 | |
|                 expect.objectContaining({ title: "Your messages are not secure" }),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it("should not show error modal when the verification request is changed for some other reason", () => {
 | |
|             renderComponent({ phase: RightPanelPhases.EncryptionPanel, verificationRequest });
 | |
| 
 | |
|             const spy = jest.spyOn(Modal, "createDialog");
 | |
| 
 | |
|             // change to "started"
 | |
|             act(() => {
 | |
|                 verificationRequest.phase = Phase.Started;
 | |
|                 verificationRequest.emit(VerificationRequestEvent.Change);
 | |
|             });
 | |
| 
 | |
|             // cancelled for some other reason
 | |
|             act(() => {
 | |
|                 verificationRequest.phase = Phase.Cancelled;
 | |
|                 verificationRequest.cancellationCode = "changed my mind";
 | |
|                 verificationRequest.emit(VerificationRequestEvent.Change);
 | |
|             });
 | |
| 
 | |
|             expect(spy).not.toHaveBeenCalled();
 | |
|         });
 | |
| 
 | |
|         it("renders close button correctly when encryption panel with a pending verification request", () => {
 | |
|             renderComponent({ phase: RightPanelPhases.EncryptionPanel, verificationRequest });
 | |
|             screen.getByTestId("base-card-close-button").focus();
 | |
|             expect(screen.getByRole("tooltip")).toHaveTextContent("Cancel");
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("with a room", () => {
 | |
|         it("renders user info", () => {
 | |
|             renderComponent({ room: mockRoom });
 | |
|             expect(screen.getByRole("heading", { name: defaultUserId })).toBeInTheDocument();
 | |
|         });
 | |
| 
 | |
|         it("does not render space header when room is not a space room", () => {
 | |
|             renderComponent({ room: mockRoom });
 | |
|             expect(screen.queryByTestId("space-header")).not.toBeInTheDocument();
 | |
|         });
 | |
| 
 | |
|         it("renders space header when room is a space room", () => {
 | |
|             const spaceRoom = {
 | |
|                 ...mockRoom,
 | |
|                 isSpaceRoom: jest.fn().mockReturnValue(true),
 | |
|             };
 | |
|             renderComponent({ room: spaceRoom });
 | |
|             expect(screen.getByTestId("space-header")).toBeInTheDocument();
 | |
|         });
 | |
| 
 | |
|         it("renders encryption info panel without pending verification", () => {
 | |
|             renderComponent({ phase: RightPanelPhases.EncryptionPanel, room: mockRoom });
 | |
|             expect(screen.getByRole("heading", { name: /encryption/i })).toBeInTheDocument();
 | |
|         });
 | |
| 
 | |
|         it("renders encryption verification panel with pending verification", () => {
 | |
|             renderComponent({ phase: RightPanelPhases.EncryptionPanel, verificationRequest, room: mockRoom });
 | |
| 
 | |
|             expect(screen.queryByRole("heading", { name: /encryption/i })).not.toBeInTheDocument();
 | |
|             // the verificationRequest has phase of Phase.Ready but .otherPartySupportsMethod
 | |
|             // will not return true, so we expect to see the noCommonMethod error from VerificationPanel
 | |
|             expect(screen.getByText(/try with a different client/i)).toBeInTheDocument();
 | |
|         });
 | |
| 
 | |
|         it("renders the message button", () => {
 | |
|             render(
 | |
|                 <MatrixClientContext.Provider value={mockClient}>
 | |
|                     <UserInfo {...defaultProps} />
 | |
|                 </MatrixClientContext.Provider>,
 | |
|             );
 | |
| 
 | |
|             screen.getByRole("button", { name: "Message" });
 | |
|         });
 | |
| 
 | |
|         it("hides the message button if the visibility customisation hides all create room features", () => {
 | |
|             mocked(shouldShowComponent).withImplementation(
 | |
|                 (component) => {
 | |
|                     return component !== UIComponent.CreateRooms;
 | |
|                 },
 | |
|                 () => {
 | |
|                     render(
 | |
|                         <MatrixClientContext.Provider value={mockClient}>
 | |
|                             <UserInfo {...defaultProps} />
 | |
|                         </MatrixClientContext.Provider>,
 | |
|                     );
 | |
| 
 | |
|                     expect(screen.queryByRole("button", { name: "Message" })).toBeNull();
 | |
|                 },
 | |
|             );
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("with crypto enabled", () => {
 | |
|         beforeEach(() => {
 | |
|             mockClient.isCryptoEnabled.mockReturnValue(true);
 | |
|             mockClient.doesServerSupportUnstableFeature.mockResolvedValue(true);
 | |
|             mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(false, false, false));
 | |
| 
 | |
|             const device = new Device({
 | |
|                 deviceId: "d1",
 | |
|                 userId: defaultUserId,
 | |
|                 displayName: "my device",
 | |
|                 algorithms: [],
 | |
|                 keys: new Map(),
 | |
|             });
 | |
|             const devicesMap = new Map<string, Device>([[device.deviceId, device]]);
 | |
|             const userDeviceMap = new Map<string, Map<string, Device>>([[defaultUserId, devicesMap]]);
 | |
|             mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
 | |
|         });
 | |
| 
 | |
|         it("renders a device list which can be expanded", async () => {
 | |
|             renderComponent();
 | |
|             await act(flushPromises);
 | |
| 
 | |
|             // check the button exists with the expected text
 | |
|             const devicesButton = screen.getByRole("button", { name: "1 session" });
 | |
| 
 | |
|             // click it
 | |
|             await userEvent.click(devicesButton);
 | |
| 
 | |
|             // there should now be a button with the device id which should contain the device name
 | |
|             expect(screen.getByRole("button", { name: "my device" })).toBeInTheDocument();
 | |
|         });
 | |
| 
 | |
|         it("renders <BasicUserInfo />", async () => {
 | |
|             mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(false, false, false));
 | |
| 
 | |
|             const { container } = renderComponent({
 | |
|                 phase: RightPanelPhases.SpaceMemberInfo,
 | |
|                 verificationRequest,
 | |
|                 room: mockRoom,
 | |
|             });
 | |
|             await act(flushPromises);
 | |
| 
 | |
|             await waitFor(() => expect(screen.getByRole("button", { name: "Verify" })).toBeInTheDocument());
 | |
|             expect(container).toMatchSnapshot();
 | |
|         });
 | |
| 
 | |
|         describe("device dehydration", () => {
 | |
|             it("hides a verified dehydrated device (unverified user)", async () => {
 | |
|                 const device1 = new Device({
 | |
|                     deviceId: "d1",
 | |
|                     userId: defaultUserId,
 | |
|                     displayName: "my device",
 | |
|                     algorithms: [],
 | |
|                     keys: new Map(),
 | |
|                 });
 | |
|                 const device2 = new Device({
 | |
|                     deviceId: "d2",
 | |
|                     userId: defaultUserId,
 | |
|                     displayName: "dehydrated device",
 | |
|                     algorithms: [],
 | |
|                     keys: new Map(),
 | |
|                     dehydrated: true,
 | |
|                 });
 | |
|                 const devicesMap = new Map<string, Device>([
 | |
|                     [device1.deviceId, device1],
 | |
|                     [device2.deviceId, device2],
 | |
|                 ]);
 | |
|                 const userDeviceMap = new Map<string, Map<string, Device>>([[defaultUserId, devicesMap]]);
 | |
|                 mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
 | |
| 
 | |
|                 renderComponent({ room: mockRoom });
 | |
|                 await act(flushPromises);
 | |
| 
 | |
|                 // check the button exists with the expected text (the dehydrated device shouldn't be counted)
 | |
|                 const devicesButton = screen.getByRole("button", { name: "1 session" });
 | |
| 
 | |
|                 // click it
 | |
|                 await act(() => {
 | |
|                     return userEvent.click(devicesButton);
 | |
|                 });
 | |
| 
 | |
|                 // there should now be a button with the non-dehydrated device ID
 | |
|                 expect(screen.getByRole("button", { name: "my device" })).toBeInTheDocument();
 | |
| 
 | |
|                 // but not for the dehydrated device ID
 | |
|                 expect(screen.queryByRole("button", { name: "dehydrated device" })).not.toBeInTheDocument();
 | |
| 
 | |
|                 // there should be a line saying that the user has "Offline device" enabled
 | |
|                 expect(screen.getByText("Offline device enabled")).toBeInTheDocument();
 | |
|             });
 | |
| 
 | |
|             it("hides a verified dehydrated device (verified user)", async () => {
 | |
|                 const device1 = new Device({
 | |
|                     deviceId: "d1",
 | |
|                     userId: defaultUserId,
 | |
|                     displayName: "my device",
 | |
|                     algorithms: [],
 | |
|                     keys: new Map(),
 | |
|                 });
 | |
|                 const device2 = new Device({
 | |
|                     deviceId: "d2",
 | |
|                     userId: defaultUserId,
 | |
|                     displayName: "dehydrated device",
 | |
|                     algorithms: [],
 | |
|                     keys: new Map(),
 | |
|                     dehydrated: true,
 | |
|                 });
 | |
|                 const devicesMap = new Map<string, Device>([
 | |
|                     [device1.deviceId, device1],
 | |
|                     [device2.deviceId, device2],
 | |
|                 ]);
 | |
|                 const userDeviceMap = new Map<string, Map<string, Device>>([[defaultUserId, devicesMap]]);
 | |
|                 mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
 | |
|                 mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, true, true));
 | |
|                 mockCrypto.getDeviceVerificationStatus.mockResolvedValue({
 | |
|                     isVerified: () => true,
 | |
|                 } as DeviceVerificationStatus);
 | |
| 
 | |
|                 renderComponent({ room: mockRoom });
 | |
|                 await act(flushPromises);
 | |
| 
 | |
|                 // check the button exists with the expected text (the dehydrated device shouldn't be counted)
 | |
|                 const devicesButton = screen.getByRole("button", { name: "1 verified session" });
 | |
| 
 | |
|                 // click it
 | |
|                 await act(() => {
 | |
|                     return userEvent.click(devicesButton);
 | |
|                 });
 | |
| 
 | |
|                 // there should now be a button with the non-dehydrated device ID
 | |
|                 expect(screen.getByTitle("d1")).toBeInTheDocument();
 | |
| 
 | |
|                 // but not for the dehydrated device ID
 | |
|                 expect(screen.queryByTitle("d2")).not.toBeInTheDocument();
 | |
| 
 | |
|                 // there should be a line saying that the user has "Offline device" enabled
 | |
|                 expect(screen.getByText("Offline device enabled")).toBeInTheDocument();
 | |
|             });
 | |
| 
 | |
|             it("shows an unverified dehydrated device", async () => {
 | |
|                 const device1 = new Device({
 | |
|                     deviceId: "d1",
 | |
|                     userId: defaultUserId,
 | |
|                     displayName: "my device",
 | |
|                     algorithms: [],
 | |
|                     keys: new Map(),
 | |
|                 });
 | |
|                 const device2 = new Device({
 | |
|                     deviceId: "d2",
 | |
|                     userId: defaultUserId,
 | |
|                     displayName: "dehydrated device",
 | |
|                     algorithms: [],
 | |
|                     keys: new Map(),
 | |
|                     dehydrated: true,
 | |
|                 });
 | |
|                 const devicesMap = new Map<string, Device>([
 | |
|                     [device1.deviceId, device1],
 | |
|                     [device2.deviceId, device2],
 | |
|                 ]);
 | |
|                 const userDeviceMap = new Map<string, Map<string, Device>>([[defaultUserId, devicesMap]]);
 | |
|                 mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
 | |
|                 mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, true, true));
 | |
| 
 | |
|                 renderComponent({ room: mockRoom });
 | |
|                 await act(flushPromises);
 | |
| 
 | |
|                 // the dehydrated device should be shown as an unverified device, which means
 | |
|                 // there should now be a button with the device id ...
 | |
|                 const deviceButton = screen.getByRole("button", { name: "dehydrated device" });
 | |
| 
 | |
|                 // ... which should contain the device name
 | |
|                 expect(within(deviceButton).getByText("dehydrated device")).toBeInTheDocument();
 | |
|             });
 | |
| 
 | |
|             it("shows dehydrated devices if there is more than one", async () => {
 | |
|                 const device1 = new Device({
 | |
|                     deviceId: "d1",
 | |
|                     userId: defaultUserId,
 | |
|                     displayName: "dehydrated device 1",
 | |
|                     algorithms: [],
 | |
|                     keys: new Map(),
 | |
|                     dehydrated: true,
 | |
|                 });
 | |
|                 const device2 = new Device({
 | |
|                     deviceId: "d2",
 | |
|                     userId: defaultUserId,
 | |
|                     displayName: "dehydrated device 2",
 | |
|                     algorithms: [],
 | |
|                     keys: new Map(),
 | |
|                     dehydrated: true,
 | |
|                 });
 | |
|                 const devicesMap = new Map<string, Device>([
 | |
|                     [device1.deviceId, device1],
 | |
|                     [device2.deviceId, device2],
 | |
|                 ]);
 | |
|                 const userDeviceMap = new Map<string, Map<string, Device>>([[defaultUserId, devicesMap]]);
 | |
|                 mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
 | |
| 
 | |
|                 renderComponent({ room: mockRoom });
 | |
|                 await act(flushPromises);
 | |
| 
 | |
|                 // check the button exists with the expected text (the dehydrated device shouldn't be counted)
 | |
|                 const devicesButton = screen.getByRole("button", { name: "2 sessions" });
 | |
| 
 | |
|                 // click it
 | |
|                 await act(() => {
 | |
|                     return userEvent.click(devicesButton);
 | |
|                 });
 | |
| 
 | |
|                 // the dehydrated devices should be shown as an unverified device, which means
 | |
|                 // there should now be a button with the first dehydrated device...
 | |
|                 const device1Button = screen.getByRole("button", { name: "dehydrated device 1" });
 | |
|                 expect(device1Button).toBeVisible();
 | |
| 
 | |
|                 // ... which should contain the device name
 | |
|                 expect(within(device1Button).getByText("dehydrated device 1")).toBeInTheDocument();
 | |
|                 // and a button with the second dehydrated device...
 | |
|                 const device2Button = screen.getByRole("button", { name: "dehydrated device 2" });
 | |
|                 expect(device2Button).toBeVisible();
 | |
| 
 | |
|                 // ... which should contain the device name
 | |
|                 expect(within(device2Button).getByText("dehydrated device 2")).toBeInTheDocument();
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("with an encrypted room", () => {
 | |
|         beforeEach(() => {
 | |
|             mockClient.isCryptoEnabled.mockReturnValue(true);
 | |
|             mockClient.isRoomEncrypted.mockReturnValue(true);
 | |
|         });
 | |
| 
 | |
|         it("renders unverified user info", async () => {
 | |
|             mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(false, false, false));
 | |
|             renderComponent({ room: mockRoom });
 | |
|             await act(flushPromises);
 | |
| 
 | |
|             const userHeading = screen.getByRole("heading", { name: /@user:example.com/ });
 | |
| 
 | |
|             // there should be a "normal" E2E padlock
 | |
|             expect(userHeading.getElementsByClassName("mx_E2EIcon_normal")).toHaveLength(1);
 | |
|         });
 | |
| 
 | |
|         it("renders verified user info", async () => {
 | |
|             mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, false, false));
 | |
|             renderComponent({ room: mockRoom });
 | |
|             await act(flushPromises);
 | |
| 
 | |
|             const userHeading = screen.getByRole("heading", { name: /@user:example.com/ });
 | |
| 
 | |
|             // there should be a "verified" E2E padlock
 | |
|             expect(userHeading.getElementsByClassName("mx_E2EIcon_verified")).toHaveLength(1);
 | |
|         });
 | |
|     });
 | |
| });
 | |
| 
 | |
| describe("<UserInfoHeader />", () => {
 | |
|     const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
 | |
| 
 | |
|     const defaultProps = {
 | |
|         member: defaultMember,
 | |
|         roomId: defaultRoomId,
 | |
|     };
 | |
| 
 | |
|     const renderComponent = (props = {}) => {
 | |
|         const Wrapper = (wrapperProps = {}) => {
 | |
|             return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
 | |
|         };
 | |
| 
 | |
|         return render(<UserInfoHeader {...defaultProps} {...props} />, {
 | |
|             wrapper: Wrapper,
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     it("does not render an e2e icon in the header if e2eStatus prop is undefined", () => {
 | |
|         renderComponent();
 | |
|         const header = screen.getByRole("heading", { name: defaultUserId });
 | |
| 
 | |
|         expect(header.getElementsByClassName("mx_E2EIcon")).toHaveLength(0);
 | |
|     });
 | |
| 
 | |
|     it("renders an e2e icon in the header if e2eStatus prop is defined", () => {
 | |
|         renderComponent({ e2eStatus: E2EStatus.Normal });
 | |
|         const header = screen.getByRole("heading");
 | |
| 
 | |
|         expect(header.getElementsByClassName("mx_E2EIcon")).toHaveLength(1);
 | |
|     });
 | |
| 
 | |
|     it("renders custom user identifiers in the header", () => {
 | |
|         renderComponent();
 | |
| 
 | |
|         expect(screen.getByText("customUserIdentifier")).toBeInTheDocument();
 | |
|     });
 | |
| });
 | |
| 
 | |
| describe("<DeviceItem />", () => {
 | |
|     const device = { deviceId: "deviceId", displayName: "deviceName" } as Device;
 | |
|     const defaultProps = {
 | |
|         userId: defaultUserId,
 | |
|         device,
 | |
|         isUserVerified: false,
 | |
|     };
 | |
| 
 | |
|     const renderComponent = (props = {}) => {
 | |
|         const Wrapper = (wrapperProps = {}) => {
 | |
|             return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
 | |
|         };
 | |
| 
 | |
|         return render(<DeviceItem {...defaultProps} {...props} />, {
 | |
|             wrapper: Wrapper,
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     const setMockDeviceTrust = (isVerified = false, isCrossSigningVerified = false) => {
 | |
|         mockCrypto.getDeviceVerificationStatus.mockResolvedValue({
 | |
|             isVerified: () => isVerified,
 | |
|             crossSigningVerified: isCrossSigningVerified,
 | |
|         } as DeviceVerificationStatus);
 | |
|     };
 | |
| 
 | |
|     const mockVerifyDevice = jest.spyOn(mockVerification, "verifyDevice");
 | |
| 
 | |
|     beforeEach(() => {
 | |
|         setMockDeviceTrust();
 | |
|     });
 | |
| 
 | |
|     afterEach(() => {
 | |
|         mockCrypto.getDeviceVerificationStatus.mockReset();
 | |
|         mockVerifyDevice.mockClear();
 | |
|     });
 | |
| 
 | |
|     afterAll(() => {
 | |
|         mockVerifyDevice.mockRestore();
 | |
|     });
 | |
| 
 | |
|     it("with unverified user and device, displays button without a label", async () => {
 | |
|         renderComponent();
 | |
|         await act(flushPromises);
 | |
| 
 | |
|         expect(screen.getByRole("button", { name: device.displayName! })).toBeInTheDocument();
 | |
|         expect(screen.queryByText(/trusted/i)).not.toBeInTheDocument();
 | |
|     });
 | |
| 
 | |
|     it("with verified user only, displays button with a 'Not trusted' label", async () => {
 | |
|         renderComponent({ isUserVerified: true });
 | |
|         await act(flushPromises);
 | |
| 
 | |
|         const button = screen.getByRole("button", { name: device.displayName });
 | |
|         expect(button).toHaveTextContent(`${device.displayName}Not trusted`);
 | |
|     });
 | |
| 
 | |
|     it("with verified device only, displays no button without a label", async () => {
 | |
|         setMockDeviceTrust(true);
 | |
|         renderComponent();
 | |
|         await act(flushPromises);
 | |
| 
 | |
|         expect(screen.getByText(device.displayName!)).toBeInTheDocument();
 | |
|         expect(screen.queryByText(/trusted/)).not.toBeInTheDocument();
 | |
|     });
 | |
| 
 | |
|     it("when userId is the same as userId from client, uses isCrossSigningVerified to determine if button is shown", async () => {
 | |
|         const deferred = defer<DeviceVerificationStatus>();
 | |
|         mockCrypto.getDeviceVerificationStatus.mockReturnValue(deferred.promise);
 | |
| 
 | |
|         mockClient.getSafeUserId.mockReturnValueOnce(defaultUserId);
 | |
|         mockClient.getUserId.mockReturnValueOnce(defaultUserId);
 | |
|         renderComponent();
 | |
|         await act(flushPromises);
 | |
| 
 | |
|         // set trust to be false for isVerified, true for isCrossSigningVerified
 | |
|         deferred.resolve({
 | |
|             isVerified: () => false,
 | |
|             crossSigningVerified: true,
 | |
|         } as DeviceVerificationStatus);
 | |
| 
 | |
|         await expect(screen.findByText(device.displayName!)).resolves.toBeInTheDocument();
 | |
|         // expect to see no button in this case
 | |
|         expect(screen.queryByRole("button")).not.toBeInTheDocument();
 | |
|     });
 | |
| 
 | |
|     it("with verified user and device, displays no button and a 'Trusted' label", async () => {
 | |
|         setMockDeviceTrust(true);
 | |
|         renderComponent({ isUserVerified: true });
 | |
|         await act(flushPromises);
 | |
| 
 | |
|         expect(screen.queryByRole("button")).not.toBeInTheDocument();
 | |
|         expect(screen.getByText(device.displayName!)).toBeInTheDocument();
 | |
|         expect(screen.getByText("Trusted")).toBeInTheDocument();
 | |
|     });
 | |
| 
 | |
|     it("does not call verifyDevice if client.getUser returns null", async () => {
 | |
|         mockClient.getUser.mockReturnValueOnce(null);
 | |
|         renderComponent();
 | |
|         await act(flushPromises);
 | |
| 
 | |
|         const button = screen.getByRole("button", { name: device.displayName! });
 | |
|         expect(button).toBeInTheDocument();
 | |
|         await userEvent.click(button);
 | |
| 
 | |
|         expect(mockVerifyDevice).not.toHaveBeenCalled();
 | |
|     });
 | |
| 
 | |
|     it("calls verifyDevice if client.getUser returns an object", async () => {
 | |
|         mockClient.getUser.mockReturnValueOnce(defaultUser);
 | |
|         // set mock return of isGuest to short circuit verifyDevice call to avoid
 | |
|         // even more mocking
 | |
|         mockClient.isGuest.mockReturnValueOnce(true);
 | |
|         renderComponent();
 | |
|         await act(flushPromises);
 | |
| 
 | |
|         const button = screen.getByRole("button", { name: device.displayName! });
 | |
|         expect(button).toBeInTheDocument();
 | |
|         await userEvent.click(button);
 | |
| 
 | |
|         expect(mockVerifyDevice).toHaveBeenCalledTimes(1);
 | |
|         expect(mockVerifyDevice).toHaveBeenCalledWith(mockClient, defaultUser, device);
 | |
|     });
 | |
| 
 | |
|     it("with display name", async () => {
 | |
|         const { container } = renderComponent();
 | |
|         await act(flushPromises);
 | |
| 
 | |
|         expect(container).toMatchSnapshot();
 | |
|     });
 | |
| 
 | |
|     it("without display name", async () => {
 | |
|         const device = { deviceId: "deviceId" } as Device;
 | |
|         const { container } = renderComponent({ device, userId: defaultUserId });
 | |
|         await act(flushPromises);
 | |
| 
 | |
|         expect(container).toMatchSnapshot();
 | |
|     });
 | |
| 
 | |
|     it("ambiguous display name", async () => {
 | |
|         const device = { deviceId: "deviceId", ambiguous: true, displayName: "my display name" };
 | |
|         const { container } = renderComponent({ device, userId: defaultUserId });
 | |
|         await act(flushPromises);
 | |
| 
 | |
|         expect(container).toMatchSnapshot();
 | |
|     });
 | |
| });
 | |
| 
 | |
| describe("<UserOptionsSection />", () => {
 | |
|     const member = new RoomMember(defaultRoomId, defaultUserId);
 | |
|     const defaultProps = { member, isIgnored: false, 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", () => {
 | |
|         renderComponent();
 | |
|         expect(screen.getByRole("button", { name: /share link to user/i })).toBeInTheDocument();
 | |
|     });
 | |
| 
 | |
|     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 ignore, 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: /ignore/i })).toBeInTheDocument();
 | |
|         expect(screen.getByRole("button", { name: /message/i })).toBeInTheDocument();
 | |
|         expect(screen.getByRole("button", { name: /mention/i })).toBeInTheDocument();
 | |
|     });
 | |
| 
 | |
|     it("when call to client.getRoom is null, does not show read receipt button", () => {
 | |
|         mockClient.getRoom.mockReturnValueOnce(null);
 | |
|         renderComponent();
 | |
| 
 | |
|         expect(screen.queryByRole("button", { name: /jump to read receipt/i })).not.toBeInTheDocument();
 | |
|     });
 | |
| 
 | |
|     it("when call to client.getRoom is non-null and room.getEventReadUpTo is null, does not show read receipt button", () => {
 | |
|         mockRoom.getEventReadUpTo.mockReturnValueOnce(null);
 | |
|         mockClient.getRoom.mockReturnValueOnce(mockRoom);
 | |
|         renderComponent();
 | |
| 
 | |
|         expect(screen.queryByRole("button", { name: /jump to read receipt/i })).not.toBeInTheDocument();
 | |
|     });
 | |
| 
 | |
|     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/i })).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/i });
 | |
| 
 | |
|         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/i });
 | |
| 
 | |
|         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 waitFor(() => {
 | |
|             expect(screen.getByText(mockErrorMessage.message)).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 waitFor(() => {
 | |
|             expect(screen.getByText(/operation failed/i)).toBeInTheDocument();
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     it("shows a modal before ignoring the user", async () => {
 | |
|         const originalCreateDialog = Modal.createDialog;
 | |
|         const modalSpy = (Modal.createDialog = jest.fn().mockReturnValue({
 | |
|             finished: Promise.resolve([true]),
 | |
|             close: () => {},
 | |
|         }));
 | |
| 
 | |
|         try {
 | |
|             mockClient.getIgnoredUsers.mockReturnValue([]);
 | |
|             renderComponent({ isIgnored: false });
 | |
| 
 | |
|             await userEvent.click(screen.getByRole("button", { name: "Ignore" }));
 | |
|             expect(modalSpy).toHaveBeenCalled();
 | |
|             expect(mockClient.setIgnoredUsers).toHaveBeenLastCalledWith([member.userId]);
 | |
|         } finally {
 | |
|             Modal.createDialog = originalCreateDialog;
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     it("cancels ignoring the user", async () => {
 | |
|         const originalCreateDialog = Modal.createDialog;
 | |
|         const modalSpy = (Modal.createDialog = jest.fn().mockReturnValue({
 | |
|             finished: Promise.resolve([false]),
 | |
|             close: () => {},
 | |
|         }));
 | |
| 
 | |
|         try {
 | |
|             mockClient.getIgnoredUsers.mockReturnValue([]);
 | |
|             renderComponent({ isIgnored: false });
 | |
| 
 | |
|             await userEvent.click(screen.getByRole("button", { name: "Ignore" }));
 | |
|             expect(modalSpy).toHaveBeenCalled();
 | |
|             expect(mockClient.setIgnoredUsers).not.toHaveBeenCalled();
 | |
|         } finally {
 | |
|             Modal.createDialog = originalCreateDialog;
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     it("unignores the user", async () => {
 | |
|         mockClient.getIgnoredUsers.mockReturnValue([member.userId]);
 | |
|         renderComponent({ isIgnored: true });
 | |
| 
 | |
|         await userEvent.click(screen.getByRole("button", { name: "Unignore" }));
 | |
|         expect(mockClient.setIgnoredUsers).toHaveBeenCalledWith([]);
 | |
|     });
 | |
| 
 | |
|     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 = defer<string>();
 | |
|             mocked(startDmOnFirstMessage).mockReturnValue(deferred.promise);
 | |
| 
 | |
|             renderComponent({ member });
 | |
|             await userEvent.click(screen.getByText("Message"));
 | |
| 
 | |
|             // Checking the attribute, because the button is a DIV and toBeDisabled() does not work.
 | |
|             expect(screen.getByText("Message")).toHaveAttribute("disabled");
 | |
| 
 | |
|             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.getByText("Message")).not.toHaveAttribute("disabled");
 | |
|         },
 | |
|     );
 | |
| });
 | |
| 
 | |
| describe("<PowerLevelEditor />", () => {
 | |
|     const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
 | |
| 
 | |
|     let defaultProps: Parameters<typeof PowerLevelEditor>[0];
 | |
|     beforeEach(() => {
 | |
|         defaultProps = {
 | |
|             user: defaultMember,
 | |
|             room: mockRoom,
 | |
|             roomPermissions: {
 | |
|                 modifyLevelMax: 100,
 | |
|                 canEdit: false,
 | |
|                 canInvite: false,
 | |
|             },
 | |
|         };
 | |
|     });
 | |
| 
 | |
|     const renderComponent = (props = {}) => {
 | |
|         const Wrapper = (wrapperProps = {}) => {
 | |
|             return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
 | |
|         };
 | |
| 
 | |
|         return render(<PowerLevelEditor {...defaultProps} {...props} />, {
 | |
|             wrapper: Wrapper,
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     it("renders a power level combobox", () => {
 | |
|         renderComponent();
 | |
| 
 | |
|         expect(screen.getByRole("combobox", { name: "Power level" })).toBeInTheDocument();
 | |
|     });
 | |
| 
 | |
|     it("renders a combobox and attempts to change power level on change of the combobox", async () => {
 | |
|         const startPowerLevel = 999;
 | |
|         const powerLevelEvent = new MatrixEvent({
 | |
|             type: EventType.RoomPowerLevels,
 | |
|             content: { users: { [defaultUserId]: startPowerLevel }, users_default: 1 },
 | |
|         });
 | |
|         mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent);
 | |
|         mockClient.getSafeUserId.mockReturnValueOnce(defaultUserId);
 | |
|         mockClient.getUserId.mockReturnValueOnce(defaultUserId);
 | |
|         mockClient.setPowerLevel.mockResolvedValueOnce({ event_id: "123" });
 | |
|         renderComponent();
 | |
| 
 | |
|         const changedPowerLevel = 100;
 | |
| 
 | |
|         fireEvent.change(screen.getByRole("combobox", { name: "Power level" }), {
 | |
|             target: { value: changedPowerLevel },
 | |
|         });
 | |
| 
 | |
|         await screen.findByText("Demote", { exact: true });
 | |
| 
 | |
|         // firing the event will raise a dialog warning about self demotion, wait for this to appear then click on it
 | |
|         await userEvent.click(await screen.findByText("Demote", { exact: true }));
 | |
|         expect(mockClient.setPowerLevel).toHaveBeenCalledTimes(1);
 | |
|         expect(mockClient.setPowerLevel).toHaveBeenCalledWith(mockRoom.roomId, defaultMember.userId, changedPowerLevel);
 | |
|     });
 | |
| });
 | |
| 
 | |
| describe("<RoomKickButton />", () => {
 | |
|     const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
 | |
|     const memberWithInviteMembership = { ...defaultMember, membership: KnownMembership.Invite };
 | |
|     const memberWithJoinMembership = { ...defaultMember, membership: KnownMembership.Join };
 | |
| 
 | |
|     let defaultProps: Parameters<typeof RoomKickButton>[0];
 | |
|     beforeEach(() => {
 | |
|         defaultProps = {
 | |
|             room: mockRoom,
 | |
|             member: defaultMember,
 | |
|             startUpdating: jest.fn(),
 | |
|             stopUpdating: jest.fn(),
 | |
|             isUpdating: false,
 | |
|         };
 | |
|     });
 | |
| 
 | |
|     const renderComponent = (props = {}) => {
 | |
|         const Wrapper = (wrapperProps = {}) => {
 | |
|             return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
 | |
|         };
 | |
| 
 | |
|         return render(<RoomKickButton {...defaultProps} {...props} />, {
 | |
|             wrapper: Wrapper,
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     const createDialogSpy: jest.SpyInstance = jest.spyOn(Modal, "createDialog");
 | |
| 
 | |
|     afterEach(() => {
 | |
|         createDialogSpy.mockReset();
 | |
|     });
 | |
| 
 | |
|     it("renders nothing if member.membership is undefined", () => {
 | |
|         // .membership is undefined in our member by default
 | |
|         const { container } = renderComponent();
 | |
|         expect(container).toBeEmptyDOMElement();
 | |
|     });
 | |
| 
 | |
|     it("renders something if member.membership is 'invite' or 'join'", () => {
 | |
|         let result = renderComponent({ member: memberWithInviteMembership });
 | |
|         expect(result.container).not.toBeEmptyDOMElement();
 | |
| 
 | |
|         cleanup();
 | |
| 
 | |
|         result = renderComponent({ member: memberWithJoinMembership });
 | |
|         expect(result.container).not.toBeEmptyDOMElement();
 | |
|     });
 | |
| 
 | |
|     it("renders the correct label", () => {
 | |
|         // test for room
 | |
|         renderComponent({ member: memberWithJoinMembership });
 | |
|         expect(screen.getByText(/remove from room/i)).toBeInTheDocument();
 | |
|         cleanup();
 | |
| 
 | |
|         renderComponent({ member: memberWithInviteMembership });
 | |
|         expect(screen.getByText(/disinvite from room/i)).toBeInTheDocument();
 | |
|         cleanup();
 | |
| 
 | |
|         // test for space
 | |
|         mockRoom.isSpaceRoom.mockReturnValue(true);
 | |
|         renderComponent({ member: memberWithJoinMembership });
 | |
|         expect(screen.getByText(/remove from space/i)).toBeInTheDocument();
 | |
|         cleanup();
 | |
| 
 | |
|         renderComponent({ member: memberWithInviteMembership });
 | |
|         expect(screen.getByText(/disinvite from space/i)).toBeInTheDocument();
 | |
|         cleanup();
 | |
|         mockRoom.isSpaceRoom.mockReturnValue(false);
 | |
|     });
 | |
| 
 | |
|     it("clicking the kick button calls Modal.createDialog with the correct arguments", async () => {
 | |
|         createDialogSpy.mockReturnValueOnce({ finished: Promise.resolve([]), close: jest.fn() });
 | |
| 
 | |
|         renderComponent({ room: mockSpace, member: memberWithInviteMembership });
 | |
|         await userEvent.click(screen.getByText(/disinvite from/i));
 | |
| 
 | |
|         // 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);
 | |
|     });
 | |
| });
 | |
| 
 | |
| describe("<BanToggleButton />", () => {
 | |
|     const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
 | |
|     const memberWithBanMembership = { ...defaultMember, membership: KnownMembership.Ban };
 | |
|     let defaultProps: Parameters<typeof BanToggleButton>[0];
 | |
|     beforeEach(() => {
 | |
|         defaultProps = {
 | |
|             room: mockRoom,
 | |
|             member: defaultMember,
 | |
|             startUpdating: jest.fn(),
 | |
|             stopUpdating: jest.fn(),
 | |
|             isUpdating: false,
 | |
|         };
 | |
|     });
 | |
| 
 | |
|     const renderComponent = (props = {}) => {
 | |
|         const Wrapper = (wrapperProps = {}) => {
 | |
|             return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
 | |
|         };
 | |
| 
 | |
|         return render(<BanToggleButton {...defaultProps} {...props} />, {
 | |
|             wrapper: Wrapper,
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     const createDialogSpy: jest.SpyInstance = jest.spyOn(Modal, "createDialog");
 | |
| 
 | |
|     afterEach(() => {
 | |
|         createDialogSpy.mockReset();
 | |
|     });
 | |
| 
 | |
|     it("renders the correct labels for banned and unbanned members", () => {
 | |
|         // test for room
 | |
|         // defaultMember is not banned
 | |
|         renderComponent();
 | |
|         expect(screen.getByText("Ban from room")).toBeInTheDocument();
 | |
|         cleanup();
 | |
| 
 | |
|         renderComponent({ member: memberWithBanMembership });
 | |
|         expect(screen.getByText("Unban from room")).toBeInTheDocument();
 | |
|         cleanup();
 | |
| 
 | |
|         // test for space
 | |
|         mockRoom.isSpaceRoom.mockReturnValue(true);
 | |
|         renderComponent();
 | |
|         expect(screen.getByText("Ban from space")).toBeInTheDocument();
 | |
|         cleanup();
 | |
| 
 | |
|         renderComponent({ member: memberWithBanMembership });
 | |
|         expect(screen.getByText("Unban from space")).toBeInTheDocument();
 | |
|         cleanup();
 | |
|         mockRoom.isSpaceRoom.mockReturnValue(false);
 | |
|     });
 | |
| 
 | |
|     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() });
 | |
| 
 | |
|         renderComponent({ room: mockSpace });
 | |
|         await userEvent.click(screen.getByText(/ban from/i));
 | |
| 
 | |
|         // 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() });
 | |
| 
 | |
|         renderComponent({ room: mockSpace, member: memberWithBanMembership });
 | |
|         await userEvent.click(screen.getByText(/ban from/i));
 | |
| 
 | |
|         // 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);
 | |
|     });
 | |
| });
 | |
| 
 | |
| describe("<RoomAdminToolsContainer />", () => {
 | |
|     const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
 | |
|     defaultMember.membership = KnownMembership.Invite;
 | |
| 
 | |
|     let defaultProps: Parameters<typeof RoomAdminToolsContainer>[0];
 | |
|     beforeEach(() => {
 | |
|         defaultProps = {
 | |
|             room: mockRoom,
 | |
|             member: defaultMember,
 | |
|             isUpdating: false,
 | |
|             startUpdating: jest.fn(),
 | |
|             stopUpdating: jest.fn(),
 | |
|             powerLevels: {},
 | |
|         };
 | |
|     });
 | |
| 
 | |
|     const renderComponent = (props = {}) => {
 | |
|         const Wrapper = (wrapperProps = {}) => {
 | |
|             return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
 | |
|         };
 | |
| 
 | |
|         return render(<RoomAdminToolsContainer {...defaultProps} {...props} />, {
 | |
|             wrapper: Wrapper,
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     it("returns a single empty div if room.getMember is falsy", () => {
 | |
|         const { asFragment } = renderComponent();
 | |
|         expect(asFragment()).toMatchInlineSnapshot(`
 | |
|             <DocumentFragment>
 | |
|               <div />
 | |
|             </DocumentFragment>
 | |
|         `);
 | |
|     });
 | |
| 
 | |
|     it("can return a single empty div in case where room.getMember is not falsy", () => {
 | |
|         mockRoom.getMember.mockReturnValueOnce(defaultMember);
 | |
|         const { asFragment } = renderComponent();
 | |
|         expect(asFragment()).toMatchInlineSnapshot(`
 | |
|             <DocumentFragment>
 | |
|               <div />
 | |
|             </DocumentFragment>
 | |
|         `);
 | |
|     });
 | |
| 
 | |
|     it("returns kick, redact messages, ban buttons if conditions met", () => {
 | |
|         const mockMeMember = new RoomMember(mockRoom.roomId, "arbitraryId");
 | |
|         mockMeMember.powerLevel = 51; // defaults to 50
 | |
|         mockRoom.getMember.mockReturnValueOnce(mockMeMember);
 | |
| 
 | |
|         const defaultMemberWithPowerLevel = { ...defaultMember, powerLevel: 0 };
 | |
| 
 | |
|         renderComponent({ member: defaultMemberWithPowerLevel });
 | |
| 
 | |
|         expect(screen.getByRole("heading", { name: /admin tools/i })).toBeInTheDocument();
 | |
|         expect(screen.getByText(/disinvite from room/i)).toBeInTheDocument();
 | |
|         expect(screen.getByText(/ban from room/i)).toBeInTheDocument();
 | |
|         expect(screen.getByText(/remove recent messages/i)).toBeInTheDocument();
 | |
|     });
 | |
| 
 | |
|     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,
 | |
|         };
 | |
| 
 | |
|         renderComponent({
 | |
|             member: defaultMemberWithPowerLevelAndJoinMembership,
 | |
|             powerLevels: { events: { "m.room.power_levels": 1 } },
 | |
|         });
 | |
| 
 | |
|         const button = screen.getByText(/mute/i);
 | |
|         expect(button).toBeInTheDocument();
 | |
|         fireEvent.click(button);
 | |
|         expect(defaultProps.startUpdating).toHaveBeenCalled();
 | |
|     });
 | |
| 
 | |
|     it("should disable buttons when isUpdating=true", () => {
 | |
|         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,
 | |
|         };
 | |
| 
 | |
|         renderComponent({
 | |
|             member: defaultMemberWithPowerLevelAndJoinMembership,
 | |
|             powerLevels: { events: { "m.room.power_levels": 1 } },
 | |
|             isUpdating: true,
 | |
|         });
 | |
| 
 | |
|         const button = screen.getByText(/mute/i);
 | |
|         expect(button).toBeInTheDocument();
 | |
|         expect(button).toHaveAttribute("disabled");
 | |
|         expect(button).toHaveAttribute("aria-disabled", "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);
 | |
| 
 | |
|         renderComponent({
 | |
|             member: mockMeMember,
 | |
|             powerLevels: { events: { "m.room.power_levels": 100 } },
 | |
|         });
 | |
| 
 | |
|         const button = screen.queryByText(/mute/i);
 | |
|         expect(button).not.toBeInTheDocument();
 | |
|     });
 | |
| });
 | |
| 
 | |
| describe("disambiguateDevices", () => {
 | |
|     it("does not add ambiguous key to unique names", () => {
 | |
|         const initialDevices = [
 | |
|             { deviceId: "id1", displayName: "name1" } as Device,
 | |
|             { deviceId: "id2", displayName: "name2" } as Device,
 | |
|             { deviceId: "id3", displayName: "name3" } as Device,
 | |
|         ];
 | |
|         disambiguateDevices(initialDevices);
 | |
| 
 | |
|         // mutates input so assert against initialDevices
 | |
|         initialDevices.forEach((device) => {
 | |
|             expect(device).not.toHaveProperty("ambiguous");
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     it("adds ambiguous key to all ids with non-unique names", () => {
 | |
|         const uniqueNameDevices = [
 | |
|             { deviceId: "id3", displayName: "name3" } as Device,
 | |
|             { deviceId: "id4", displayName: "name4" } as Device,
 | |
|             { deviceId: "id6", displayName: "name6" } as Device,
 | |
|         ];
 | |
|         const nonUniqueNameDevices = [
 | |
|             { deviceId: "id1", displayName: "nonUnique" } as Device,
 | |
|             { deviceId: "id2", displayName: "nonUnique" } as Device,
 | |
|             { deviceId: "id5", displayName: "nonUnique" } as Device,
 | |
|         ];
 | |
|         const initialDevices = [...uniqueNameDevices, ...nonUniqueNameDevices];
 | |
|         disambiguateDevices(initialDevices);
 | |
| 
 | |
|         // mutates input so assert against initialDevices
 | |
|         uniqueNameDevices.forEach((device) => {
 | |
|             expect(device).not.toHaveProperty("ambiguous");
 | |
|         });
 | |
|         nonUniqueNameDevices.forEach((device) => {
 | |
|             expect(device).toHaveProperty("ambiguous", true);
 | |
|         });
 | |
|     });
 | |
| });
 | |
| 
 | |
| describe("isMuted", () => {
 | |
|     // this member has a power level of 0
 | |
|     const isMutedMember = new RoomMember(defaultRoomId, defaultUserId);
 | |
| 
 | |
|     it("returns false if either argument is falsy", () => {
 | |
|         // @ts-ignore to let us purposely pass incorrect args
 | |
|         expect(isMuted(isMutedMember, 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(isMutedMember, powerLevelContents)).toBe(false);
 | |
|     });
 | |
| 
 | |
|     it("when powerLevelContent.events is undefined, uses .events_default", () => {
 | |
|         const higherPowerLevelContents = { events_default: 10 };
 | |
|         expect(isMuted(isMutedMember, higherPowerLevelContents)).toBe(true);
 | |
| 
 | |
|         const lowerPowerLevelContents = { events_default: -10 };
 | |
|         expect(isMuted(isMutedMember, 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(isMutedMember, higherPowerLevelContents)).toBe(true);
 | |
| 
 | |
|         const lowerPowerLevelContents = { events: {}, events_default: -10 };
 | |
|         expect(isMuted(isMutedMember, 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(isMutedMember, higherPowerLevelContents)).toBe(false);
 | |
| 
 | |
|         const lowerPowerLevelContents = { events: { "m.room.message": 10 }, events_default: -10 };
 | |
|         expect(isMuted(isMutedMember, lowerPowerLevelContents)).toBe(true);
 | |
|     });
 | |
| });
 | |
| 
 | |
| describe("getPowerLevels", () => {
 | |
|     it("returns an empty object when room.currentState.getStateEvents return null", () => {
 | |
|         mockRoom.currentState.getStateEvents.mockReturnValueOnce(null);
 | |
|         expect(getPowerLevels(mockRoom)).toEqual({});
 | |
|     });
 | |
| });
 |