From b75e2cee1dbc01ce69bb6db34a263b3bae9ddcc5 Mon Sep 17 00:00:00 2001 From: Thomas Belin Date: Wed, 11 Oct 2023 13:47:11 +0200 Subject: [PATCH] test: Add tests to member message rendering (#15969) --- .../Message/MemberMessage.test.tsx | 307 +++++++++++++++--- .../MessagesList/Message/MemberMessage.tsx | 244 +++++++------- .../ConnectedMessage.tsx | 6 +- .../Message/MemberMessage/MessageContent.tsx | 26 ++ src/script/entity/message/MemberMessage.ts | 20 -- 5 files changed, 413 insertions(+), 190 deletions(-) rename src/script/components/MessagesList/Message/{memberMessage => MemberMessage}/ConnectedMessage.tsx (96%) create mode 100644 src/script/components/MessagesList/Message/MemberMessage/MessageContent.tsx diff --git a/src/script/components/MessagesList/Message/MemberMessage.test.tsx b/src/script/components/MessagesList/Message/MemberMessage.test.tsx index d9dd926670a..51e2b2fbd46 100644 --- a/src/script/components/MessagesList/Message/MemberMessage.test.tsx +++ b/src/script/components/MessagesList/Message/MemberMessage.test.tsx @@ -18,63 +18,284 @@ */ import {render} from '@testing-library/react'; -import ko from 'knockout'; +import {CONVERSATION_EVENT} from '@wireapp/api-client/lib/event'; +import {randomInt} from 'crypto'; + +import en from 'I18n/en-US.json'; import {MemberMessage as MemberMessageEntity} from 'src/script/entity/message/MemberMessage'; import {User} from 'src/script/entity/User'; +import {SystemMessageType} from 'src/script/message/SystemMessageType'; +import {generateUser} from 'test/helper/UserGenerator'; +import {setStrings} from 'Util/LocalizerUtil'; import {MemberMessage} from './MemberMessage'; -const createMemberMessage = (partialMemberMessage: Partial) => { - const memberMessage: Partial = { - hasUsers: ko.pureComputed(() => false), - isGroupCreation: () => false, - isMemberChange: () => false, - isMemberJoin: () => false, - isMemberLeave: () => false, - isMemberRemoval: () => false, - showLargeAvatar: () => false, - showNamedCreation: ko.pureComputed(() => true), - timestamp: ko.observable(Date.now()), - ...partialMemberMessage, - }; - return memberMessage as MemberMessageEntity; +setStrings({en}); + +const config = MemberMessageEntity.CONFIG; + +function createMemberMessage({systemType, type}: {systemType?: SystemMessageType; type?: string}, users?: User[]) { + const message = new MemberMessageEntity(); + if (systemType) { + message.memberMessageType = systemType; + } + if (type) { + message.type = type; + } + const actor = generateUser(); + message.user(actor); + if (users) { + message.userIds(users.map(user => user.qualifiedId)); + message.userEntities(users); + } else { + message.userIds([actor.qualifiedId]); + message.userEntities([actor]); + } + message.name('message'); + + return message; +} + +const baseProps = { + hasReadReceiptsTurnedOn: false, + isSelfTemporaryGuest: false, + onClickCancelRequest: jest.fn(), + onClickInvitePeople: jest.fn(), + onClickParticipants: jest.fn(), + shouldShowInvitePeople: false, + conversationName: 'group 1', }; describe('MemberMessage', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + it('shows connected message', async () => { const props = { - hasReadReceiptsTurnedOn: false, - isSelfTemporaryGuest: false, - message: createMemberMessage({ - otherUser: ko.pureComputed(() => new User('id')), - showLargeAvatar: () => true, - }), - onClickCancelRequest: () => {}, - onClickInvitePeople: () => {}, - onClickParticipants: () => {}, - shouldShowInvitePeople: false, - conversationName: 'group 1', + ...baseProps, + message: createMemberMessage({systemType: SystemMessageType.CONNECTION_ACCEPTED}, [new User('id')]), }; - const {queryByTestId} = render(); - expect(queryByTestId('element-connected-message')).not.toBeNull(); + const {getByTestId} = render(); + expect(getByTestId('element-connected-message')).not.toBeNull(); }); - it('shows conversation title', async () => { - const props = { - hasReadReceiptsTurnedOn: false, - isSelfTemporaryGuest: false, - message: createMemberMessage({ - otherUser: ko.pureComputed(() => new User('id')), - }), - onClickCancelRequest: () => {}, - onClickInvitePeople: () => {}, - onClickParticipants: () => {}, - shouldShowInvitePeople: false, - conversationName: 'group 1', - }; - const {getByTestId} = render(); - expect(getByTestId('conversation-name').textContent).toBe(props.conversationName); + describe('CONVERSATION_CREATE', () => { + it('displays participants of a newly created conversation', () => { + const nbUsers = randomInt(1, 10); + const users = Array.from({length: nbUsers}, () => generateUser()); + const message = createMemberMessage({systemType: SystemMessageType.CONVERSATION_CREATE}, users); + const props = { + ...baseProps, + message, + }; + + const {getByText} = render(); + users.forEach(user => { + expect(getByText(user.name())).not.toBeNull(); + }); + }); + + it('displays a showMore when there are more than 17 users', () => { + const nbExtraUsers = randomInt(1, 10); + const nbUsers = config.MAX_USERS_VISIBLE + nbExtraUsers; + + const users = Array.from({length: nbUsers}, () => generateUser()); + const message = createMemberMessage({systemType: SystemMessageType.CONVERSATION_CREATE}, users); + const props = { + ...baseProps, + message, + }; + + const {getByText} = render(); + const showMoreButton = getByText(`${nbUsers - config.REDUCED_USERS_COUNT} more`); + showMoreButton.click(); + + expect(props.onClickParticipants).toHaveBeenCalledTimes(1); + }); + + it('displays all team members', () => { + const nbExtraUsers = randomInt(1, 10); + const nbTeamUsers = config.MAX_WHOLE_TEAM_USERS_VISIBLE + nbExtraUsers; + + const teamUsers = Array.from({length: nbTeamUsers}, () => generateUser()); + const message = createMemberMessage({systemType: SystemMessageType.CONVERSATION_CREATE}, teamUsers); + message.allTeamMembers = teamUsers; + const props = { + ...baseProps, + message, + }; + + const {getByText} = render(); + const showMoreButton = getByText(`all team members`); + showMoreButton.click(); + + expect(props.onClickParticipants).toHaveBeenCalledTimes(1); + }); + + it('displays all team members and one guest message', () => { + const nbExtraUsers = randomInt(1, 10); + const nbTeamUsers = config.MAX_WHOLE_TEAM_USERS_VISIBLE + nbExtraUsers; + + const teamUsers = Array.from({length: nbTeamUsers}, () => generateUser()); + const guest = generateUser(); + guest.isGuest(true); + const message = createMemberMessage({systemType: SystemMessageType.CONVERSATION_CREATE}, [...teamUsers, guest]); + message.allTeamMembers = teamUsers; + const props = { + ...baseProps, + message, + }; + + const {getByText} = render(); + expect(getByText(`all team members and one guest`)).not.toBeNull(); + }); + + it('displays all team members and multiple guests message', () => { + const nbGuests = randomInt(2, 10); + const nbTeamUsers = config.MAX_WHOLE_TEAM_USERS_VISIBLE; + + const teamUsers = Array.from({length: nbTeamUsers}, () => generateUser()); + const guests = Array.from({length: nbGuests}, () => { + const guest = generateUser(); + guest.isGuest(true); + return guest; + }); + const message = createMemberMessage({systemType: SystemMessageType.CONVERSATION_CREATE}, [ + ...teamUsers, + ...guests, + ]); + message.allTeamMembers = teamUsers; + const props = { + ...baseProps, + message, + }; + + const {getByText} = render(); + expect(getByText(`all team members and ${nbGuests} guests`)).not.toBeNull(); + }); + + it('displays that another user created a conversation', () => { + const nbUsers = randomInt(1, 10); + const users = Array.from({length: nbUsers}, () => generateUser()); + const message = createMemberMessage({systemType: SystemMessageType.CONVERSATION_CREATE}, users); + message.name(''); + message.user().name('Creator'); + const props = { + ...baseProps, + message, + }; + + const {container} = render(); + expect(container.textContent).toContain(`Creator started a conversation with`); + }); + + it('displays that self user created a conversation', () => { + const nbUsers = randomInt(1, 10); + const users = Array.from({length: nbUsers}, () => generateUser()); + const message = createMemberMessage({systemType: SystemMessageType.CONVERSATION_CREATE}, users); + message.name(''); + message.user().isMe = true; + const props = { + ...baseProps, + message, + }; + + const {container} = render(); + expect(container.textContent).toContain(`You started a conversation with`); + }); + }); + + describe('MEMBER_JOIN', () => { + it('displays that self user added new members', () => { + const nbUsers = randomInt(1, 10); + const users = Array.from({length: nbUsers}, () => generateUser()); + const message = createMemberMessage({type: CONVERSATION_EVENT.MEMBER_JOIN}, users); + message.user().isMe = true; + const props = { + ...baseProps, + message, + }; + + const {container} = render(); + expect(container.textContent).toContain(`You added `); + }); + + it('displays that a new members were added by someone', () => { + const nbUsers = randomInt(1, 10); + const users = Array.from({length: nbUsers}, () => generateUser()); + const message = createMemberMessage({type: CONVERSATION_EVENT.MEMBER_JOIN}, users); + const props = { + ...baseProps, + message, + }; + + const {container} = render(); + expect(container.textContent).toContain(`${message.user().name()} added `); + }); + + it('displays that a new members joined the conversation', () => { + const message = createMemberMessage({type: CONVERSATION_EVENT.MEMBER_JOIN}); + const props = { + ...baseProps, + message, + }; + + const {container} = render(); + expect(container.textContent).toContain(`${message.user().name()} joined`); + }); + }); + + describe('MEMBER_LEAVE', () => { + it('displays that self user left the conversation', () => { + const message = createMemberMessage({type: CONVERSATION_EVENT.MEMBER_LEAVE}); + message.user().isMe = true; + const props = { + ...baseProps, + message, + }; + + const {container} = render(); + expect(container.textContent).toContain(`You left`); + }); + + it('displays that a member left the conversation', () => { + const message = createMemberMessage({type: CONVERSATION_EVENT.MEMBER_LEAVE}); + const props = { + ...baseProps, + message, + }; + + const {container} = render(); + expect(container.textContent).toContain(`${message.user().name()} left`); + }); + + it('displays that a member was removed by someone', () => { + const removedUser = generateUser(); + const message = createMemberMessage({type: CONVERSATION_EVENT.MEMBER_LEAVE}, [removedUser]); + const props = { + ...baseProps, + message, + }; + + const {container} = render(); + expect(container.textContent).toContain(`${message.user().name()} removed ${removedUser.name()}`); + }); + + it('displays that many users were removed', () => { + const nbUsers = randomInt(1, 10); + const users = Array.from({length: nbUsers}, () => generateUser()); + const message = createMemberMessage({type: CONVERSATION_EVENT.MEMBER_LEAVE}, users); + message.user().id = ''; + const props = { + ...baseProps, + message, + }; + + const {container} = render(); + expect(container.textContent).toContain(`were removed`); + }); }); }); diff --git a/src/script/components/MessagesList/Message/MemberMessage.tsx b/src/script/components/MessagesList/Message/MemberMessage.tsx index 1d877cba96d..b8e32b5b4e6 100644 --- a/src/script/components/MessagesList/Message/MemberMessage.tsx +++ b/src/script/components/MessagesList/Message/MemberMessage.tsx @@ -22,16 +22,17 @@ import React from 'react'; import {Button, ButtonVariant} from '@wireapp/react-ui-kit'; import {Icon} from 'Components/Icon'; +import {MemberMessage as MemberMessageEntity} from 'src/script/entity/message/MemberMessage'; import {User} from 'src/script/entity/User'; +import {SystemMessageType} from 'src/script/message/SystemMessageType'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; import {t} from 'Util/LocalizerUtil'; -import {ConnectedMessage} from './memberMessage/ConnectedMessage'; +import {ConnectedMessage} from './MemberMessage/ConnectedMessage'; +import {MessageContent} from './MemberMessage/MessageContent'; import {MessageTime} from './MessageTime'; -import {MemberMessage as MemberMessageEntity} from '../../../entity/message/MemberMessage'; - -export interface MemberMessageProps { +interface MemberMessageProps { classifiedDomains?: string[]; hasReadReceiptsTurnedOn: boolean; isSelfTemporaryGuest: boolean; @@ -43,7 +44,7 @@ export interface MemberMessageProps { conversationName: string; } -const MemberMessage: React.FC = ({ +export const MemberMessage: React.FC = ({ message, shouldShowInvitePeople, isSelfTemporaryGuest, @@ -54,28 +55,18 @@ const MemberMessage: React.FC = ({ classifiedDomains, conversationName, }) => { - const { - otherUser, - timestamp, - user, - htmlGroupCreationHeader, - htmlCaption, - highlightedUsers, - showNamedCreation, - hasUsers, - } = useKoSubscribableChildren(message, [ - 'otherUser', - 'timestamp', - 'user', - 'name', - 'htmlGroupCreationHeader', - 'htmlCaption', - 'highlightedUsers', - 'showNamedCreation', - 'hasUsers', - ]); - - const showConnectedMessage = message.showLargeAvatar(); + const {otherUser, timestamp, user, htmlGroupCreationHeader, highlightedUsers, showNamedCreation, hasUsers} = + useKoSubscribableChildren(message, [ + 'otherUser', + 'timestamp', + 'user', + 'name', + 'htmlGroupCreationHeader', + 'highlightedUsers', + 'showNamedCreation', + 'hasUsers', + ]); + const isGroupCreation = message.isGroupCreation(); const isMemberRemoval = message.isMemberRemoval(); const isMemberJoin = message.isMemberJoin(); @@ -89,112 +80,119 @@ const MemberMessage: React.FC = ({ } }; + const isConnectedMessage = [SystemMessageType.CONNECTION_ACCEPTED, SystemMessageType.CONNECTION_REQUEST].includes( + message.memberMessageType, + ); + + if (isConnectedMessage) { + return ( + onClickCancelRequest(message)} + classifiedDomains={classifiedDomains} + /> + ); + } + return ( <> - {showConnectedMessage ? ( - onClickCancelRequest(message)} - classifiedDomains={classifiedDomains} - /> - ) : ( - <> - {showNamedCreation && ( -
-

+

+

+ {conversationName} +

+
+ )} + + {hasUsers && ( +
+
+ {isGroupCreation && } + {isMemberRemoval && } + {isMemberJoin && } +
+ {/* event is being triggered only when clicked on tag with specified class (keyboard accessible by default) */} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} +
+ +
+ {isMemberChange && ( +
+ -

- {conversationName} -

)} - {hasUsers && ( -
- )} - {hasUsers && message.showServicesWarning && ( -

- {t('conversationServicesWarning')} -

- )} +
+ )} - {isGroupCreation && shouldShowInvitePeople && ( -
-

{t('guestRoomConversationHead')}

- - -
- )} + {hasUsers && message.showServicesWarning && ( +

+ {t('conversationServicesWarning')} +

+ )} - {isGroupCreation && isSelfTemporaryGuest && ( -
-

{t('temporaryGuestJoinMessage')}

-

{t('temporaryGuestJoinDescription')}

-
- )} - {isGroupCreation && hasReadReceiptsTurnedOn && ( -
-
- -
-

- {t('conversationCreateReceiptsEnabled')} -

-
- )} - {isMemberLeave && user.isMe && isSelfTemporaryGuest && ( -
-

{t('temporaryGuestLeaveDescription')}

+ {isGroupCreation && shouldShowInvitePeople && ( +
+

{t('guestRoomConversationHead')}

+ + +
+ )} + + {isGroupCreation && isSelfTemporaryGuest && ( +
+

{t('temporaryGuestJoinMessage')}

+

{t('temporaryGuestJoinDescription')}

+
+ )} + + {isGroupCreation && hasReadReceiptsTurnedOn && ( +
+
+ +
+

+ {t('conversationCreateReceiptsEnabled')} +

+
+ )} + + {isMemberLeave && user.isMe && isSelfTemporaryGuest && ( +
+

{t('temporaryGuestLeaveDescription')}

+
+ )} + + {isGroupCreation && ( + <> +
+
+

{t('conversationNewConversation')}

+
+
+
+
- )} - {isGroupCreation && ( - <> -
-
-

{t('conversationNewConversation')}

-
-
-
- -
-

{t('conversationUnverifiedUserWarning')}

-
- - )} +

{t('conversationUnverifiedUserWarning')}

+
)} ); }; - -export {MemberMessage}; diff --git a/src/script/components/MessagesList/Message/memberMessage/ConnectedMessage.tsx b/src/script/components/MessagesList/Message/MemberMessage/ConnectedMessage.tsx similarity index 96% rename from src/script/components/MessagesList/Message/memberMessage/ConnectedMessage.tsx rename to src/script/components/MessagesList/Message/MemberMessage/ConnectedMessage.tsx index 26c75eafb8a..a60b1f1d094 100644 --- a/src/script/components/MessagesList/Message/memberMessage/ConnectedMessage.tsx +++ b/src/script/components/MessagesList/Message/MemberMessage/ConnectedMessage.tsx @@ -29,14 +29,14 @@ import {User} from 'src/script/entity/User'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; import {t} from 'Util/LocalizerUtil'; -export interface ConnectedMessageProps { +interface ConnectedMessageProps { classifiedDomains?: string[]; onClickCancelRequest: () => void; showServicesWarning?: boolean; user: User; } -const ConnectedMessage: React.FC = ({ +export const ConnectedMessage: React.FC = ({ user, onClickCancelRequest, showServicesWarning = false, @@ -105,5 +105,3 @@ const ConnectedMessage: React.FC = ({
); }; - -export {ConnectedMessage}; diff --git a/src/script/components/MessagesList/Message/MemberMessage/MessageContent.tsx b/src/script/components/MessagesList/Message/MemberMessage/MessageContent.tsx new file mode 100644 index 00000000000..07b11530e2c --- /dev/null +++ b/src/script/components/MessagesList/Message/MemberMessage/MessageContent.tsx @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {MemberMessage as MemberMessageEntity} from 'src/script/entity/message/MemberMessage'; +import {useKoSubscribableChildren} from 'Util/ComponentUtil'; + +export function MessageContent({message}: {message: MemberMessageEntity}) { + const {htmlCaption} = useKoSubscribableChildren(message, ['htmlCaption']); + return

; +} diff --git a/src/script/entity/message/MemberMessage.ts b/src/script/entity/message/MemberMessage.ts index a2417a2fb63..ea179712db6 100644 --- a/src/script/entity/message/MemberMessage.ts +++ b/src/script/entity/message/MemberMessage.ts @@ -134,21 +134,6 @@ export class MemberMessage extends SystemMessage { const name = this.senderName(); switch (this.memberMessageType) { - case SystemMessageType.CONNECTION_ACCEPTED: - case SystemMessageType.CONNECTION_REQUEST: { - if (this.otherUser()) { - if (this.otherUser().isBlocked()) { - return t('conversationConnectionBlocked'); - } - - if (this.otherUser().isOutgoingRequest()) { - return ''; - } - } - - return t('conversationConnectionAccepted'); - } - case SystemMessageType.CONVERSATION_CREATE: { if (this.name().length) { const exceedsMaxTeam = this.joinedUserEntities().length > MemberMessage.CONFIG.MAX_WHOLE_TEAM_USERS_VISIBLE; @@ -286,11 +271,6 @@ export class MemberMessage extends SystemMessage { return t('conversationMultipleMembersRemovedMissingLegalHoldConsent', users, replaceLinkLegalHold); }; - readonly showLargeAvatar = (): boolean => { - const largeAvatarTypes = [SystemMessageType.CONNECTION_ACCEPTED, SystemMessageType.CONNECTION_REQUEST]; - return largeAvatarTypes.includes(this.memberMessageType); - }; - private generateNameString(skipAnd = false, declension = Declension.ACCUSATIVE): string { return joinNames(this.visibleUsers(), declension, skipAnd, true); }