Convert enzyme to rtl: UserInfo (#9846)

* converts tests from Enzyme to Rtl
* increases test coverage
pull/28788/head^2
alunturner 2023-01-11 10:46:35 +00:00 committed by GitHub
parent a691e634b0
commit 880a86c8b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 960 additions and 144 deletions

View File

@ -69,7 +69,7 @@ const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
if (onClose) { if (onClose) {
closeButton = ( closeButton = (
<AccessibleButton <AccessibleButton
data-test-id="base-card-close-button" data-testid="base-card-close-button"
className="mx_BaseCard_close" className="mx_BaseCard_close"
onClick={onClose} onClick={onClose}
title={closeLabel || _t("Close")} title={closeLabel || _t("Close")}

View File

@ -29,6 +29,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
@ -84,7 +85,7 @@ export interface IDevice {
getDisplayName(): string; getDisplayName(): string;
} }
const disambiguateDevices = (devices: IDevice[]) => { export const disambiguateDevices = (devices: IDevice[]) => {
const names = Object.create(null); const names = Object.create(null);
for (let i = 0; i < devices.length; i++) { for (let i = 0; i < devices.length; i++) {
const name = devices[i].getDisplayName(); const name = devices[i].getDisplayName();
@ -94,7 +95,7 @@ const disambiguateDevices = (devices: IDevice[]) => {
} }
for (const name in names) { for (const name in names) {
if (names[name].length > 1) { if (names[name].length > 1) {
names[name].forEach((j) => { names[name].forEach((j: number) => {
devices[j].ambiguous = true; devices[j].ambiguous = true;
}); });
} }
@ -149,7 +150,7 @@ function useHasCrossSigningKeys(cli: MatrixClient, member: User, canVerify: bool
}, [cli, member, canVerify]); }, [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 cli = useContext(MatrixClientContext);
const isMe = userId === cli.getUserId(); const isMe = userId === cli.getUserId();
const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId); const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId);
@ -172,7 +173,10 @@ function DeviceItem({ userId, device }: { userId: string; device: IDevice }) {
}); });
const onDeviceClick = () => { const onDeviceClick = () => {
verifyDevice(cli.getUser(userId), device); const user = cli.getUser(userId);
if (user) {
verifyDevice(user, device);
}
}; };
let deviceName; let deviceName;
@ -315,7 +319,7 @@ const MessageButton = ({ member }: { member: RoomMember }) => {
); );
}; };
const UserOptionsSection: React.FC<{ export const UserOptionsSection: React.FC<{
member: RoomMember; member: RoomMember;
isIgnored: boolean; isIgnored: boolean;
canInvite: boolean; canInvite: boolean;
@ -367,7 +371,8 @@ const UserOptionsSection: React.FC<{
dis.dispatch<ViewRoomPayload>({ dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom, action: Action.ViewRoom,
highlighted: true, 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, room_id: member.roomId,
metricsTrigger: undefined, // room doesn't change metricsTrigger: undefined, // room doesn't change
}); });
@ -402,16 +407,18 @@ const UserOptionsSection: React.FC<{
const onInviteUserButton = async (ev: ButtonEvent) => { const onInviteUserButton = async (ev: ButtonEvent) => {
try { try {
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user. // 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(() => { await inviter.invite([member.userId]).then(() => {
if (inviter.getCompletionState(member.userId) !== "invited") { if (inviter.getCompletionState(member.userId) !== "invited") {
throw new Error(inviter.getErrorText(member.userId)); throw new Error(inviter.getErrorText(member.userId));
} }
}); });
} catch (err) { } catch (err) {
const description = err instanceof Error ? err.message : _t("Operation failed");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: _t("Failed to invite"), title: _t("Failed to invite"),
description: err && err.message ? err.message : _t("Operation failed"), description,
}); });
} }
@ -432,10 +439,7 @@ const UserOptionsSection: React.FC<{
</AccessibleButton> </AccessibleButton>
); );
let directMessageButton: JSX.Element; const directMessageButton = isMe ? null : <MessageButton member={member} />;
if (!isMe) {
directMessageButton = <MessageButton member={member} />;
}
return ( return (
<div className="mx_UserInfo_container"> <div className="mx_UserInfo_container">
@ -499,16 +503,24 @@ interface IPowerLevelsContent {
redact?: number; redact?: number;
} }
const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) => { export const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) => {
if (!powerLevelContent || !member) return false; if (!powerLevelContent || !member) return false;
const levelToSend = const levelToSend =
(powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) || (powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) ||
powerLevelContent.events_default; 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; 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) => { export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>(getPowerLevels(room)); const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>(getPowerLevels(room));
@ -538,7 +550,7 @@ interface IBaseProps {
stopUpdating(): void; stopUpdating(): void;
} }
const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => { export const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
// check if user can be kicked/disinvited // check if user can be kicked/disinvited
@ -566,7 +578,7 @@ const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBas
space: room, space: room,
spaceChildFilter: (child: Room) => { spaceChildFilter: (child: Room) => {
// Return true if the target member is not banned and we have sufficient PL to ban them // 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); const theirMember = child.getMember(member.userId);
return ( return (
myMember && myMember &&
@ -648,7 +660,7 @@ const RedactMessagesButton: React.FC<IBaseProps> = ({ member }) => {
); );
}; };
const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => { export const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const isBanned = member.membership === "ban"; const isBanned = member.membership === "ban";
@ -674,7 +686,7 @@ const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBa
spaceChildFilter: isBanned spaceChildFilter: isBanned
? (child: Room) => { ? (child: Room) => {
// Return true if the target member is banned and we have sufficient PL to unban // 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); const theirMember = child.getMember(member.userId);
return ( return (
myMember && myMember &&
@ -686,7 +698,7 @@ const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBa
} }
: (child: Room) => { : (child: Room) => {
// Return true if the target member isn't banned and we have sufficient PL to ban // 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); const theirMember = child.getMember(member.userId);
return ( return (
myMember && myMember &&
@ -835,7 +847,7 @@ const MuteToggleButton: React.FC<IBaseRoomProps> = ({ member, room, powerLevels,
); );
}; };
const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({ export const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
room, room,
children, children,
member, member,
@ -855,7 +867,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
// if these do not exist in the event then they should default to 50 as per the spec // 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 { ban: banPowerLevel = 50, kick: kickPowerLevel = 50, redact: redactPowerLevel = 50 } = powerLevels;
const me = room.getMember(cli.getUserId()); const me = room.getMember(cli.getUserId() || "");
if (!me) { if (!me) {
// we aren't in the room, so return no admin tooling // we aren't in the room, so return no admin tooling
return <div />; return <div />;
@ -879,7 +891,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
<BanToggleButton room={room} member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} /> <BanToggleButton room={room} member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
); );
} }
if (!isMe && canAffectUser && me.powerLevel >= editPowerLevel && !room.isSpaceRoom()) { if (!isMe && canAffectUser && me.powerLevel >= Number(editPowerLevel) && !room.isSpaceRoom()) {
muteButton = ( muteButton = (
<MuteToggleButton <MuteToggleButton
member={member} member={member}
@ -949,7 +961,7 @@ function useRoomPermissions(cli: MatrixClient, room: Room, user: RoomMember): IR
const powerLevels = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent(); const powerLevels = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
if (!powerLevels) return; if (!powerLevels) return;
const me = room.getMember(cli.getUserId()); const me = room.getMember(cli.getUserId() || "");
if (!me) return; if (!me) return;
const them = user; const them = user;
@ -1006,7 +1018,7 @@ const PowerLevelSection: React.FC<{
} }
}; };
const PowerLevelEditor: React.FC<{ export const PowerLevelEditor: React.FC<{
user: RoomMember; user: RoomMember;
room: Room; room: Room;
roomPermissions: IRoomPermissions; roomPermissions: IRoomPermissions;
@ -1022,8 +1034,13 @@ const PowerLevelEditor: React.FC<{
async (powerLevel: number) => { async (powerLevel: number) => {
setSelectedPowerLevel(powerLevel); setSelectedPowerLevel(powerLevel);
const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { const applyPowerChange = (
return cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( roomId: string,
target: string,
powerLevel: number,
powerLevelEvent: MatrixEvent,
) => {
return cli.setPowerLevel(roomId, target, powerLevel, powerLevelEvent).then(
function () { function () {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
@ -1046,7 +1063,7 @@ const PowerLevelEditor: React.FC<{
if (!powerLevelEvent) return; if (!powerLevelEvent) return;
const myUserId = cli.getUserId(); const myUserId = cli.getUserId();
const myPower = powerLevelEvent.getContent().users[myUserId]; const myPower = powerLevelEvent.getContent().users[myUserId || ""];
if (myPower && parseInt(myPower) <= powerLevel && myUserId !== target) { if (myPower && parseInt(myPower) <= powerLevel && myUserId !== target) {
const { finished } = Modal.createDialog(QuestionDialog, { const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("Warning!"), title: _t("Warning!"),
@ -1085,7 +1102,7 @@ const PowerLevelEditor: React.FC<{
return ( return (
<div className="mx_UserInfo_profileField"> <div className="mx_UserInfo_profileField">
<PowerSelector <PowerSelector
label={null} label={undefined}
value={selectedPowerLevel} value={selectedPowerLevel}
maxValue={roomPermissions.modifyLevelMax} maxValue={roomPermissions.modifyLevelMax}
usersDefault={powerLevelUsersDefault} usersDefault={powerLevelUsersDefault}
@ -1099,7 +1116,7 @@ export const useDevices = (userId: string) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
// undefined means yet to be loaded, null means failed to load, otherwise list of devices // 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 | null | IDevice[]>(undefined);
// Download device lists // Download device lists
useEffect(() => { useEffect(() => {
setDevices(undefined); setDevices(undefined);
@ -1116,8 +1133,8 @@ export const useDevices = (userId: string) => {
return; return;
} }
disambiguateDevices(devices); disambiguateDevices(devices as IDevice[]);
setDevices(devices); setDevices(devices as IDevice[]);
} catch (err) { } catch (err) {
setDevices(null); setDevices(null);
} }
@ -1136,17 +1153,17 @@ export const useDevices = (userId: string) => {
const updateDevices = async () => { const updateDevices = async () => {
const newDevices = cli.getStoredDevicesForUser(userId); const newDevices = cli.getStoredDevicesForUser(userId);
if (cancel) return; if (cancel) return;
setDevices(newDevices); setDevices(newDevices as IDevice[]);
}; };
const onDevicesUpdated = (users) => { const onDevicesUpdated = (users: string[]) => {
if (!users.includes(userId)) return; if (!users.includes(userId)) return;
updateDevices(); updateDevices();
}; };
const onDeviceVerificationChanged = (_userId, device) => { const onDeviceVerificationChanged = (_userId: string, deviceId: string) => {
if (_userId !== userId) return; if (_userId !== userId) return;
updateDevices(); updateDevices();
}; };
const onUserTrustStatusChanged = (_userId, trustStatus) => { const onUserTrustStatusChanged = (_userId: string, trustLevel: UserTrustLevel) => {
if (_userId !== userId) return; if (_userId !== userId) return;
updateDevices(); updateDevices();
}; };
@ -1229,9 +1246,11 @@ const BasicUserInfo: React.FC<{
logger.error("Failed to deactivate user"); logger.error("Failed to deactivate user");
logger.error(err); logger.error(err);
const description = err instanceof Error ? err.message : _t("Operation failed");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: _t("Failed to deactivate user"), title: _t("Failed to deactivate user"),
description: err && err.message ? err.message : _t("Operation failed"), description,
}); });
} }
}, [cli, member.userId]); }, [cli, member.userId]);
@ -1317,12 +1336,12 @@ const BasicUserInfo: React.FC<{
const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli); const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli);
const userTrust = cryptoEnabled && cli.checkUserTrust(member.userId); 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 isMe = member.userId === cli.getUserId();
const canVerify = const canVerify =
cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe && devices && devices.length > 0; cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe && devices && devices.length > 0;
const setUpdating = (updating) => { const setUpdating: SetUpdating = (updating) => {
setPendingUpdateCount((count) => count + (updating ? 1 : -1)); setPendingUpdateCount((count) => count + (updating ? 1 : -1));
}; };
const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify, setUpdating); const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify, setUpdating);
@ -1408,9 +1427,9 @@ const BasicUserInfo: React.FC<{
export type Member = User | RoomMember; export type Member = User | RoomMember;
const UserInfoHeader: React.FC<{ export const UserInfoHeader: React.FC<{
member: Member; member: Member;
e2eStatus: E2EStatus; e2eStatus?: E2EStatus;
roomId?: string; roomId?: string;
}> = ({ member, e2eStatus, roomId }) => { }> = ({ member, e2eStatus, roomId }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
@ -1427,9 +1446,11 @@ const UserInfoHeader: React.FC<{
name: (member as RoomMember).name || (member as User).displayName, 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]); }, [member]);
const avatarUrl = (member as User).avatarUrl;
const avatarElement = ( const avatarElement = (
<div className="mx_UserInfo_avatar"> <div className="mx_UserInfo_avatar">
<div className="mx_UserInfo_avatar_transition"> <div className="mx_UserInfo_avatar_transition">
@ -1442,7 +1463,7 @@ const UserInfoHeader: React.FC<{
resizeMethod="scale" resizeMethod="scale"
fallbackUserId={member.userId} fallbackUserId={member.userId}
onClick={onMemberAvatarClick} onClick={onMemberAvatarClick}
urls={(member as User).avatarUrl ? [(member as User).avatarUrl] : undefined} urls={avatarUrl ? [avatarUrl] : undefined}
/> />
</div> </div>
</div> </div>
@ -1475,10 +1496,7 @@ const UserInfoHeader: React.FC<{
); );
} }
let e2eIcon; const e2eIcon = e2eStatus ? <E2EIcon size={18} status={e2eStatus} isUser={true} /> : null;
if (e2eStatus) {
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
}
const displayName = (member as RoomMember).rawDisplayName; const displayName = (member as RoomMember).rawDisplayName;
return ( return (
@ -1496,7 +1514,7 @@ const UserInfoHeader: React.FC<{
</h2> </h2>
</div> </div>
<div className="mx_UserInfo_profile_mxid"> <div className="mx_UserInfo_profile_mxid">
{UserIdentifierCustomisations.getDisplayUserIdentifier(member.userId, { {UserIdentifierCustomisations.getDisplayUserIdentifier?.(member.userId, {
roomId, roomId,
withDisplayName: true, withDisplayName: true,
})} })}
@ -1533,7 +1551,7 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
const classes = ["mx_UserInfo"]; 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 // We have no previousPhase for when viewing a UserInfo without a Room at this time
if (room && phase === RightPanelPhases.EncryptionPanel) { if (room && phase === RightPanelPhases.EncryptionPanel) {
cardState = { member }; cardState = { member };
@ -1551,10 +1569,10 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
case RightPanelPhases.SpaceMemberInfo: case RightPanelPhases.SpaceMemberInfo:
content = ( content = (
<BasicUserInfo <BasicUserInfo
room={room} room={room as Room}
member={member as User} member={member as User}
devices={devices} devices={devices as IDevice[]}
isRoomEncrypted={isRoomEncrypted} isRoomEncrypted={Boolean(isRoomEncrypted)}
/> />
); );
break; break;
@ -1565,7 +1583,7 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
{...(props as React.ComponentProps<typeof EncryptionPanel>)} {...(props as React.ComponentProps<typeof EncryptionPanel>)}
member={member as User | RoomMember} member={member as User | RoomMember}
onClose={onEncryptionPanelClose} onClose={onEncryptionPanelClose}
isRoomEncrypted={isRoomEncrypted} isRoomEncrypted={Boolean(isRoomEncrypted)}
/> />
); );
break; break;
@ -1582,7 +1600,7 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
let scopeHeader; let scopeHeader;
if (room?.isSpaceRoom()) { if (room?.isSpaceRoom()) {
scopeHeader = ( scopeHeader = (
<div data-test-id="space-header" className="mx_RightPanel_scopeHeader"> <div data-testid="space-header" className="mx_RightPanel_scopeHeader">
<RoomAvatar room={room} height={32} width={32} /> <RoomAvatar room={room} height={32} width={32} />
<RoomName room={room} /> <RoomName room={room} />
</div> </div>

File diff suppressed because it is too large Load Diff