diff --git a/src/components/views/right_panel/BaseCard.tsx b/src/components/views/right_panel/BaseCard.tsx index 186460b44d..643af55605 100644 --- a/src/components/views/right_panel/BaseCard.tsx +++ b/src/components/views/right_panel/BaseCard.tsx @@ -69,7 +69,7 @@ const BaseCard: React.FC = forwardRef( if (onClose) { closeButton = ( { +export const disambiguateDevices = (devices: IDevice[]) => { const names = Object.create(null); for (let i = 0; i < devices.length; i++) { const name = devices[i].getDisplayName(); @@ -94,7 +95,7 @@ const disambiguateDevices = (devices: IDevice[]) => { } for (const name in names) { if (names[name].length > 1) { - names[name].forEach((j) => { + names[name].forEach((j: number) => { devices[j].ambiguous = true; }); } @@ -149,7 +150,7 @@ function useHasCrossSigningKeys(cli: MatrixClient, member: User, canVerify: bool }, [cli, member, canVerify]); } -function DeviceItem({ userId, device }: { userId: string; device: IDevice }) { +export function DeviceItem({ userId, device }: { userId: string; device: IDevice }) { const cli = useContext(MatrixClientContext); const isMe = userId === cli.getUserId(); const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId); @@ -172,7 +173,10 @@ function DeviceItem({ userId, device }: { userId: string; device: IDevice }) { }); const onDeviceClick = () => { - verifyDevice(cli.getUser(userId), device); + const user = cli.getUser(userId); + if (user) { + verifyDevice(user, device); + } }; let deviceName; @@ -315,7 +319,7 @@ const MessageButton = ({ member }: { member: RoomMember }) => { ); }; -const UserOptionsSection: React.FC<{ +export const UserOptionsSection: React.FC<{ member: RoomMember; isIgnored: boolean; canInvite: boolean; @@ -367,7 +371,8 @@ const UserOptionsSection: React.FC<{ dis.dispatch({ action: Action.ViewRoom, highlighted: true, - event_id: room.getEventReadUpTo(member.userId), + // this could return null, the default prevents a type error + event_id: room?.getEventReadUpTo(member.userId) || undefined, room_id: member.roomId, metricsTrigger: undefined, // room doesn't change }); @@ -402,16 +407,18 @@ const UserOptionsSection: React.FC<{ const onInviteUserButton = async (ev: ButtonEvent) => { try { // We use a MultiInviter to re-use the invite logic, even though we're only inviting one user. - const inviter = new MultiInviter(roomId); + const inviter = new MultiInviter(roomId || ""); await inviter.invite([member.userId]).then(() => { if (inviter.getCompletionState(member.userId) !== "invited") { throw new Error(inviter.getErrorText(member.userId)); } }); } catch (err) { + const description = err instanceof Error ? err.message : _t("Operation failed"); + Modal.createDialog(ErrorDialog, { title: _t("Failed to invite"), - description: err && err.message ? err.message : _t("Operation failed"), + description, }); } @@ -432,10 +439,7 @@ const UserOptionsSection: React.FC<{ ); - let directMessageButton: JSX.Element; - if (!isMe) { - directMessageButton = ; - } + const directMessageButton = isMe ? null : ; return (
@@ -499,16 +503,24 @@ interface IPowerLevelsContent { redact?: number; } -const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) => { +export const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) => { if (!powerLevelContent || !member) return false; const levelToSend = (powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) || powerLevelContent.events_default; + + // levelToSend could be undefined as .events_default is optional. Coercing in this case using + // Number() would always return false, so this preserves behaviour + // FIXME: per the spec, if `events_default` is unset, it defaults to zero. If + // the member has a negative powerlevel, this will give an incorrect result. + if (levelToSend === undefined) return false; + return member.powerLevel < levelToSend; }; -const getPowerLevels = (room) => room?.currentState?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {}; +export const getPowerLevels = (room: Room): IPowerLevelsContent => + room?.currentState?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {}; export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => { const [powerLevels, setPowerLevels] = useState(getPowerLevels(room)); @@ -538,7 +550,7 @@ interface IBaseProps { stopUpdating(): void; } -const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit) => { +export const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit) => { const cli = useContext(MatrixClientContext); // check if user can be kicked/disinvited @@ -566,7 +578,7 @@ const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit { // Return true if the target member is not banned and we have sufficient PL to ban them - const myMember = child.getMember(cli.credentials.userId); + const myMember = child.getMember(cli.credentials.userId || ""); const theirMember = child.getMember(member.userId); return ( myMember && @@ -648,7 +660,7 @@ const RedactMessagesButton: React.FC = ({ member }) => { ); }; -const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit) => { +export const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit) => { const cli = useContext(MatrixClientContext); const isBanned = member.membership === "ban"; @@ -674,7 +686,7 @@ const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit { // Return true if the target member is banned and we have sufficient PL to unban - const myMember = child.getMember(cli.credentials.userId); + const myMember = child.getMember(cli.credentials.userId || ""); const theirMember = child.getMember(member.userId); return ( myMember && @@ -686,7 +698,7 @@ const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit { // Return true if the target member isn't banned and we have sufficient PL to ban - const myMember = child.getMember(cli.credentials.userId); + const myMember = child.getMember(cli.credentials.userId || ""); const theirMember = child.getMember(member.userId); return ( myMember && @@ -835,7 +847,7 @@ const MuteToggleButton: React.FC = ({ member, room, powerLevels, ); }; -const RoomAdminToolsContainer: React.FC = ({ +export const RoomAdminToolsContainer: React.FC = ({ room, children, member, @@ -855,7 +867,7 @@ const RoomAdminToolsContainer: React.FC = ({ // if these do not exist in the event then they should default to 50 as per the spec const { ban: banPowerLevel = 50, kick: kickPowerLevel = 50, redact: redactPowerLevel = 50 } = powerLevels; - const me = room.getMember(cli.getUserId()); + const me = room.getMember(cli.getUserId() || ""); if (!me) { // we aren't in the room, so return no admin tooling return
; @@ -879,7 +891,7 @@ const RoomAdminToolsContainer: React.FC = ({ ); } - if (!isMe && canAffectUser && me.powerLevel >= editPowerLevel && !room.isSpaceRoom()) { + if (!isMe && canAffectUser && me.powerLevel >= Number(editPowerLevel) && !room.isSpaceRoom()) { muteButton = ( { setSelectedPowerLevel(powerLevel); - const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { - return cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( + const applyPowerChange = ( + roomId: string, + target: string, + powerLevel: number, + powerLevelEvent: MatrixEvent, + ) => { + return cli.setPowerLevel(roomId, target, powerLevel, powerLevelEvent).then( function () { // NO-OP; rely on the m.room.member event coming down else we could // get out of sync if we force setState here! @@ -1046,7 +1063,7 @@ const PowerLevelEditor: React.FC<{ if (!powerLevelEvent) return; const myUserId = cli.getUserId(); - const myPower = powerLevelEvent.getContent().users[myUserId]; + const myPower = powerLevelEvent.getContent().users[myUserId || ""]; if (myPower && parseInt(myPower) <= powerLevel && myUserId !== target) { const { finished } = Modal.createDialog(QuestionDialog, { title: _t("Warning!"), @@ -1085,7 +1102,7 @@ const PowerLevelEditor: React.FC<{ return (
{ const cli = useContext(MatrixClientContext); // undefined means yet to be loaded, null means failed to load, otherwise list of devices - const [devices, setDevices] = useState(undefined); + const [devices, setDevices] = useState(undefined); // Download device lists useEffect(() => { setDevices(undefined); @@ -1116,8 +1133,8 @@ export const useDevices = (userId: string) => { return; } - disambiguateDevices(devices); - setDevices(devices); + disambiguateDevices(devices as IDevice[]); + setDevices(devices as IDevice[]); } catch (err) { setDevices(null); } @@ -1136,17 +1153,17 @@ export const useDevices = (userId: string) => { const updateDevices = async () => { const newDevices = cli.getStoredDevicesForUser(userId); if (cancel) return; - setDevices(newDevices); + setDevices(newDevices as IDevice[]); }; - const onDevicesUpdated = (users) => { + const onDevicesUpdated = (users: string[]) => { if (!users.includes(userId)) return; updateDevices(); }; - const onDeviceVerificationChanged = (_userId, device) => { + const onDeviceVerificationChanged = (_userId: string, deviceId: string) => { if (_userId !== userId) return; updateDevices(); }; - const onUserTrustStatusChanged = (_userId, trustStatus) => { + const onUserTrustStatusChanged = (_userId: string, trustLevel: UserTrustLevel) => { if (_userId !== userId) return; updateDevices(); }; @@ -1229,9 +1246,11 @@ const BasicUserInfo: React.FC<{ logger.error("Failed to deactivate user"); logger.error(err); + const description = err instanceof Error ? err.message : _t("Operation failed"); + Modal.createDialog(ErrorDialog, { title: _t("Failed to deactivate user"), - description: err && err.message ? err.message : _t("Operation failed"), + description, }); } }, [cli, member.userId]); @@ -1317,12 +1336,12 @@ const BasicUserInfo: React.FC<{ const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli); const userTrust = cryptoEnabled && cli.checkUserTrust(member.userId); - const userVerified = cryptoEnabled && userTrust.isCrossSigningVerified(); + const userVerified = cryptoEnabled && userTrust && userTrust.isCrossSigningVerified(); const isMe = member.userId === cli.getUserId(); const canVerify = cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe && devices && devices.length > 0; - const setUpdating = (updating) => { + const setUpdating: SetUpdating = (updating) => { setPendingUpdateCount((count) => count + (updating ? 1 : -1)); }; const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify, setUpdating); @@ -1408,9 +1427,9 @@ const BasicUserInfo: React.FC<{ export type Member = User | RoomMember; -const UserInfoHeader: React.FC<{ +export const UserInfoHeader: React.FC<{ member: Member; - e2eStatus: E2EStatus; + e2eStatus?: E2EStatus; roomId?: string; }> = ({ member, e2eStatus, roomId }) => { const cli = useContext(MatrixClientContext); @@ -1427,9 +1446,11 @@ const UserInfoHeader: React.FC<{ name: (member as RoomMember).name || (member as User).displayName, }; - Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true); + Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true); }, [member]); + const avatarUrl = (member as User).avatarUrl; + const avatarElement = (
@@ -1442,7 +1463,7 @@ const UserInfoHeader: React.FC<{ resizeMethod="scale" fallbackUserId={member.userId} onClick={onMemberAvatarClick} - urls={(member as User).avatarUrl ? [(member as User).avatarUrl] : undefined} + urls={avatarUrl ? [avatarUrl] : undefined} />
@@ -1475,10 +1496,7 @@ const UserInfoHeader: React.FC<{ ); } - let e2eIcon; - if (e2eStatus) { - e2eIcon = ; - } + const e2eIcon = e2eStatus ? : null; const displayName = (member as RoomMember).rawDisplayName; return ( @@ -1496,7 +1514,7 @@ const UserInfoHeader: React.FC<{
- {UserIdentifierCustomisations.getDisplayUserIdentifier(member.userId, { + {UserIdentifierCustomisations.getDisplayUserIdentifier?.(member.userId, { roomId, withDisplayName: true, })} @@ -1533,7 +1551,7 @@ const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPha const classes = ["mx_UserInfo"]; - let cardState: IRightPanelCardState; + let cardState: IRightPanelCardState = {}; // We have no previousPhase for when viewing a UserInfo without a Room at this time if (room && phase === RightPanelPhases.EncryptionPanel) { cardState = { member }; @@ -1551,10 +1569,10 @@ const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPha case RightPanelPhases.SpaceMemberInfo: content = ( ); break; @@ -1565,7 +1583,7 @@ const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPha {...(props as React.ComponentProps)} member={member as User | RoomMember} onClose={onEncryptionPanelClose} - isRoomEncrypted={isRoomEncrypted} + isRoomEncrypted={Boolean(isRoomEncrypted)} /> ); break; @@ -1582,7 +1600,7 @@ const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPha let scopeHeader; if (room?.isSpaceRoom()) { scopeHeader = ( -
+
diff --git a/test/components/views/right_panel/UserInfo-test.tsx b/test/components/views/right_panel/UserInfo-test.tsx index 2998efe5b3..49a5b134bc 100644 --- a/test/components/views/right_panel/UserInfo-test.tsx +++ b/test/components/views/right_panel/UserInfo-test.tsx @@ -15,21 +15,42 @@ limitations under the License. */ import React from "react"; -// eslint-disable-next-line deprecate/import -import { mount } from "enzyme"; +import { fireEvent, render, screen, waitFor, cleanup } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { mocked } from "jest-mock"; -import { act } from "react-dom/test-utils"; -import { Room, User, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { Room, User, MatrixClient, RoomMember, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; import { Phase, VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { DeviceTrustLevel, UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; -import UserInfo from "../../../../src/components/views/right_panel/UserInfo"; +import UserInfo, { + BanToggleButton, + DeviceItem, + disambiguateDevices, + getPowerLevels, + IDevice, + 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 VerificationPanel from "../../../../src/components/views/right_panel/VerificationPanel"; -import EncryptionInfo from "../../../../src/components/views/right_panel/EncryptionInfo"; +import MultiInviter from "../../../../src/utils/MultiInviter"; +import * as mockVerification from "../../../../src/verification"; +import Modal from "../../../../src/Modal"; +import { E2EStatus } from "../../../../src/utils/ShieldUtils"; -const findByTestId = (component, id) => component.find(`[data-test-id="${id}"]`); +jest.mock("../../../../src/dispatcher/dispatcher"); + +jest.mock("../../../../src/customisations/UserIdentifier", () => { + return { + getDisplayUserIdentifier: jest.fn().mockReturnValue("customUserIdentifier"), + }; +}); jest.mock("../../../../src/utils/DMRoomMap", () => { const mock = { @@ -43,33 +64,62 @@ jest.mock("../../../../src/utils/DMRoomMap", () => { }; }); -describe("", () => { - const defaultUserId = "@test:test"; - const defaultUser = new User(defaultUserId); - - const mockClient = mocked({ - getUser: jest.fn(), - isGuest: jest.fn().mockReturnValue(false), - isUserIgnored: jest.fn(), - isCryptoEnabled: jest.fn(), - getUserId: jest.fn(), +const mockRoom = mocked({ + roomId: "!fkfk", + 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(), + currentState: { + getStateEvents: jest.fn(), on: 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(), - }, - } as unknown as MatrixClient); + }, + getEventReadUpTo: jest.fn(), +} as unknown as Room); +const mockClient = mocked({ + getUser: jest.fn(), + isGuest: jest.fn().mockReturnValue(false), + isUserIgnored: jest.fn(), + isCryptoEnabled: jest.fn(), + getUserId: jest.fn(), + on: 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(), + }, + checkDeviceTrust: jest.fn(), + checkUserTrust: jest.fn(), + getRoom: jest.fn(), + credentials: {}, + setPowerLevel: jest.fn(), +} as unknown as MatrixClient); + +const defaultUserId = "@test:test"; +const defaultUser = new User(defaultUserId); + +beforeEach(() => { + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); +}); + +afterEach(() => { + mockClient.getUser.mockClear().mockReturnValue({} as unknown as User); +}); + +describe("", () => { const verificationRequest = { pending: true, on: jest.fn(), phase: Phase.Ready, channel: { transactionId: 1 }, otherPartySupportsMethod: jest.fn(), + off: jest.fn(), } as unknown as VerificationRequest; const defaultProps = { @@ -79,111 +129,859 @@ describe("", () => { onClose: jest.fn(), }; - const getComponent = (props = {}) => - mount(, { - wrappingComponent: MatrixClientContext.Provider, - wrappingComponentProps: { value: mockClient }, + const renderComponent = (props = {}) => { + const Wrapper = (wrapperProps = {}) => { + return ; + }; + + return render(, { + wrapper: Wrapper, }); + }; - beforeAll(() => { - jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); - }); + it("closes on close button click", async () => { + renderComponent(); - beforeEach(() => { - mockClient.getUser.mockClear().mockReturnValue({} as unknown as User); - }); + await userEvent.click(screen.getByTestId("base-card-close-button")); - it("closes on close button click", () => { - const onClose = jest.fn(); - const component = getComponent({ onClose }); - act(() => { - findByTestId(component, "base-card-close-button").at(0).simulate("click"); - }); - - expect(onClose).toHaveBeenCalled(); + expect(defaultProps.onClose).toHaveBeenCalled(); }); describe("without a room", () => { it("does not render space header", () => { - const component = getComponent(); - expect(findByTestId(component, "space-header").length).toBeFalsy(); + renderComponent(); + expect(screen.queryByTestId("space-header")).not.toBeInTheDocument(); }); it("renders user info", () => { - const component = getComponent(); - expect(component.find("BasicUserInfo").length).toBeTruthy(); + renderComponent(); + expect(screen.getByRole("heading", { name: defaultUserId })).toBeInTheDocument(); }); it("renders encryption info panel without pending verification", () => { - const phase = RightPanelPhases.EncryptionPanel; - const component = getComponent({ phase }); - - expect(component.find(EncryptionInfo).length).toBeTruthy(); + renderComponent({ phase: RightPanelPhases.EncryptionPanel }); + expect(screen.getByRole("heading", { name: /encryption/i })).toBeInTheDocument(); }); it("renders encryption verification panel with pending verification", () => { - const phase = RightPanelPhases.EncryptionPanel; - const component = getComponent({ phase, verificationRequest }); + renderComponent({ phase: RightPanelPhases.EncryptionPanel, verificationRequest }); - expect(component.find(EncryptionInfo).length).toBeFalsy(); - expect(component.find(VerificationPanel).length).toBeTruthy(); + 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 close button correctly when encryption panel with a pending verification request", () => { - const phase = RightPanelPhases.EncryptionPanel; - const component = getComponent({ phase, verificationRequest }); - - expect(findByTestId(component, "base-card-close-button").at(0).props().title).toEqual("Cancel"); + renderComponent({ phase: RightPanelPhases.EncryptionPanel, verificationRequest }); + expect(screen.getByTestId("base-card-close-button")).toHaveAttribute("title", "Cancel"); }); }); describe("with a room", () => { - const room = { - roomId: "!fkfk", - 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(), - currentState: { - getStateEvents: jest.fn(), - on: jest.fn(), - }, - } as unknown as Room; - it("renders user info", () => { - const component = getComponent(); - expect(component.find("BasicUserInfo").length).toBeTruthy(); + renderComponent({ room: mockRoom }); + expect(screen.getByRole("heading", { name: defaultUserId })).toBeInTheDocument(); }); it("does not render space header when room is not a space room", () => { - const component = getComponent({ room }); - expect(findByTestId(component, "space-header").length).toBeFalsy(); + renderComponent({ room: mockRoom }); + expect(screen.queryByTestId("space-header")).not.toBeInTheDocument(); }); it("renders space header when room is a space room", () => { const spaceRoom = { - ...room, + ...mockRoom, isSpaceRoom: jest.fn().mockReturnValue(true), }; - const component = getComponent({ room: spaceRoom }); - expect(findByTestId(component, "space-header").length).toBeTruthy(); + renderComponent({ room: spaceRoom }); + expect(screen.getByTestId("space-header")).toBeInTheDocument(); }); it("renders encryption info panel without pending verification", () => { - const phase = RightPanelPhases.EncryptionPanel; - const component = getComponent({ phase, room }); - - expect(component.find(EncryptionInfo).length).toBeTruthy(); + renderComponent({ phase: RightPanelPhases.EncryptionPanel, room: mockRoom }); + expect(screen.getByRole("heading", { name: /encryption/i })).toBeInTheDocument(); }); it("renders encryption verification panel with pending verification", () => { - const phase = RightPanelPhases.EncryptionPanel; - const component = getComponent({ phase, verificationRequest, room }); + renderComponent({ phase: RightPanelPhases.EncryptionPanel, verificationRequest, room: mockRoom }); - expect(component.find(EncryptionInfo).length).toBeFalsy(); - expect(component.find(VerificationPanel).length).toBeTruthy(); + 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(); }); }); }); + +describe("", () => { + const defaultMember = new RoomMember(mockRoom.roomId, defaultUserId); + + const defaultProps = { + member: defaultMember, + roomId: mockRoom.roomId, + }; + + const renderComponent = (props = {}) => { + const Wrapper = (wrapperProps = {}) => { + return ; + }; + + return render(, { + 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", { name: defaultUserId }); + + expect(header.getElementsByClassName("mx_E2EIcon")).toHaveLength(1); + }); + + it("renders custom user identifiers in the header", () => { + renderComponent(); + + expect(screen.getByText("customUserIdentifier")).toBeInTheDocument(); + }); +}); + +describe("", () => { + const device: IDevice = { deviceId: "deviceId", getDisplayName: () => "deviceName" }; + const defaultProps = { + userId: defaultUserId, + device, + }; + + const renderComponent = (props = {}) => { + const Wrapper = (wrapperProps = {}) => { + return ; + }; + + return render(, { + wrapper: Wrapper, + }); + }; + + const setMockUserTrust = (isVerified = false) => { + mockClient.checkUserTrust.mockReturnValue({ isVerified: () => isVerified } as UserTrustLevel); + }; + const setMockDeviceTrust = (isVerified = false, isCrossSigningVerified = false) => { + mockClient.checkDeviceTrust.mockReturnValue({ + isVerified: () => isVerified, + isCrossSigningVerified: () => isCrossSigningVerified, + } as DeviceTrustLevel); + }; + + const mockVerifyDevice = jest.spyOn(mockVerification, "verifyDevice"); + + beforeEach(() => { + setMockUserTrust(); + setMockDeviceTrust(); + }); + + afterEach(() => { + mockClient.checkDeviceTrust.mockReset(); + mockClient.checkUserTrust.mockReset(); + mockVerifyDevice.mockClear(); + }); + + afterAll(() => { + mockVerifyDevice.mockRestore(); + }); + + it("with unverified user and device, displays button without a label", () => { + renderComponent(); + + expect(screen.getByRole("button", { name: device.getDisplayName() })).toBeInTheDocument; + expect(screen.queryByText(/trusted/i)).not.toBeInTheDocument(); + }); + + it("with verified user only, displays button with a 'Not trusted' label", () => { + setMockUserTrust(true); + renderComponent(); + + expect(screen.getByRole("button", { name: `${device.getDisplayName()} Not trusted` })).toBeInTheDocument; + }); + + it("with verified device only, displays no button without a label", () => { + setMockDeviceTrust(true); + renderComponent(); + + expect(screen.getByText(device.getDisplayName())).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", () => { + mockClient.getUserId.mockReturnValueOnce(defaultUserId); + renderComponent(); + + // set trust to be false for isVerified, true for isCrossSigningVerified + setMockDeviceTrust(false, true); + + // expect to see no button in this case + expect(screen.queryByRole("button")).not.toBeInTheDocument; + expect(screen.getByText(device.getDisplayName())).toBeInTheDocument(); + }); + + it("with verified user and device, displays no button and a 'Trusted' label", () => { + setMockUserTrust(true); + setMockDeviceTrust(true); + renderComponent(); + + expect(screen.queryByRole("button")).not.toBeInTheDocument; + expect(screen.getByText(device.getDisplayName())).toBeInTheDocument(); + expect(screen.getByText("Trusted")).toBeInTheDocument(); + }); + + it("does not call verifyDevice if client.getUser returns null", async () => { + mockClient.getUser.mockReturnValueOnce(null); + renderComponent(); + + const button = screen.getByRole("button", { name: device.getDisplayName() }); + 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(); + + const button = screen.getByRole("button", { name: device.getDisplayName() }); + expect(button).toBeInTheDocument; + await userEvent.click(button); + + expect(mockVerifyDevice).toHaveBeenCalledTimes(1); + expect(mockVerifyDevice).toHaveBeenCalledWith(defaultUser, device); + }); +}); + +describe("", () => { + const member = new RoomMember(mockRoom.roomId, defaultUserId); + const defaultProps = { member, isIgnored: false, canInvite: false, isSpace: false }; + + const renderComponent = (props = {}) => { + const Wrapper = (wrapperProps = {}) => { + return ; + }; + + return render(, { + wrapper: Wrapper, + }); + }; + + const inviteSpy = jest.spyOn(MultiInviter.prototype, "invite"); + + beforeEach(() => { + inviteSpy.mockReset(); + }); + + 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.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("calling .invite with a null roomId still calls .invite and shows default error message", async () => { + inviteSpy.mockRejectedValue({ this: "could be anything" }); + + // render the component and click the button + renderComponent({ canInvite: true, member: { ...member, roomId: null } }); + const inviteButton = screen.getByRole("button", { name: /invite/i }); + expect(inviteButton).toBeInTheDocument(); + await userEvent.click(inviteButton); + + expect(inviteSpy).toHaveBeenCalledTimes(1); + + // check that the default test error message is displayed + await waitFor(() => { + expect(screen.getByText(/operation failed/i)).toBeInTheDocument(); + }); + }); +}); + +describe("", () => { + const defaultMember = new RoomMember(mockRoom.roomId, defaultUserId); + + const defaultProps = { + user: defaultMember, + room: mockRoom, + roomPermissions: { + modifyLevelMax: 100, + canEdit: false, + canInvite: false, + }, + }; + + const renderComponent = (props = {}) => { + const Wrapper = (wrapperProps = {}) => { + return ; + }; + + return render(, { + 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.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, + powerLevelEvent, + ); + }); +}); + +describe("", () => { + const defaultMember = new RoomMember(mockRoom.roomId, defaultUserId); + const memberWithInviteMembership = { ...defaultMember, membership: "invite" }; + const memberWithJoinMembership = { ...defaultMember, membership: "join" }; + + const defaultProps = { room: mockRoom, member: defaultMember, startUpdating: jest.fn(), stopUpdating: jest.fn() }; + + const renderComponent = (props = {}) => { + const Wrapper = (wrapperProps = {}) => { + return ; + }; + + return render(, { + 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({ 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) }), + undefined, + ); + + // 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: "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(null); + expect(callback(mockRoom)).toBe(true); + }); +}); + +describe("", () => { + const defaultMember = new RoomMember(mockRoom.roomId, defaultUserId); + const memberWithBanMembership = { ...defaultMember, membership: "ban" }; + + const defaultProps = { room: mockRoom, member: defaultMember, startUpdating: jest.fn(), stopUpdating: jest.fn() }; + + const renderComponent = (props = {}) => { + const Wrapper = (wrapperProps = {}) => { + return ; + }; + + return render(, { + 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(); + 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) }), + undefined, + ); + + // 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(null); + 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({ 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) }), + undefined, + ); + + // 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: "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(null); + expect(callback(mockRoom)).toBe(true); + }); +}); + +describe("", () => { + const defaultMember = new RoomMember(mockRoom.roomId, defaultUserId); + defaultMember.membership = "invite"; + + const defaultProps = { + room: mockRoom, + member: defaultMember, + startUpdating: jest.fn(), + stopUpdating: jest.fn(), + powerLevels: {}, + }; + + const renderComponent = (props = {}) => { + const Wrapper = (wrapperProps = {}) => { + return ; + }; + + return render(, { + wrapper: Wrapper, + }); + }; + + it("returns a single empty div if room.getMember is falsy", () => { + const { asFragment } = renderComponent(); + expect(asFragment()).toMatchInlineSnapshot(` + +
+ + `); + }); + + 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(` + +
+ + `); + }); + + 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: "join" }; + + renderComponent({ + member: defaultMemberWithPowerLevelAndJoinMembership, + powerLevels: { events: { "m.room.power_levels": 1 } }, + }); + + expect(screen.getByText(/mute/i)).toBeInTheDocument(); + }); +}); + +describe("disambiguateDevices", () => { + it("does not add ambiguous key to unique names", () => { + const initialDevices = [ + { deviceId: "id1", getDisplayName: () => "name1" }, + { deviceId: "id2", getDisplayName: () => "name2" }, + { deviceId: "id3", getDisplayName: () => "name3" }, + ]; + 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", getDisplayName: () => "name3" }, + { deviceId: "id4", getDisplayName: () => "name4" }, + { deviceId: "id6", getDisplayName: () => "name6" }, + ]; + const nonUniqueNameDevices = [ + { deviceId: "id1", getDisplayName: () => "nonUnique" }, + { deviceId: "id2", getDisplayName: () => "nonUnique" }, + { deviceId: "id5", getDisplayName: () => "nonUnique" }, + ]; + 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(mockRoom.roomId, 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({}); + }); +});