From 141b5199ba96ecc4bf0d6afc447b20aa846c03de Mon Sep 17 00:00:00 2001 From: Joseph Baylon Date: Wed, 3 Mar 2021 08:23:32 -0800 Subject: [PATCH] MM-30424 Detox/E2E: Add e2e for MM-T3237, MM-T3238, MM-T3251, MM-T3257, MM-T3258 (#5188) --- .../at_mention_item/at_mention_item.tsx | 1 + .../channel_mention/channel_mention.js | 1 + .../channel_mention_item.js | 3 + .../__snapshots__/avatars.test.tsx.snap | 4 + app/components/avatars/avatars.tsx | 1 + app/components/channel_intro/channel_intro.js | 1 + .../__snapshots__/user_list_test.test.js.snap | 4 + .../user_list_row/user_list_row.js | 2 + app/components/formatted_time.js | 12 +- .../__snapshots__/post_header.test.js.snap | 18 +++ app/components/post_header/post_header.js | 6 + .../post_profile_picture.js | 1 + .../profile_picture/profile_picture.js | 8 +- .../settings/status_label/status_label.js | 1 + .../sidebars/settings/user_info/user_info.js | 1 + .../__snapshots__/user_status.test.js.snap | 4 + app/components/user_status/user_status.js | 1 + .../__snapshots__/edit_profile.test.js.snap | 1 + app/screens/edit_profile/edit_profile.js | 1 + .../notification/notification_icon.tsx | 1 + .../__snapshots__/recent_item.test.js.snap | 1 + app/screens/search/recent_item.js | 1 + .../clock_display/clock_display.android.js | 14 +- .../clock_display/clock_display.ios.js | 3 + .../display_settings.test.js.snap | 4 + .../display_settings/display_settings.js | 4 + .../__snapshots__/user_profile.test.js.snap | 1 + app/screens/user_profile/user_profile.js | 1 + detox/e2e/support/server_api/user.js | 42 ++++- .../e2e/support/ui/component/autocomplete.js | 8 +- .../e2e/support/ui/component/bottom_sheet.js | 4 + detox/e2e/support/ui/component/index.js | 2 + detox/e2e/support/ui/component/post.js | 60 +++++-- detox/e2e/support/ui/component/post_list.js | 16 +- .../support/ui/component/profile_picture.js | 21 +++ detox/e2e/support/ui/component/recent_item.js | 13 +- .../support/ui/component/settings_sidebar.js | 47 ++++++ detox/e2e/support/ui/screen/channel.js | 8 +- .../ui/screen/clock_display_settings.js | 65 ++++++++ .../e2e/support/ui/screen/display_settings.js | 41 +++++ detox/e2e/support/ui/screen/index.js | 4 + detox/e2e/support/ui/screen/long_post.js | 17 +- detox/e2e/support/ui/screen/more_channels.js | 2 +- .../support/ui/screen/more_direct_messages.js | 10 +- detox/e2e/support/ui/screen/permalink.js | 8 +- .../e2e/support/ui/screen/pinned_messages.js | 4 +- .../e2e/support/ui/screen/recent_mentions.js | 4 +- detox/e2e/support/ui/screen/saved_messages.js | 4 +- detox/e2e/support/ui/screen/search.js | 6 +- .../support/ui/screen/search_result_post.js | 17 +- detox/e2e/support/ui/screen/thread.js | 8 +- .../e2e/test/smoke_test/clock_display.e2e.js | 112 +++++++++++++ .../smoke_test/email_notifications.e2e.js | 12 +- detox/e2e/test/smoke_test/search.e2e.js | 148 ++++++++++++++---- detox/e2e/test/smoke_test/user_status.e2e.js | 75 +++++++++ 55 files changed, 767 insertions(+), 92 deletions(-) create mode 100644 detox/e2e/support/ui/component/profile_picture.js create mode 100644 detox/e2e/support/ui/screen/clock_display_settings.js create mode 100644 detox/e2e/support/ui/screen/display_settings.js create mode 100644 detox/e2e/test/smoke_test/clock_display.e2e.js create mode 100644 detox/e2e/test/smoke_test/user_status.e2e.js diff --git a/app/components/autocomplete/at_mention_item/at_mention_item.tsx b/app/components/autocomplete/at_mention_item/at_mention_item.tsx index 9642703da..ec14f8622 100644 --- a/app/components/autocomplete/at_mention_item/at_mention_item.tsx +++ b/app/components/autocomplete/at_mention_item/at_mention_item.tsx @@ -115,6 +115,7 @@ const AtMentionItem = (props: AtMentionItemProps) => { size={24} status={null} showStatus={false} + testID={`${testID}.profile_picture`} /> ); }; diff --git a/app/components/autocomplete/channel_mention_item/channel_mention_item.js b/app/components/autocomplete/channel_mention_item/channel_mention_item.js index b78b7df54..4be107afc 100644 --- a/app/components/autocomplete/channel_mention_item/channel_mention_item.js +++ b/app/components/autocomplete/channel_mention_item/channel_mention_item.js @@ -47,6 +47,7 @@ const ChannelMentionItem = (props) => { isGuest, name, onPress, + testID, theme, type, } = props; @@ -78,6 +79,7 @@ const ChannelMentionItem = (props) => { onPress={completeMention} style={[style.row, margins]} type={'opacity'} + testID={testID} > {'@' + displayName} { style={margins} underlayColor={changeOpacity(theme.buttonBg, 0.08)} type={'native'} + testID={testID} > @@ -49,6 +50,7 @@ exports[`Avatars should match snapshot for overflow 1`] = ` @@ -70,6 +72,7 @@ exports[`Avatars should match snapshot for overflow 1`] = ` @@ -147,6 +150,7 @@ exports[`Avatars should match snapshot for single avatar 1`] = ` diff --git a/app/components/avatars/avatars.tsx b/app/components/avatars/avatars.tsx index b2af1c0d8..57192eae1 100644 --- a/app/components/avatars/avatars.tsx +++ b/app/components/avatars/avatars.tsx @@ -108,6 +108,7 @@ export default class Avatars extends PureComponent { userId={userId} size={ViewTypes.AVATAR_LIST_PICTURE_SIZE} showStatus={false} + testID='avatars.profile_picture' /> ))} diff --git a/app/components/channel_intro/channel_intro.js b/app/components/channel_intro/channel_intro.js index 046e9689a..c401ee08a 100644 --- a/app/components/channel_intro/channel_intro.js +++ b/app/components/channel_intro/channel_intro.js @@ -92,6 +92,7 @@ class ChannelIntro extends PureComponent { iconSize={48} statusBorderWidth={2} statusSize={25} + testID='channel_intro.profile_picture' /> )); diff --git a/app/components/custom_list/user_list_row/__snapshots__/user_list_test.test.js.snap b/app/components/custom_list/user_list_row/__snapshots__/user_list_test.test.js.snap index 64236e85b..e97ef94ec 100644 --- a/app/components/custom_list/user_list_row/__snapshots__/user_list_test.test.js.snap +++ b/app/components/custom_list/user_list_row/__snapshots__/user_list_test.test.js.snap @@ -29,6 +29,7 @@ exports[`UserListRow should match snapshot 1`] = ` @@ -164,6 +165,7 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated @@ -297,6 +299,7 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = ` @@ -443,6 +446,7 @@ exports[`UserListRow should match snapshot for guest user 1`] = ` diff --git a/app/components/custom_list/user_list_row/user_list_row.js b/app/components/custom_list/user_list_row/user_list_row.js index e42c0bde7..3e27e3007 100644 --- a/app/components/custom_list/user_list_row/user_list_row.js +++ b/app/components/custom_list/user_list_row/user_list_row.js @@ -66,6 +66,7 @@ export default class UserListRow extends React.PureComponent { const testID = this.props.testID; const itemTestID = `${testID}.${id}`; const displayUsernameTestID = `${testID}.display_username`; + const profilePictureTestID = `${itemTestID}.profile_picture`; return ( @@ -82,6 +83,7 @@ export default class UserListRow extends React.PureComponent { userId={id} size={32} iconSize={24} + testID={profilePictureTestID} /> { @@ -38,13 +39,20 @@ export default class FormattedTime extends React.PureComponent { }; render() { - const {children, style} = this.props; + const {children, style, testID} = this.props; const formattedTime = this.getFormattedTime(); if (typeof children === 'function') { return children(formattedTime); } - return {formattedTime}; + return ( + + {formattedTime} + + ); } } diff --git a/app/components/post_header/__snapshots__/post_header.test.js.snap b/app/components/post_header/__snapshots__/post_header.test.js.snap index 0fa554399..37ee31856 100644 --- a/app/components/post_header/__snapshots__/post_header.test.js.snap +++ b/app/components/post_header/__snapshots__/post_header.test.js.snap @@ -47,6 +47,7 @@ exports[`PostHeader should match snapshot when just a base post 1`] = ` "paddingVertical": 2, } } + testID="post_header.display_name" > John Smith @@ -62,6 +63,7 @@ exports[`PostHeader should match snapshot when just a base post 1`] = ` "opacity": 0.5, } } + testID="post_header.date_time" timeZone="" value={0} /> @@ -119,6 +121,7 @@ exports[`PostHeader should match snapshot when just a base post in landscape mod "paddingVertical": 2, } } + testID="post_header.display_name" > John Smith @@ -134,6 +137,7 @@ exports[`PostHeader should match snapshot when just a base post in landscape mod "opacity": 0.5, } } + testID="post_header.date_time" timeZone="" value={0} /> @@ -187,6 +191,7 @@ exports[`PostHeader should match snapshot when post is autoresponder 1`] = ` "paddingVertical": 2, } } + testID="post_header.display_name" > John Smith @@ -243,6 +248,7 @@ exports[`PostHeader should match snapshot when post is autoresponder 1`] = ` "opacity": 0.5, } } + testID="post_header.date_time" timeZone="" value={0} /> @@ -295,6 +301,7 @@ exports[`PostHeader should match snapshot when post is from system message 1`] = }, ] } + testID="post_header.display_name" /> @@ -363,6 +371,7 @@ exports[`PostHeader should match snapshot when post is same thread, so dont disp "paddingVertical": 2, } } + testID="post_header.display_name" > John Smith @@ -378,6 +387,7 @@ exports[`PostHeader should match snapshot when post is same thread, so dont disp "opacity": 0.5, } } + testID="post_header.date_time" timeZone="" value={0} /> @@ -452,6 +462,7 @@ exports[`PostHeader should match snapshot when post isBot and shouldRenderReplyB "paddingVertical": 2, } } + testID="post_header.display_name" > John Smith @@ -505,6 +516,7 @@ exports[`PostHeader should match snapshot when post isBot and shouldRenderReplyB "opacity": 0.5, } } + testID="post_header.date_time" timeZone="" value={0} /> @@ -605,6 +617,7 @@ exports[`PostHeader should match snapshot when post isBot and shouldRenderReplyB "paddingVertical": 2, } } + testID="post_header.display_name" > John Smith @@ -658,6 +671,7 @@ exports[`PostHeader should match snapshot when post isBot and shouldRenderReplyB "opacity": 0.5, } } + testID="post_header.date_time" timeZone="" value={0} /> @@ -756,6 +770,7 @@ exports[`PostHeader should match snapshot when post renders Commented On for new "paddingVertical": 2, } } + testID="post_header.display_name" > John Smith @@ -771,6 +786,7 @@ exports[`PostHeader should match snapshot when post renders Commented On for new "opacity": 0.5, } } + testID="post_header.date_time" timeZone="" value={0} /> @@ -843,6 +859,7 @@ exports[`PostHeader should match snapshot when post should display reply button "paddingVertical": 2, } } + testID="post_header.display_name" > John Smith @@ -858,6 +875,7 @@ exports[`PostHeader should match snapshot when post should display reply button "opacity": 0.5, } } + testID="post_header.date_time" timeZone="" value={0} /> diff --git a/app/components/post_header/post_header.js b/app/components/post_header/post_header.js index 2a4dff729..f636d6d4e 100644 --- a/app/components/post_header/post_header.js +++ b/app/components/post_header/post_header.js @@ -165,6 +165,7 @@ export default class PostHeader extends PureComponent { style={style.displayName} ellipsizeMode={'tail'} numberOfLines={1} + testID='post_header.display_name' > {name} @@ -177,6 +178,7 @@ export default class PostHeader extends PureComponent { id='post_info.system' defaultMessage='System' style={[style.displayName]} + testID='post_header.display_name' /> ); @@ -191,6 +193,7 @@ export default class PostHeader extends PureComponent { style={style.displayName} ellipsizeMode={'tail'} numberOfLines={1} + testID='post_header.display_name' > {displayName} @@ -204,6 +207,7 @@ export default class PostHeader extends PureComponent { id='channel_loader.someone' defaultMessage='Someone' style={style.displayName} + testID='post_header.display_name' /> ); @@ -302,6 +306,7 @@ export default class PostHeader extends PureComponent { timeZone={userTimezone} value={createAt} style={style.time} + testID='post_header.date_time' /> ); } else { @@ -311,6 +316,7 @@ export default class PostHeader extends PureComponent { hour12={!militaryTime} value={createAt} style={style.time} + testID='post_header.date_time' /> ); } diff --git a/app/components/post_profile_picture/post_profile_picture.js b/app/components/post_profile_picture/post_profile_picture.js index 23c97997e..6e003ee78 100644 --- a/app/components/post_profile_picture/post_profile_picture.js +++ b/app/components/post_profile_picture/post_profile_picture.js @@ -105,6 +105,7 @@ export default class PostProfilePicture extends PureComponent { size={ViewTypes.PROFILE_PICTURE_SIZE} iconSize={24} showStatus={showProfileStatus} + testID='post_profile_picture.profile_picture' /> ); diff --git a/app/components/profile_picture/profile_picture.js b/app/components/profile_picture/profile_picture.js index 3c7a0ec11..299045635 100644 --- a/app/components/profile_picture/profile_picture.js +++ b/app/components/profile_picture/profile_picture.js @@ -31,6 +31,7 @@ export default class ProfilePicture extends PureComponent { profileImageUri: PropTypes.string, profileImageRemove: PropTypes.bool, theme: PropTypes.object.isRequired, + testID: PropTypes.string, actions: PropTypes.shape({ getStatusForId: PropTypes.func.isRequired, setProfileImageUri: PropTypes.func.isRequired, @@ -109,7 +110,7 @@ export default class ProfilePicture extends PureComponent { } render() { - const {edit, showStatus, theme, user, size} = this.props; + const {edit, showStatus, theme, user, size, testID} = this.props; const {pictureUrl} = this.state; const style = getStyleSheet(theme); @@ -178,7 +179,10 @@ export default class ProfilePicture extends PureComponent { } return ( - + {image} {(showStatus || edit) && (user && !user.is_bot) && diff --git a/app/components/sidebars/settings/status_label/status_label.js b/app/components/sidebars/settings/status_label/status_label.js index ad4522c1c..11e9de0c2 100644 --- a/app/components/sidebars/settings/status_label/status_label.js +++ b/app/components/sidebars/settings/status_label/status_label.js @@ -51,6 +51,7 @@ export default class UserInfo extends PureComponent { id={i18nId} defaultMessage={defaultMessage} style={style.label} + testID={`user_status.label.${status}`} /> ); } diff --git a/app/components/sidebars/settings/user_info/user_info.js b/app/components/sidebars/settings/user_info/user_info.js index 3aaa8ef99..20388bf7e 100644 --- a/app/components/sidebars/settings/user_info/user_info.js +++ b/app/components/sidebars/settings/user_info/user_info.js @@ -47,6 +47,7 @@ export default class UserInfo extends PureComponent { iconSize={28} showStatus={false} userId={user.id} + testID={`${testID}.profile_picture`} /> diff --git a/app/components/user_status/__snapshots__/user_status.test.js.snap b/app/components/user_status/__snapshots__/user_status.test.js.snap index 4a4579b8e..148cb501f 100644 --- a/app/components/user_status/__snapshots__/user_status.test.js.snap +++ b/app/components/user_status/__snapshots__/user_status.test.js.snap @@ -9,6 +9,7 @@ exports[`UserStatus should match snapshot, away status 1`] = ` "fontSize": 32, } } + testID="user_status.icon.away" /> `; @@ -21,6 +22,7 @@ exports[`UserStatus should match snapshot, dnd status 1`] = ` "fontSize": 32, } } + testID="user_status.icon.dnd" /> `; @@ -33,6 +35,7 @@ exports[`UserStatus should match snapshot, online status 1`] = ` "fontSize": 32, } } + testID="user_status.icon.online" /> `; @@ -45,5 +48,6 @@ exports[`UserStatus should match snapshot, should default to offline status 1`] "fontSize": 32, } } + testID="user_status.icon.offline" /> `; diff --git a/app/components/user_status/user_status.js b/app/components/user_status/user_status.js index bda29d27f..9a88b3851 100644 --- a/app/components/user_status/user_status.js +++ b/app/components/user_status/user_status.js @@ -50,6 +50,7 @@ export default class UserStatus extends PureComponent { ); } diff --git a/app/screens/edit_profile/__snapshots__/edit_profile.test.js.snap b/app/screens/edit_profile/__snapshots__/edit_profile.test.js.snap index f16072b85..2681f5d78 100644 --- a/app/screens/edit_profile/__snapshots__/edit_profile.test.js.snap +++ b/app/screens/edit_profile/__snapshots__/edit_profile.test.js.snap @@ -17,6 +17,7 @@ exports[`edit_profile should match snapshot 1`] = ` size={153} statusBorderWidth={6} statusSize={36} + testID="edit_profile.profile_picture" /> `; diff --git a/app/screens/edit_profile/edit_profile.js b/app/screens/edit_profile/edit_profile.js index 08a00f5a7..1ffbbffc2 100644 --- a/app/screens/edit_profile/edit_profile.js +++ b/app/screens/edit_profile/edit_profile.js @@ -525,6 +525,7 @@ export default class EditProfile extends PureComponent { edit={!profilePictureDisabled} imageUri={uri} profileImageRemove={profileImageRemove} + testID='edit_profile.profile_picture' /> ); diff --git a/app/screens/notification/notification_icon.tsx b/app/screens/notification/notification_icon.tsx index 34ad024e1..73bfc88a1 100644 --- a/app/screens/notification/notification_icon.tsx +++ b/app/screens/notification/notification_icon.tsx @@ -51,6 +51,7 @@ const NotificationIcon = ({fromWebhook, overrideIconUrl, senderId, useUserIcon}: userId={user.id} size={IMAGE_SIZE} iconSize={24} + testID='in_app_notification.profile_picture' /> ); } else { diff --git a/app/screens/search/__snapshots__/recent_item.test.js.snap b/app/screens/search/__snapshots__/recent_item.test.js.snap index 6c2715d9f..582cb9da4 100644 --- a/app/screens/search/__snapshots__/recent_item.test.js.snap +++ b/app/screens/search/__snapshots__/recent_item.test.js.snap @@ -40,6 +40,7 @@ exports[`Search RecentItem should match snapshot and respond to events 1`] = ` "width": 50, } } + testID="search.recent_item.test.remove.button" > - + @@ -105,6 +110,7 @@ export default class ClockDisplay extends ClockDisplayBase { id='mobile.notification_settings.modal_cancel' defaultMessage='CANCEL' style={style.modalFooterOption} + testID='clock_display_settings.clock_modal_cancel.button' /> @@ -116,6 +122,7 @@ export default class ClockDisplay extends ClockDisplayBase { id='mobile.notification_settings.modal_save' defaultMessage='SAVE' style={style.modalFooterOption} + testID='clock_display_settings.clock_modal_save.button' /> @@ -131,7 +138,10 @@ export default class ClockDisplay extends ClockDisplayBase { const style = getStyleSheet(theme); return ( - + {this.renderClockDisplayModal(style)} diff --git a/app/screens/settings/clock_display/clock_display.ios.js b/app/screens/settings/clock_display/clock_display.ios.js index 7994107b0..f3794b8b5 100644 --- a/app/screens/settings/clock_display/clock_display.ios.js +++ b/app/screens/settings/clock_display/clock_display.ios.js @@ -26,6 +26,7 @@ export default class ClockDisplay extends ClockDisplayBase { @@ -46,6 +47,7 @@ export default class ClockDisplay extends ClockDisplayBase { actionType='select' actionValue='false' selected={newMilitaryTime === 'false'} + testID='clock_display_settings.normal_clock.action' theme={theme} /> @@ -60,6 +62,7 @@ export default class ClockDisplay extends ClockDisplayBase { actionType='select' actionValue='true' selected={newMilitaryTime === 'true'} + testID='clock_display_settings.military_clock.action' theme={theme} /> diff --git a/app/screens/settings/display_settings/__snapshots__/display_settings.test.js.snap b/app/screens/settings/display_settings/__snapshots__/display_settings.test.js.snap index 45a182026..3a5d5bb28 100644 --- a/app/screens/settings/display_settings/__snapshots__/display_settings.test.js.snap +++ b/app/screens/settings/display_settings/__snapshots__/display_settings.test.js.snap @@ -14,6 +14,7 @@ exports[`DisplaySettings should match snapshot 1`] = ` "flex": 1, } } + testID="display_settings.screen" > ); @@ -124,6 +125,7 @@ export default class DisplaySettings extends PureComponent { @@ -137,6 +139,7 @@ export default class DisplaySettings extends PureComponent { onPress={this.goToThemeSettings} separator={true} showArrow={showArrow} + testID='display_settings.theme.action' theme={theme} /> )} @@ -147,6 +150,7 @@ export default class DisplaySettings extends PureComponent { onPress={this.goToClockDisplaySettings} separator={disableClockDisplaySeparator} showArrow={showArrow} + testID='display_settings.clock_display.action' theme={theme} /> {timezoneOption} diff --git a/app/screens/user_profile/__snapshots__/user_profile.test.js.snap b/app/screens/user_profile/__snapshots__/user_profile.test.js.snap index c261ba98e..0b61dc2f3 100644 --- a/app/screens/user_profile/__snapshots__/user_profile.test.js.snap +++ b/app/screens/user_profile/__snapshots__/user_profile.test.js.snap @@ -36,6 +36,7 @@ exports[`user_profile should match snapshot 1`] = ` size={153} statusBorderWidth={6} statusSize={36} + testID="user_profile.profile_picture" userId="4hzdnk6mg7gepe7yze6m3domnc" /> {this.getDisplayName()} {`@${user.username}`} diff --git a/detox/e2e/support/server_api/user.js b/detox/e2e/support/server_api/user.js index f44c0a4fe..9f08053ff 100644 --- a/detox/e2e/support/server_api/user.js +++ b/detox/e2e/support/server_api/user.js @@ -24,6 +24,7 @@ import {getResponseFromError} from './common'; * See https://api.mattermost.com/#tag/users/paths/~1users~1login/post * @param {string} user.username - username of a user * @param {string} user.password - password of a user + * @return {Object} returns {user, status} on success or {error, status} on error */ export const apiLogin = async (user) => { try { @@ -49,6 +50,7 @@ export const apiLogin = async (user) => { /** * Login to Mattermost server as sysadmin. + * @return {Object} returns {user, status} on success or {error, status} on error */ export const apiAdminLogin = () => { return apiLogin({ @@ -60,6 +62,7 @@ export const apiAdminLogin = () => { /** * Logout from the Mattermost server. * See https://api.mattermost.com/#tag/users/paths/~1users~1logout/post + * @return {Object} returns data on success */ export const apiLogout = async () => { const response = await client.post('/api/v4/users/logout'); @@ -73,6 +76,7 @@ export const apiLogout = async () => { * Create a user. * See https://api.mattermost.com/#tag/users/paths/~1users/post * @param {Object} user - user object to be created + * @return {Object} returns {user} on success or {error, status} on error */ export const apiCreateUser = async ({prefix = 'user', user = null} = {}) => { try { @@ -91,6 +95,7 @@ export const apiCreateUser = async ({prefix = 'user', user = null} = {}) => { /** * Get user from a current session. + * @return {Object} returns {user} on success or {error, status} on error */ export const apiGetMe = () => { return apiGetUserById('me'); @@ -99,7 +104,8 @@ export const apiGetMe = () => { /** * Get a user by ID. * See https://api.mattermost.com/#tag/users/paths/~1users~1{user_id}/get - * @param {string} userId + * @param {string} userId - the user ID + * @return {Object} returns {user} on success or {error, status} on error */ export const apiGetUserById = async (userId) => { try { @@ -114,7 +120,8 @@ export const apiGetUserById = async (userId) => { /** * Get a user by username. * See https://api.mattermost.com/#tag/users/paths/~1users~1username~1{username}/get - * @param {string} username + * @param {string} username - the username + * @return {Object} returns {user} on success or {error, status} on error */ export const apiGetUserByUsername = async (username) => { try { @@ -126,6 +133,35 @@ export const apiGetUserByUsername = async (username) => { } }; +/** + * Patch user from a current session. + * @param {Object} userData - data to partially update a user + * @return {Object} returns {user} on success or {error, status} on error + */ +export const apiPatchMe = (userData) => { + return apiPatchUser('me', userData); +}; + +/** + * Patch a user. + * See https://api.mattermost.com/#tag/users/paths/~1users~1{user_id}~1patch/put + * @param {string} userId - the user ID + * @param {Object} userData - data to partially update a user + * @return {Object} returns {user} on success or {error, status} on error + */ +export const apiPatchUser = async (userId, userData) => { + try { + const response = await client.put( + `/api/v4/users/${userId}/patch`, + userData, + ); + + return {user: response.data}; + } catch (err) { + return getResponseFromError(err); + } +}; + function generateRandomUser(prefix) { const randomId = getRandomId(); @@ -147,6 +183,8 @@ export const User = { apiGetMe, apiGetUserById, apiGetUserByUsername, + apiPatchMe, + apiPatchUser, }; export default User; diff --git a/detox/e2e/support/ui/component/autocomplete.js b/detox/e2e/support/ui/component/autocomplete.js index 0e029a2bd..3cafdf5da 100644 --- a/detox/e2e/support/ui/component/autocomplete.js +++ b/detox/e2e/support/ui/component/autocomplete.js @@ -3,6 +3,8 @@ class Autocomplete { testID = { + atMentionItemPrefix: 'autocomplete.at_mention.item.', + channelMentionItemPrefix: 'autocomplete.channel_mention.item.', autocomplete: 'autocomplete', atMentionSuggestionList: 'at_mention_suggestion.list', channelMentionSuggestionList: 'channel_mention_suggestion.list', @@ -19,7 +21,11 @@ class Autocomplete { slashSuggestionList = element(by.id(this.testID.slashSuggestionList)); getAtMentionItem = (userId) => { - return element(by.id(`autocomplete.at_mention.item.${userId}`)); + return element(by.id(`${this.testID.atMentionItemPrefix}${userId}`)); + } + + getChannelMentionItem = (channelId) => { + return element(by.id(`${this.testID.channelMentionItemPrefix}${channelId}`)); } toBeVisible = async (isVisible = true) => { diff --git a/detox/e2e/support/ui/component/bottom_sheet.js b/detox/e2e/support/ui/component/bottom_sheet.js index 95cfc4fc0..6589d6066 100644 --- a/detox/e2e/support/ui/component/bottom_sheet.js +++ b/detox/e2e/support/ui/component/bottom_sheet.js @@ -9,7 +9,11 @@ class BottomSheet { // bottom sheet options archivedChannelsOption = isAndroid() ? element(by.text('Archived Channels')) : element(by.label('Archived Channels')).atIndex(0); + awayOption = isAndroid() ? element(by.text('Away')) : element(by.label('Away')).atIndex(0); cancelOption = isAndroid() ? element(by.text('Cancel')) : element(by.label('Cancel')).atIndex(0); + doNotDisturbOption = isAndroid() ? element(by.text('Do Not Disturb')) : element(by.label('Do No Disturb')).atIndex(0); + offlineOption = isAndroid() ? element(by.text('Offline')) : element(by.label('Offline')).atIndex(0); + onlineOption = isAndroid() ? element(by.text('Online')) : element(by.label('Online')).atIndex(0); publicChannelsOption = isAndroid() ? element(by.text('Public Channels')) : element(by.label('Public Channels')).atIndex(0); } diff --git a/detox/e2e/support/ui/component/index.js b/detox/e2e/support/ui/component/index.js index ec59e1615..f3208381e 100644 --- a/detox/e2e/support/ui/component/index.js +++ b/detox/e2e/support/ui/component/index.js @@ -16,6 +16,7 @@ import Post from './post'; import PostDraft from './post_draft'; import PostList from './post_list'; import PostOptions from './post_options'; +import ProfilePicture from './profile_picture'; import RecentItem from './recent_item'; import SearchBar from './search_bar'; import SendButton from './send_button'; @@ -37,6 +38,7 @@ export { PostDraft, PostList, PostOptions, + ProfilePicture, RecentItem, SearchBar, SendButton, diff --git a/detox/e2e/support/ui/component/post.js b/detox/e2e/support/ui/component/post.js index c68c634bf..9e987eb08 100644 --- a/detox/e2e/support/ui/component/post.js +++ b/detox/e2e/support/ui/component/post.js @@ -1,31 +1,71 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import ProfilePicture from './profile_picture'; + class Post { testID = { + postProfilePicturePrefix: 'post_profile_picture.profile_picture.', + message: 'markdown_text', + postHeaderDateTime: 'post_header.date_time', + postHeaderDisplayName: 'post_header.display_name', postHeaderReply: 'post_header.reply', postPreHeaderText: 'post_pre_header.text', - markdownText: 'markdown_text', } - getPost = (postItemSourceTestID, postId, postMessage) => { - const postTestID = `${postItemSourceTestID}.${postId}`; - const baseMatcher = by.id(postTestID); - const postItemMatcher = postMessage ? baseMatcher.withDescendant(by.text(postMessage)) : baseMatcher; - const postItemHeaderReplyMatcher = by.id(this.testID.postHeaderReply).withAncestor(postItemMatcher); + getPost = (postItemSourceTestID, postId, postMessage, postProfileOptions = {}) => { + const postItemMatcher = this.getPostItemMatcher(postItemSourceTestID, postId, postMessage); + const postItemMessageMatcher = by.id(this.testID.message).withAncestor(postItemMatcher); const postItemPreHeaderTextMatch = by.id(this.testID.postPreHeaderText).withAncestor(postItemMatcher); - const postItemMessageMatcher = by.id(this.testID.markdownText).withAncestor(postItemMatcher); return { postItem: element(postItemMatcher), - postItemHeaderReply: element(postItemHeaderReplyMatcher), - postItemPreHeaderText: element(postItemPreHeaderTextMatch), postItemMessage: element(postItemMessageMatcher), + postItemPreHeaderText: element(postItemPreHeaderTextMatch), + ...this.getPostHeader(postItemMatcher), + ...this.getPostProfilePicture(postItemMatcher, postProfileOptions), }; } + getPostHeader = (postItemMatcher) => { + const postItemHeaderDateTimeMatcher = by.id(this.testID.postHeaderDateTime).withAncestor(postItemMatcher); + const postItemHeaderDisplayNameMatcher = by.id(this.testID.postHeaderDisplayName).withAncestor(postItemMatcher); + const postItemHeaderReplyMatcher = by.id(this.testID.postHeaderReply).withAncestor(postItemMatcher); + + return { + postItemHeaderDateTime: element(postItemHeaderDateTimeMatcher), + postItemHeaderDisplayName: element(postItemHeaderDisplayNameMatcher), + postItemHeaderReply: element(postItemHeaderReplyMatcher), + }; + } + + getPostItemMatcher = (postItemSourceTestID, postId, postMessage) => { + const postTestID = `${postItemSourceTestID}.${postId}`; + const baseMatcher = by.id(postTestID); + return postMessage ? baseMatcher.withDescendant(by.text(postMessage)) : baseMatcher; + } + getPostMessage = (postItemSourceTestID) => { - return element(by.id(this.testID.markdownText).withAncestor(by.id(postItemSourceTestID))); + return element(by.id(this.testID.message).withAncestor(by.id(postItemSourceTestID))); + } + + getPostProfilePicture = (postItemMatcher, postProfileOptions = {userId: null, userStatus: 'online'}) => { + if (Object.keys(postProfileOptions).length === 0 || !postProfileOptions.userId) { + return { + postItemProfilePicture: null, + postItemProfilePictureUserStatus: null, + }; + } + + const profilePictureItemMatcher = ProfilePicture.getProfilePictureItemMatcher(this.testID.postProfilePicturePrefix, postProfileOptions.userId); + const profilePictureItemUserStatusMatcher = ProfilePicture.getProfilePictureItemUserStatusMatcher(profilePictureItemMatcher, postProfileOptions.userStatus); + const postItemProfilePictureMatcher = profilePictureItemMatcher.withAncestor(postItemMatcher); + const postItemProfilePictureUserStatusMatcher = profilePictureItemUserStatusMatcher.withAncestor(postItemMatcher); + + return { + postItemProfilePicture: element(postItemProfilePictureMatcher), + postItemProfilePictureUserStatus: element(postItemProfilePictureUserStatusMatcher), + }; } } diff --git a/detox/e2e/support/ui/component/post_list.js b/detox/e2e/support/ui/component/post_list.js index e02896e0b..fc9d99bc7 100644 --- a/detox/e2e/support/ui/component/post_list.js +++ b/detox/e2e/support/ui/component/post_list.js @@ -10,19 +10,27 @@ class PostList { }; } - getPost = (postId, postMessage) => { + getPost = (postId, postMessage, postProfileOptions = {}) => { const { postItem, + postItemHeaderDateTime, + postItemHeaderDisplayName, postItemHeaderReply, - postItemPreHeaderText, postItemMessage, - } = Post.getPost(this.testID.postListPostItem, postId, postMessage); + postItemPreHeaderText, + postItemProfilePicture, + postItemProfilePictureUserStatus, + } = Post.getPost(this.testID.postListPostItem, postId, postMessage, postProfileOptions); return { postListPostItem: postItem, + postListPostItemHeaderDateTime: postItemHeaderDateTime, + postListPostItemHeaderDisplayName: postItemHeaderDisplayName, postListPostItemHeaderReply: postItemHeaderReply, - postListPostItemPreHeaderText: postItemPreHeaderText, postListPostItemMessage: postItemMessage, + postListPostItemPreHeaderText: postItemPreHeaderText, + postListPostItemProfilePicture: postItemProfilePicture, + postListPostItemProfilePictureUserStatus: postItemProfilePictureUserStatus, }; } diff --git a/detox/e2e/support/ui/component/profile_picture.js b/detox/e2e/support/ui/component/profile_picture.js new file mode 100644 index 000000000..53ef4b8e9 --- /dev/null +++ b/detox/e2e/support/ui/component/profile_picture.js @@ -0,0 +1,21 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +class ProfilePicture { + testID = { + userStatusIconPrefix: 'user_status.icon.', + } + + getProfilePictureItemMatcher = (profilePictureSourcePrefix, userId) => { + const profilePictureTestID = `${profilePictureSourcePrefix}${userId}`; + return by.id(profilePictureTestID); + } + + getProfilePictureItemUserStatusMatcher(profilePictureItemMatcher, userStatus = 'online') { + const userStatusIconTestID = `${this.testID.userStatusIconPrefix}${userStatus}`; + return by.id(userStatusIconTestID).withAncestor(profilePictureItemMatcher); + } +} + +const profilePicture = new ProfilePicture(); +export default profilePicture; diff --git a/detox/e2e/support/ui/component/recent_item.js b/detox/e2e/support/ui/component/recent_item.js index c36440a1d..eaf0e170e 100644 --- a/detox/e2e/support/ui/component/recent_item.js +++ b/detox/e2e/support/ui/component/recent_item.js @@ -3,11 +3,18 @@ class RecentItem { testID = { - recentItemPrefix: 'search.recent_item.', + recentSearchItemPrefix: 'search.recent_item.', + recentSearchItemRemoveButtonSuffix: '.remove.button', } - getRecentItem = (searchTerms) => { - return element(by.id(`${this.testID.recentItemPrefix}${searchTerms}`)); + getRecentSearchItem = (searchTerms) => { + const recentSearchItemTestID = `${this.testID.recentSearchItemPrefix}${searchTerms}`; + const recentSearchItemRemoveButtonTestID = `${recentSearchItemTestID}${this.testID.recentSearchItemRemoveButtonSuffix}`; + + return { + recentSearchItem: element(by.id(recentSearchItemTestID)), + recentSearchItemRemoveButton: element(by.id(recentSearchItemRemoveButtonTestID)), + }; } } diff --git a/detox/e2e/support/ui/component/settings_sidebar.js b/detox/e2e/support/ui/component/settings_sidebar.js index 93b28781d..f618c982a 100644 --- a/detox/e2e/support/ui/component/settings_sidebar.js +++ b/detox/e2e/support/ui/component/settings_sidebar.js @@ -1,8 +1,12 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import BottomSheet from './bottom_sheet'; + class SettingsSidebar { testID = { + userStatusIconPrefix: 'user_status.icon.', + userStatusLabelPrefix: 'user_status.label.', settingsSidebar: 'settings.sidebar', userInfoAction: 'settings.sidebar.user_info.action', statusAction: 'settings.sidebar.status.action', @@ -22,12 +26,55 @@ class SettingsSidebar { settingsAction = element(by.id(this.testID.settingsAction)); logoutAction = element(by.id(this.testID.logoutAction)); + getUserStatus(userStatus) { + const userStatusIconTestID = `${this.testID.userStatusIconPrefix}${userStatus}`; + const userStatusLabelTestID = `${this.testID.userStatusLabelPrefix}${userStatus}`; + + return { + userStatusIcon: element(by.id(userStatusIconTestID)), + userStatusLabel: element(by.id(userStatusLabelTestID)), + }; + } + toBeVisible = async () => { await expect(this.settingsSidebar).toBeVisible(); return this.settingsSidebar; } + setUserStatusTo = async (userStatus) => { + const { + awayOption, + doNotDisturbOption, + offlineOption, + onlineOption, + } = BottomSheet; + + // # Set user status + await this.statusAction.tap(); + switch (userStatus) { + case 'online': + await onlineOption.tap(); + break; + case 'away': + await awayOption.tap(); + break; + case 'dnd': + await doNotDisturbOption.tap(); + break; + case 'offline': + await offlineOption.tap(); + break; + default: + throw new Error('Not a valid user status option: ' + userStatus); + } + + // * Verify user status + const {userStatusIcon, userStatusLabel} = this.getUserStatus(userStatus); + await expect(userStatusIcon).toBeVisible(); + await expect(userStatusLabel).toBeVisible(); + } + tapLogoutAction = async () => { await this.logoutAction.tap(); await expect(this.settingsSidebar).not.toBeVisible(); diff --git a/detox/e2e/support/ui/screen/channel.js b/detox/e2e/support/ui/screen/channel.js index b7c938c92..65021a983 100644 --- a/detox/e2e/support/ui/screen/channel.js +++ b/detox/e2e/support/ui/screen/channel.js @@ -67,16 +67,16 @@ class ChannelScreen { postList = new PostList(this.testID.channelScreenPrefix); - getLongPostItem = (postId, text) => { - return LongPostScreen.getPost(postId, text); + getLongPostItem = (postId, text, postProfileOptions = {}) => { + return LongPostScreen.getPost(postId, text, postProfileOptions); } getLongPostMessage = () => { return LongPostScreen.getPostMessage(); } - getPostListPostItem = (postId, text) => { - return this.postList.getPost(postId, text); + getPostListPostItem = (postId, text, postProfileOptions = {}) => { + return this.postList.getPost(postId, text, postProfileOptions); } getPostMessageAtIndex = (index) => { diff --git a/detox/e2e/support/ui/screen/clock_display_settings.js b/detox/e2e/support/ui/screen/clock_display_settings.js new file mode 100644 index 000000000..bf46ae871 --- /dev/null +++ b/detox/e2e/support/ui/screen/clock_display_settings.js @@ -0,0 +1,65 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {DisplaySettingsScreen} from '@support/ui/screen'; +import {isAndroid} from '@support/utils'; + +class ClockDisplaySettingsScreen { + testID = { + clockDisplaySettingsScreen: 'clock_display_settings.screen', + backButton: 'screen.back.button', + militaryClockAction: 'clock_display_settings.military_clock.action', + normalClockAction: 'clock_display_settings.normal_clock.action', + + // ANDROID ONLY + clockModal: 'clock_display_settings.clock.modal', + clockModalCancelButton: 'clock_display_settings.clock_modal_cancel.button', + clockModalSaveButton: 'clock_display_settings.clock_modal_save.button', + } + + clockDisplaySettingsScreen = element(by.id(this.testID.clockDisplaySettingsScreen)); + backButton = element(by.id(this.testID.backButton)); + militaryClockAction = element(by.id(this.testID.militaryClockAction)); + normalClockAction = element(by.id(this.testID.normalClockAction)); + + // ANDROID ONLY + clockModal = element(by.id(this.testID.clockModal)); + clockModalCancelButton = element(by.id(this.testID.clockModalCancelButton)); + clockModalSaveButton = element(by.id(this.testID.clockModalSaveButton)); + + getClockActionFor = (clockKey) => { + switch (clockKey) { + case 'military': + return this.militaryClockAction; + case 'normal': + return this.normalClockAction; + default: + throw new Error('Not a valid clock option: ' + clockKey); + } + } + + toBeVisible = async () => { + if (isAndroid()) { + await expect(this.clockModal).toBeVisible(); + return this.clockModal; + } + + await expect(this.clockDisplaySettingsScreen).toBeVisible(); + return this.clockDisplaySettingsScreen; + } + + open = async () => { + // # Open clock display settings screen + await DisplaySettingsScreen.clockDisplayAction.tap(); + + return this.toBeVisible(); + } + + back = async () => { + await this.backButton.tap(); + await expect(this.clockDisplaySettingsScreen).not.toBeVisible(); + } +} + +const clockDisplaySettingsScreen = new ClockDisplaySettingsScreen(); +export default clockDisplaySettingsScreen; diff --git a/detox/e2e/support/ui/screen/display_settings.js b/detox/e2e/support/ui/screen/display_settings.js new file mode 100644 index 000000000..05d571704 --- /dev/null +++ b/detox/e2e/support/ui/screen/display_settings.js @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {GeneralSettingsScreen} from '@support/ui/screen'; + +class DisplaySettingsScreen { + testID = { + displaySettingsScreen: 'display_settings.screen', + backButton: 'screen.back.button', + clockDisplayAction: 'display_settings.clock_display.action', + themeAction: 'display_settings.theme.action', + timezoneAction: 'display_settings.timezone.action', + } + + displaySettingsScreen = element(by.id(this.testID.displaySettingsScreen)); + backButton = element(by.id(this.testID.backButton)); + clockDisplayAction = element(by.id(this.testID.clockDisplayAction)); + themeAction = element(by.id(this.testID.themeAction)); + timezoneAction = element(by.id(this.testID.timezoneAction)); + + toBeVisible = async () => { + await expect(this.displaySettingsScreen).toBeVisible(); + + return this.displaySettingsScreen; + } + + open = async () => { + // # Open display settings screen + await GeneralSettingsScreen.displayAction.tap(); + + return this.toBeVisible(); + } + + back = async () => { + await this.backButton.tap(); + await expect(this.displaySettingsScreen).not.toBeVisible(); + } +} + +const displaySettingsScreen = new DisplaySettingsScreen(); +export default displaySettingsScreen; diff --git a/detox/e2e/support/ui/screen/index.js b/detox/e2e/support/ui/screen/index.js index c156642e3..c5b9a4fb3 100644 --- a/detox/e2e/support/ui/screen/index.js +++ b/detox/e2e/support/ui/screen/index.js @@ -5,7 +5,9 @@ import AddReactionScreen from './add_reaction'; import ChannelInfoScreen from './channel_info'; import ChannelNotificationPreferenceScreen from './channel_notification_preference'; import ChannelScreen from './channel'; +import ClockDisplaySettingsScreen from './clock_display_settings'; import CreateChannelScreen from './create_channel'; +import DisplaySettingsScreen from './display_settings'; import EditChannelScreen from './edit_channel'; import EditPostScreen from './edit_post'; import GeneralSettingsScreen from './general_settings'; @@ -32,7 +34,9 @@ export { ChannelInfoScreen, ChannelNotificationPreferenceScreen, ChannelScreen, + ClockDisplaySettingsScreen, CreateChannelScreen, + DisplaySettingsScreen, EditChannelScreen, EditPostScreen, GeneralSettingsScreen, diff --git a/detox/e2e/support/ui/screen/long_post.js b/detox/e2e/support/ui/screen/long_post.js index 2fc203402..6ef87983d 100644 --- a/detox/e2e/support/ui/screen/long_post.js +++ b/detox/e2e/support/ui/screen/long_post.js @@ -8,12 +8,25 @@ class LongPostScreen { longPostItem: 'long_post.post', } - getPost = (postId, postMessage) => { - const {postItem, postItemHeaderReply, postItemMessage} = Post.getPost(this.testID.longPostItem, postId, postMessage); + getPost = (postId, postMessage, postProfileOptions = {}) => { + const { + postItem, + postItemHeaderDateTime, + postItemHeaderDisplayName, + postItemHeaderReply, + postItemMessage, + postItemProfilePicture, + postItemProfilePictureUserStatus, + } = Post.getPost(this.testID.longPostItem, postId, postMessage, postProfileOptions); + return { longPostItem: postItem, + longPostItemHeaderDateTime: postItemHeaderDateTime, + longPostItemHeaderDisplayName: postItemHeaderDisplayName, longPostItemHeaderReply: postItemHeaderReply, longPostItemMessage: postItemMessage, + longPostItemProfilePicture: postItemProfilePicture, + longPostItemProfilePictureUserStatus: postItemProfilePictureUserStatus, }; } diff --git a/detox/e2e/support/ui/screen/more_channels.js b/detox/e2e/support/ui/screen/more_channels.js index 51c96d988..64e77feb3 100644 --- a/detox/e2e/support/ui/screen/more_channels.js +++ b/detox/e2e/support/ui/screen/more_channels.js @@ -9,8 +9,8 @@ import { class MoreChannelsScreen { testID = { - moreChannelsScreen: 'more_channels.screen', moreChannelsScreenPrefix: 'more_channels.', + moreChannelsScreen: 'more_channels.screen', closeMoreChannelsButton: 'close.more_channels.button', createButton: 'more_channels.create.button', channelDropdown: 'more_channels.channel.dropdown', diff --git a/detox/e2e/support/ui/screen/more_direct_messages.js b/detox/e2e/support/ui/screen/more_direct_messages.js index 55c3d0059..8adec04aa 100644 --- a/detox/e2e/support/ui/screen/more_direct_messages.js +++ b/detox/e2e/support/ui/screen/more_direct_messages.js @@ -8,8 +8,8 @@ import { class MoreDirectMessagesScreen { testID = { + moreDirectMessagesScreenPrefix: 'more_direct_messages.', moreDirectMessagesScreen: 'more_direct_messages.screen', - moreDirectMessagesPrefix: 'more_direct_messages.', startButton: 'more_direct_messages.start.button', usersList: 'more_direct_messages.custom_list', userItem: 'more_direct_messages.custom_list.user_item', @@ -21,10 +21,10 @@ class MoreDirectMessagesScreen { usersList = element(by.id(this.testID.usersList)); // convenience props - searchBar = SearchBar.getSearchBar(this.testID.moreDirectMessagesPrefix); - searchInput = SearchBar.getSearchInput(this.testID.moreDirectMessagesPrefix); - cancelButton = SearchBar.getCancelButton(this.testID.moreDirectMessagesPrefix); - clearButton = SearchBar.getClearButton(this.testID.moreDirectMessagesPrefix); + searchBar = SearchBar.getSearchBar(this.testID.moreDirectMessagesScreenPrefix); + searchInput = SearchBar.getSearchInput(this.testID.moreDirectMessagesScreenPrefix); + cancelButton = SearchBar.getCancelButton(this.testID.moreDirectMessagesScreenPrefix); + clearButton = SearchBar.getClearButton(this.testID.moreDirectMessagesScreenPrefix); getUser = (userId, diplayUsername) => { const userItemTestID = `${this.testID.userItem}.${userId}`; diff --git a/detox/e2e/support/ui/screen/permalink.js b/detox/e2e/support/ui/screen/permalink.js index cbf4cce73..0fbdc41a7 100644 --- a/detox/e2e/support/ui/screen/permalink.js +++ b/detox/e2e/support/ui/screen/permalink.js @@ -16,16 +16,16 @@ class PermalinkScreen { postList = new PostList(this.testID.permalinkScreenPrefix); - getLongPostItem = (postId, text) => { - return LongPostScreen.getPost(postId, text); + getLongPostItem = (postId, text, postProfileOptions = {}) => { + return LongPostScreen.getPost(postId, text, postProfileOptions); } getLongPostMessage = () => { return LongPostScreen.getPostMessage(); } - getPostListPostItem = (postId, text) => { - return this.postList.getPost(postId, text); + getPostListPostItem = (postId, text, postProfileOptions = {}) => { + return this.postList.getPost(postId, text, postProfileOptions); } getPostMessageAtIndex = (index) => { diff --git a/detox/e2e/support/ui/screen/pinned_messages.js b/detox/e2e/support/ui/screen/pinned_messages.js index e83af8ebd..4d4150163 100644 --- a/detox/e2e/support/ui/screen/pinned_messages.js +++ b/detox/e2e/support/ui/screen/pinned_messages.js @@ -15,8 +15,8 @@ class PinnedMessagesScreen { pinnedMessagesScreen = element(by.id(this.testID.pinnedMessagesScreen)); backButton = element(by.id(this.testID.backButton)); - getSearchResultPostItem = (postId, text) => { - return SearchResultPostScreen.getPost(postId, text); + getSearchResultPostItem = (postId, text, postProfileOptions = {}) => { + return SearchResultPostScreen.getPost(postId, text, postProfileOptions); } toBeVisible = async () => { diff --git a/detox/e2e/support/ui/screen/recent_mentions.js b/detox/e2e/support/ui/screen/recent_mentions.js index c49ab5e7f..84332eb7a 100644 --- a/detox/e2e/support/ui/screen/recent_mentions.js +++ b/detox/e2e/support/ui/screen/recent_mentions.js @@ -16,8 +16,8 @@ class RecentMentionsScreen { recentMentionsScreen = element(by.id(this.testID.recentMentionsScreen)); closeSettingsButton = element(by.id(this.testID.closeSettingsButton)); - getSearchResultPostItem = (postId, text) => { - return SearchResultPostScreen.getPost(postId, text); + getSearchResultPostItem = (postId, text, postProfileOptions = {}) => { + return SearchResultPostScreen.getPost(postId, text, postProfileOptions); } toBeVisible = async () => { diff --git a/detox/e2e/support/ui/screen/saved_messages.js b/detox/e2e/support/ui/screen/saved_messages.js index b80afeff8..6cf329d0b 100644 --- a/detox/e2e/support/ui/screen/saved_messages.js +++ b/detox/e2e/support/ui/screen/saved_messages.js @@ -16,8 +16,8 @@ class SavedMessagesScreen { savedMessagesScreen = element(by.id(this.testID.savedMessagesScreen)); closeSettingsButton = element(by.id(this.testID.closeSettingsButton)); - getSearchResultPostItem = (postId, text) => { - return SearchResultPostScreen.getPost(postId, text); + getSearchResultPostItem = (postId, text, postProfileOptions = {}) => { + return SearchResultPostScreen.getPost(postId, text, postProfileOptions); } toBeVisible = async () => { diff --git a/detox/e2e/support/ui/screen/search.js b/detox/e2e/support/ui/screen/search.js index aa3f36df8..9169188ab 100644 --- a/detox/e2e/support/ui/screen/search.js +++ b/detox/e2e/support/ui/screen/search.js @@ -41,11 +41,11 @@ class SearchScreen { clearButton = SearchBar.getClearButton(this.testID.searchScreenPrefix); getRecentSearchItem = (searchTerms) => { - return RecentItem.getRecentItem(searchTerms); + return RecentItem.getRecentSearchItem(searchTerms); } - getSearchResultPostItem = (postId, text) => { - return SearchResultPostScreen.getPost(postId, text); + getSearchResultPostItem = (postId, text, postProfileOptions = {}) => { + return SearchResultPostScreen.getPost(postId, text, postProfileOptions); } getSearchResultPostMessageAtIndex = (index) => { diff --git a/detox/e2e/support/ui/screen/search_result_post.js b/detox/e2e/support/ui/screen/search_result_post.js index e07b42ac5..301ac3979 100644 --- a/detox/e2e/support/ui/screen/search_result_post.js +++ b/detox/e2e/support/ui/screen/search_result_post.js @@ -8,12 +8,25 @@ class SearchResultPostScreen { searchResultPostItem: 'search_result_post.post', } - getPost = (postId, postMessage) => { - const {postItem, postItemHeaderReply, postItemMessage} = Post.getPost(this.testID.searchResultPostItem, postId, postMessage); + getPost = (postId, postMessage, postProfileOptions = {}) => { + const { + postItem, + postItemHeaderDateTime, + postItemHeaderDisplayName, + postItemHeaderReply, + postItemMessage, + postItemProfilePicture, + postItemProfilePictureUserStatus, + } = Post.getPost(this.testID.searchResultPostItem, postId, postMessage, postProfileOptions); + return { searchResultPostItem: postItem, + searchResultPostItemHeaderDateTime: postItemHeaderDateTime, + searchResultPostItemHeaderDisplayName: postItemHeaderDisplayName, searchResultPostItemHeaderReply: postItemHeaderReply, searchResultPostItemMessage: postItemMessage, + searchResultPostItemProfilePicture: postItemProfilePicture, + searchResultPostItemProfilePictureUserStatus: postItemProfilePictureUserStatus, }; } diff --git a/detox/e2e/support/ui/screen/thread.js b/detox/e2e/support/ui/screen/thread.js index 7458a4278..5010cc010 100644 --- a/detox/e2e/support/ui/screen/thread.js +++ b/detox/e2e/support/ui/screen/thread.js @@ -44,16 +44,16 @@ class ThreadScreen { postList = new PostList(this.testID.threadScreenPrefix); - getLongPostItem = (postId, text) => { - return LongPostScreen.getPost(postId, text); + getLongPostItem = (postId, text, postProfileOptions = {}) => { + return LongPostScreen.getPost(postId, text, postProfileOptions); } getLongPostMessage = () => { return LongPostScreen.getPostMessage(); } - getPostListPostItem = (postId, text) => { - return this.postList.getPost(postId, text); + getPostListPostItem = (postId, text, postProfileOptions = {}) => { + return this.postList.getPost(postId, text, postProfileOptions); } getPostMessageAtIndex = (index) => { diff --git a/detox/e2e/test/smoke_test/clock_display.e2e.js b/detox/e2e/test/smoke_test/clock_display.e2e.js new file mode 100644 index 000000000..077945176 --- /dev/null +++ b/detox/e2e/test/smoke_test/clock_display.e2e.js @@ -0,0 +1,112 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// ******************************************************************* +// - [#] indicates a test step (e.g. # Go to a screen) +// - [*] indicates an assertion (e.g. * Check the title) +// - Use element testID when selecting an element. Create one if none. +// ******************************************************************* + +import moment from 'moment-timezone'; + +import { + ChannelScreen, + ClockDisplaySettingsScreen, + DisplaySettingsScreen, + GeneralSettingsScreen, +} from '@support/ui/screen'; +import { + Channel, + Post, + Setup, + System, + User, +} from '@support/server_api'; +import {isAndroid, isIos} from '@support/utils'; + +describe('Clock Display', () => { + const testDate = Date.UTC(new Date().getFullYear() + 1, 0, 5, 14, 37); // Jan 5, 2:37pm + const testTimezone = 'UTC'; + const testFullDate = moment(testDate).tz(testTimezone).format(); + const testMilitaryTime = moment(testDate).tz(testTimezone).format('HH:mm'); + const testNormalTime = moment(testDate).tz(testTimezone).format('h:mm A'); + const testMessage = `Hello from ${testFullDate}, ${testTimezone}`; + let testChannel; + + beforeAll(async () => { + // # Enable timezone + await System.apiUpdateConfig({DisplaySettings: {ExperimentalTimezone: true}}); + + const {team, user} = await Setup.apiInit(); + ({channel: testChannel} = await Channel.apiGetChannelByName(team.name, 'town-square')); + + // # Set user's timezone + await User.apiPatchUser(user.id, {timezone: {automaticTimezone: '', manualTimezone: testTimezone, useAutomaticTimezone: 'false'}}); + + // # Post message with a future date as sysadmin + await User.apiAdminLogin(); + await Post.apiCreatePost({ + channelId: testChannel.id, + message: testMessage, + createAt: testDate, + }); + + // # Open channel screen + await ChannelScreen.open(user); + }); + + afterAll(async () => { + await ChannelScreen.logout(); + }); + + it('MM-T3257 should be able to set clock display to 12-hour', async () => { + // # Set clock display to normal (12-hour) + await setClockDisplayTo('normal'); + + // * Verify clock display is normal + await verifyClockDisplay(testChannel, testMessage, testNormalTime); + }); + + it('MM-T3258 should be able to set clock display to 24-hour', async () => { + // # Set clock display to military (24-hour) + await setClockDisplayTo('military'); + + // * Verify clock display is military + await verifyClockDisplay(testChannel, testMessage, testMilitaryTime); + }); +}); + +async function setClockDisplayTo(clockKey) { + const { + clockModalSaveButton, + getClockActionFor, + } = ClockDisplaySettingsScreen; + + // # Open clock display settings screen + await ChannelScreen.openSettingsSidebar(); + await GeneralSettingsScreen.open(); + await DisplaySettingsScreen.open(); + await ClockDisplaySettingsScreen.open(); + + // # Tap on clock option + await getClockActionFor(clockKey).tap(); + + // # Tap on Save button if Android + if (isAndroid()) { + await clockModalSaveButton.tap(); + } + + // # Go back to channel + await ClockDisplaySettingsScreen.back(); + if (isIos()) { + await DisplaySettingsScreen.back(); + } + await GeneralSettingsScreen.close(); +} + +async function verifyClockDisplay(testChannel, testMessage, clockDisplay) { + // * Verify clock display + const {post} = await Post.apiGetLastPostInChannel(testChannel.id); + const {postListPostItemHeaderDateTime} = await ChannelScreen.getPostListPostItem(post.id, testMessage); + await expect(postListPostItemHeaderDateTime).toHaveText(clockDisplay); +} diff --git a/detox/e2e/test/smoke_test/email_notifications.e2e.js b/detox/e2e/test/smoke_test/email_notifications.e2e.js index 33d39bc25..2ebe93ab9 100644 --- a/detox/e2e/test/smoke_test/email_notifications.e2e.js +++ b/detox/e2e/test/smoke_test/email_notifications.e2e.js @@ -77,11 +77,11 @@ describe('Email Notifications', () => { await navigateBackToChannel(); // # Post an at-mention message to mentioned user by other user - const message = `Mention @${testUser.username} by ${testOtherUser1.username}`; + const testMessage = `Mention @${testUser.username} by ${testOtherUser1.username}`; await User.apiLogin(testOtherUser1); await Post.apiCreatePost({ channelId: testChannel.id, - message, + message: testMessage, }); // * Verify mentioned user receives email notification @@ -93,7 +93,7 @@ describe('Email Notifications', () => { testChannel.display_name, testUser, testOtherUser1, - message, + testMessage, testConfig.EmailSettings.FeedbackEmail, testConfig.SupportSettings.SupportEmail); }); @@ -108,17 +108,17 @@ describe('Email Notifications', () => { await navigateBackToChannel(); // # Post an at-mention message to mentioned user by other user - const message = `Mention @${testUser.username} by ${testOtherUser2.username}`; + const testMessage = `Mention @${testUser.username} by ${testOtherUser2.username}`; await User.apiLogin(testOtherUser2); await Post.apiCreatePost({ channelId: testChannel.id, - message, + message: testMessage, }); // * Verify mentioned user does not receive email notification const {data} = await getRecentEmail(testUser.username); const bodyText = splitEmailBodyText(data.body.text); - jestExpect(bodyText[7]).not.toEqual(message); + jestExpect(bodyText[7]).not.toEqual(testMessage); }); }); diff --git a/detox/e2e/test/smoke_test/search.e2e.js b/detox/e2e/test/smoke_test/search.e2e.js index e393793d9..74b66b369 100644 --- a/detox/e2e/test/smoke_test/search.e2e.js +++ b/detox/e2e/test/smoke_test/search.e2e.js @@ -21,11 +21,15 @@ import { } from '@support/server_api'; describe('Search', () => { + const testMessage = `test ${Date.now().toString()}`; + const partialSearchTerm = testMessage.split('-')[0]; + const { + atMentionSuggestionList, + channelMentionSuggestionList, + } = Autocomplete; let testUser; let testChannel; - const {atMentionSuggestionList} = Autocomplete; - beforeAll(async () => { const {team, user} = await Setup.apiInit(); testUser = user; @@ -41,11 +45,33 @@ describe('Search', () => { await ChannelScreen.logout(); }); - it('MM-T3236 search on sender, select from autocomplete, reply from search results', async () => { - const testMessage = Date.now().toString(); + it('MM-T3235 search on text, jump to result', async () => { + // # Post message and search on text + await postMessageAndSearchText(testMessage, partialSearchTerm); + // # Open permalink from search result post item + const lastPost = await Post.apiGetLastPostInChannel(testChannel.id); + const {searchResultPostItem} = await SearchScreen.getSearchResultPostItem(lastPost.post.id, testMessage); + await expect(searchResultPostItem).toBeVisible(); + await searchResultPostItem.tap(); + + // * Verify permalink post list has the message + await PermalinkScreen.toBeVisible(); + const {postListPostItem: permalinkPostItem} = await PermalinkScreen.getPostListPostItem(lastPost.post.id, testMessage); + await waitFor(permalinkPostItem).toBeVisible(); + + // # Jump to recent messages + await PermalinkScreen.jumpToRecentMessages(); + + // * Verify user is on channel where message is posted + await expect(ChannelScreen.channelNavBarTitle).toHaveText(testChannel.display_name); + const {postListPostItem: channelPostItem} = await ChannelScreen.getPostListPostItem(lastPost.post.id, testMessage); + await expect(channelPostItem).toBeVisible(); + }); + + it('MM-T3236 search on sender, select from autocomplete, reply from search results', async () => { // # Post message and search on sender - await postMessageAndSearchFrom(testMessage, testUser, atMentionSuggestionList); + await postMessageAndSearchFrom(testMessage, testUser, atMentionSuggestionList, partialSearchTerm); // * Verify search result post has the message const lastPost = await Post.apiGetLastPostInChannel(testChannel.id); @@ -73,35 +99,53 @@ describe('Search', () => { await SearchScreen.cancel(); }); - it('MM-T3235 search on text, jump to result', async () => { - const testMessage = Date.now().toString(); + it('MM-T3237 search on channel, select from autocomplete, search in combination with a text string as well', async () => { + // # Post message and search in channel + await postMessageAndSearchIn(testMessage, testChannel, channelMentionSuggestionList, partialSearchTerm); - // # Post message and search on text - await postMessageAndSearchText(testMessage); - - // # Open permalink from search result post item + // * Verify search result post has the message const lastPost = await Post.apiGetLastPostInChannel(testChannel.id); const {searchResultPostItem} = await SearchScreen.getSearchResultPostItem(lastPost.post.id, testMessage); await expect(searchResultPostItem).toBeVisible(); - await searchResultPostItem.tap(); - // * Verify permalink post list has the message - await PermalinkScreen.toBeVisible(); - const {postListPostItem: permalinkPostItem} = await PermalinkScreen.getPostListPostItem(lastPost.post.id, testMessage); - await waitFor(permalinkPostItem).toBeVisible(); + // # Go back to channel + await SearchScreen.cancel(); + }); - // # Jump to recent messages - await PermalinkScreen.jumpToRecentMessages(); + it('MM-T3238 delete one previous search, tap on another', async () => { + const { + getRecentSearchItem, + searchInModifier, + } = SearchScreen; - // * Verify user is on channel where message is posted - await expect(ChannelScreen.channelNavBarTitle).toHaveText(testChannel.display_name); - const {postListPostItem: channelPostItem} = await ChannelScreen.getPostListPostItem(lastPost.post.id, testMessage); - await expect(channelPostItem).toBeVisible(); + // # Open search screen + await SearchScreen.open(); + + // # Delete one previous text search + const previousTextSearch = await getRecentSearchItem(testMessage); + await previousTextSearch.recentSearchItemRemoveButton.tap(); + + // * Verify previous text search is removed + await expect(previousTextSearch.recentSearchItem).not.toBeVisible(); + + // # Tap on previous in search + const inSearchTerms = `${searchInModifier} ${testChannel.name} ${partialSearchTerm}`; + const previousInSearch = await getRecentSearchItem(inSearchTerms); + await previousInSearch.recentSearchItem.tap(); + + // * Verify search result post has the message + const lastPost = await Post.apiGetLastPostInChannel(testChannel.id); + const {searchResultPostItem} = await SearchScreen.getSearchResultPostItem(lastPost.post.id, testMessage); + await expect(searchResultPostItem).toBeVisible(); + + // # Go back to channel + await SearchScreen.cancel(); }); }); -async function postMessageAndSearchFrom(testMessage, testUser, atMentionSuggestionList) { +async function postMessageAndSearchFrom(testMessage, testUser, atMentionSuggestionList, partialSearchTerm) { const { + getRecentSearchItem, searchFromModifier, searchFromSection, searchInput, @@ -126,18 +170,64 @@ async function postMessageAndSearchFrom(testMessage, testUser, atMentionSuggesti const userAtMentionAutocomplete = await Autocomplete.getAtMentionItem(testUser.id); await userAtMentionAutocomplete.tap(); + // # Type end of search term + await searchInput.typeText(partialSearchTerm); + // # Search user await searchInput.tapReturnKey(); await expect(atMentionSuggestionList).not.toExist(); // * Verify recent search item is displayed - const searchTerms = `${searchFromModifier} ${testUser.username}`; - const recentSearchItem = await SearchScreen.getRecentSearchItem(searchTerms); + const searchTerms = `${searchFromModifier} ${testUser.username} ${partialSearchTerm}`; + const {recentSearchItem} = await getRecentSearchItem(searchTerms); await expect(recentSearchItem).toBeVisible(); } -async function postMessageAndSearchText(testMessage) { - const {searchInput} = SearchScreen; +async function postMessageAndSearchIn(testMessage, testChannel, channelMentionSuggestionList, partialSearchTerm) { + const { + getRecentSearchItem, + searchInModifier, + searchInSection, + searchInput, + } = SearchScreen; + + // # Post a message + await ChannelScreen.postMessage(testMessage); + + // # Open search screen + await SearchScreen.open(); + + // # Tap "in:" modifier + await searchInput.clearText(); + await searchInSection.tap(); + + // # Type beginning of search term + const searchTerm = testChannel.name; + await searchInput.typeText(searchTerm); + + // # Select channel from autocomplete + await expect(channelMentionSuggestionList).toExist(); + const channelMentionAutocomplete = await Autocomplete.getChannelMentionItem(testChannel.id); + await channelMentionAutocomplete.tap(); + + // # Type end of search term + await searchInput.typeText(partialSearchTerm); + + // # Search channel + await searchInput.tapReturnKey(); + await expect(channelMentionSuggestionList).not.toExist(); + + // * Verify recent search item is displayed + const searchTerms = `${searchInModifier} ${testChannel.name} ${partialSearchTerm}`; + const {recentSearchItem} = await getRecentSearchItem(searchTerms); + await expect(recentSearchItem).toBeVisible(); +} + +async function postMessageAndSearchText(testMessage, partialSearchTerm) { + const { + getRecentSearchItem, + searchInput, + } = SearchScreen; // # Post a message await ChannelScreen.postMessage(testMessage); @@ -147,12 +237,12 @@ async function postMessageAndSearchText(testMessage) { // # Type beginning of search term await searchInput.clearText(); - await searchInput.typeText(testMessage); + await searchInput.typeText(partialSearchTerm); // # Search text await searchInput.tapReturnKey(); // * Verify recent search item is displayed - const recentSearchItem = await SearchScreen.getRecentSearchItem(testMessage); + const {recentSearchItem} = await getRecentSearchItem(testMessage); await expect(recentSearchItem).toBeVisible(); } diff --git a/detox/e2e/test/smoke_test/user_status.e2e.js b/detox/e2e/test/smoke_test/user_status.e2e.js new file mode 100644 index 000000000..c4d22c621 --- /dev/null +++ b/detox/e2e/test/smoke_test/user_status.e2e.js @@ -0,0 +1,75 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// ******************************************************************* +// - [#] indicates a test step (e.g. # Go to a screen) +// - [*] indicates an assertion (e.g. * Check the title) +// - Use element testID when selecting an element. Create one if none. +// ******************************************************************* + +import { + MainSidebar, + SettingsSidebar, +} from '@support/ui/component'; +import {ChannelScreen} from '@support/ui/screen'; +import { + Channel, + Post, + Setup, + Team, + User, +} from '@support/server_api'; + +describe('User Status', () => { + const testMessage = Date.now().toString(); + let testUser; + let testOtherUser; + let testChannel; + + beforeAll(async () => { + const {user, team} = await Setup.apiInit(); + testUser = user; + + ({user: testOtherUser} = await User.apiCreateUser()); + await Team.apiAddUserToTeam(testOtherUser.id, team.id); + + ({channel: testChannel} = await Channel.apiCreateDirectChannel([testUser.id, testOtherUser.id])); + await User.apiLogin(testUser); + await Post.apiCreatePost({ + channelId: testChannel.id, + message: testMessage, + }); + + // # Open channel screen + await ChannelScreen.open(testUser); + }); + + afterAll(async () => { + await ChannelScreen.logout(); + }); + + it('MM-T3251 should be able to set status', async () => { + const { + closeSettingsSidebar, + getPostListPostItem, + openMainSidebar, + openSettingsSidebar, + } = ChannelScreen; + + // # Set user status to away + await openSettingsSidebar(); + await SettingsSidebar.setUserStatusTo('away'); + await closeSettingsSidebar(); + + // # Login as other user + await ChannelScreen.logout(); + await ChannelScreen.open(testOtherUser); + + // * Verify user's profile has status away as seen by other user + await openMainSidebar(); + await MainSidebar.getChannelByDisplayName(testUser.username).tap(); + const {post} = await Post.apiGetLastPostInChannel(testChannel.id); + const {postListPostItemProfilePictureUserStatus} = await getPostListPostItem(post.id, testMessage, {userId: testUser.id, userStatus: 'away'}); + await expect(postListPostItemProfilePictureUserStatus).toBeVisible(); + }); +});