From f22d63644f3989f90951af03c72ee461db07cd28 Mon Sep 17 00:00:00 2001 From: Joseph Baylon Date: Tue, 27 Apr 2021 16:46:50 -0700 Subject: [PATCH] MM-35071 Detox/E2E: Add e2e for MM-T287, MM-T290, MM-T300, MM-T2350, MM-T3151, MM-T3472 (#5347) * MM-35071 Detox/E2E: Add e2e for MM-T287, MM-T290, MM-T2350, MM-T3151, MM-T3472 * Revert to original generic android Co-authored-by: Mattermod --- app/components/at_mention/at_mention.js | 1 + app/components/channel_intro/channel_intro.js | 1 + app/components/post/post.js | 1 + .../settings/settings_sidebar_base.js | 2 +- .../widgets/settings/text_setting.js | 14 +- app/screens/edit_profile/edit_profile.js | 8 + .../reaction_list/reaction_row/index.js | 1 + .../__snapshots__/user_profile.test.js.snap | 1 + app/screens/user_profile/user_profile.js | 21 +- detox/e2e/support/server_api/user.js | 1 + detox/e2e/support/ui/screen/edit_profile.js | 72 +++++++ detox/e2e/support/ui/screen/index.js | 2 + detox/e2e/support/ui/screen/user_profile.js | 35 +++ .../account_settings/advanced_settings.e2e.js | 49 +++++ .../test/account_settings/user_profile.e2e.js | 204 ++++++++++++++++++ 15 files changed, 406 insertions(+), 7 deletions(-) create mode 100644 detox/e2e/support/ui/screen/edit_profile.js create mode 100644 detox/e2e/test/account_settings/advanced_settings.e2e.js create mode 100644 detox/e2e/test/account_settings/user_profile.e2e.js diff --git a/app/components/at_mention/at_mention.js b/app/components/at_mention/at_mention.js index aa4be87ec..0dff16811 100644 --- a/app/components/at_mention/at_mention.js +++ b/app/components/at_mention/at_mention.js @@ -64,6 +64,7 @@ export default class AtMention extends React.PureComponent { leftButtons: [{ id: 'close-settings', icon: this.closeButton, + testID: 'close.settings.button', }], }, }; diff --git a/app/components/channel_intro/channel_intro.js b/app/components/channel_intro/channel_intro.js index c401ee08a..b28e638fb 100644 --- a/app/components/channel_intro/channel_intro.js +++ b/app/components/channel_intro/channel_intro.js @@ -53,6 +53,7 @@ class ChannelIntro extends PureComponent { leftButtons: [{ id: 'close-settings', icon: this.closeButton, + testID: 'close.settings.button', }], }, }; diff --git a/app/components/post/post.js b/app/components/post/post.js index 462e7ed33..4edbcee59 100644 --- a/app/components/post/post.js +++ b/app/components/post/post.js @@ -109,6 +109,7 @@ export default class Post extends PureComponent { leftButtons: [{ id: 'close-settings', icon: this.closeButton, + testID: 'close.settings.button', }], }, }; diff --git a/app/components/sidebars/settings/settings_sidebar_base.js b/app/components/sidebars/settings/settings_sidebar_base.js index 8e1de4ce5..ce99d76d6 100644 --- a/app/components/sidebars/settings/settings_sidebar_base.js +++ b/app/components/sidebars/settings/settings_sidebar_base.js @@ -139,9 +139,9 @@ export default class SettingsSidebarBase extends PureComponent { const options = { topBar: { leftButtons: [{ - testID: 'close.settings.button', id: 'close-settings', icon: this.closeButton, + testID: 'close.settings.button', }], }, }; diff --git a/app/components/widgets/settings/text_setting.js b/app/components/widgets/settings/text_setting.js index 30170c5a1..ae57ef2cd 100644 --- a/app/components/widgets/settings/text_setting.js +++ b/app/components/widgets/settings/text_setting.js @@ -50,6 +50,7 @@ export default class TextSetting extends PureComponent { 'url', ]), secureTextEntry: PropTypes.bool, + testID: PropTypes.string, }; static defaultProps = { @@ -78,6 +79,7 @@ export default class TextSetting extends PureComponent { value, multiline, secureTextEntry, + testID, } = this.props; const style = getStyleSheet(theme); @@ -88,10 +90,17 @@ export default class TextSetting extends PureComponent { style={style.title} id={label.id} defaultMessage={label.defaultMessage} + testID={`${testID}.label_content`} /> ); } else if (typeof label === 'string') { - labelContent = {label}; + labelContent = ( + + {label} + ); } let optionalContent; @@ -148,7 +157,7 @@ export default class TextSetting extends PureComponent { const noediting = disabled ? style.disabled : null; return ( - + {labelContent} {asterisk} @@ -172,6 +181,7 @@ export default class TextSetting extends PureComponent { keyboardType={keyboardType} secureTextEntry={secureTextEntry} keyboardAppearance={getKeyboardAppearanceFromTheme(theme)} + testID={`${testID}.input`} /> diff --git a/app/screens/edit_profile/edit_profile.js b/app/screens/edit_profile/edit_profile.js index c32051425..7621f21e3 100644 --- a/app/screens/edit_profile/edit_profile.js +++ b/app/screens/edit_profile/edit_profile.js @@ -109,6 +109,7 @@ export default class EditProfile extends PureComponent { id: 'update-profile', enabled: false, showAsAction: 'always', + testID: 'edit_profile.save.button', }; constructor(props, context) { @@ -334,6 +335,7 @@ export default class EditProfile extends PureComponent { theme={theme} value={firstName} isLandscape={isLandscape} + testID='edit_profile.text_setting.first_name' /> ); }; @@ -357,6 +359,7 @@ export default class EditProfile extends PureComponent { theme={theme} value={lastName} isLandscape={isLandscape} + testID='edit_profile.text_setting.last_name' /> ); @@ -382,6 +385,7 @@ export default class EditProfile extends PureComponent { theme={theme} value={username} isLandscape={isLandscape} + testID='edit_profile.text_setting.username' /> ); }; @@ -444,6 +448,7 @@ export default class EditProfile extends PureComponent { theme={theme} value={email} isLandscape={isLandscape} + testID='edit_profile.text_setting.email' /> ); @@ -469,6 +474,7 @@ export default class EditProfile extends PureComponent { value={nickname} isLandscape={isLandscape} optional={true} + testID='edit_profile.text_setting.nickname' /> ); }; @@ -493,6 +499,7 @@ export default class EditProfile extends PureComponent { value={position} isLandscape={isLandscape} optional={true} + testID='edit_profile.text_setting.position' /> ); }; @@ -602,6 +609,7 @@ export default class EditProfile extends PureComponent { {displayError} diff --git a/app/screens/reaction_list/reaction_row/index.js b/app/screens/reaction_list/reaction_row/index.js index 76b305448..ae3a3242a 100644 --- a/app/screens/reaction_list/reaction_row/index.js +++ b/app/screens/reaction_list/reaction_row/index.js @@ -53,6 +53,7 @@ export default class ReactionRow extends React.PureComponent { leftButtons: [{ id: 'close-settings', icon: this.closeButton, + testID: 'close.settings.button', }], }, }; 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 0f8eb72a5..712e44ee6 100644 --- a/app/screens/user_profile/__snapshots__/user_profile.test.js.snap +++ b/app/screens/user_profile/__snapshots__/user_profile.test.js.snap @@ -49,6 +49,7 @@ exports[`user_profile should match snapshot 1`] = ` "marginTop": 15, } } + testID="user_profile.username" > @fred diff --git a/app/screens/user_profile/user_profile.js b/app/screens/user_profile/user_profile.js index ad0fd92ef..aa2e32d44 100644 --- a/app/screens/user_profile/user_profile.js +++ b/app/screens/user_profile/user_profile.js @@ -61,6 +61,7 @@ export default class UserProfile extends PureComponent { rightButton = { id: 'edit-profile', showAsAction: 'always', + testID: 'user_profile.edit.button', }; constructor(props, context) { @@ -117,15 +118,20 @@ export default class UserProfile extends PureComponent { if (displayName && (config.ShowFullName === 'true' || user.is_bot || showGuest)) { return ( - + {displayName} @@ -198,12 +204,14 @@ export default class UserProfile extends PureComponent { style={style.header} testID='user_profile.timezone_block.local_time.label' /> - + @@ -364,7 +372,12 @@ export default class UserProfile extends PureComponent { testID='user_profile.profile_picture' /> {this.getDisplayName()} - {`@${user.username}`} + + {`@${user.username}`} + {this.renderDetailsBlock(style)} diff --git a/detox/e2e/support/server_api/user.js b/detox/e2e/support/server_api/user.js index b608eab8f..28dd7867c 100644 --- a/detox/e2e/support/server_api/user.js +++ b/detox/e2e/support/server_api/user.js @@ -188,6 +188,7 @@ function generateRandomUser(prefix) { first_name: `First${randomId}`, last_name: `Last${randomId}`, nickname: `Nickname${randomId}`, + position: `Position${randomId}`, }; } diff --git a/detox/e2e/support/ui/screen/edit_profile.js b/detox/e2e/support/ui/screen/edit_profile.js new file mode 100644 index 000000000..0b33bbd13 --- /dev/null +++ b/detox/e2e/support/ui/screen/edit_profile.js @@ -0,0 +1,72 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {ProfilePicture} from '@support/ui/component'; +import {UserProfileScreen} from '@support/ui/screen'; + +class EditProfileScreen { + testID = { + editProfilePicturePrefix: 'edit_profile.profile_picture.', + editProfileScreen: 'edit_profile.screen', + editProfileScrollView: 'edit_profile.scroll_view', + backButton: 'screen.back.button', + saveButton: 'edit_profile.save.button', + editProfileError: 'edit_profile.error.text', + emailInput: 'edit_profile.text_setting.email.input', + emailLabel: 'edit_profile.text_setting.email.label', + firstNameInput: 'edit_profile.text_setting.first_name.input', + firstNameLabel: 'edit_profile.text_setting.first_name.label', + lastNameInput: 'edit_profile.text_setting.last_name.input', + lastNameLabel: 'edit_profile.text_setting.last_name.label', + nicknameInput: 'edit_profile.text_setting.nickname.input', + nicknameLabel: 'edit_profile.text_setting.nickname.label', + positionInput: 'edit_profile.text_setting.position.input', + positionLabel: 'edit_profile.text_setting.position.label', + usernameInput: 'edit_profile.text_setting.username.input', + usernameLabel: 'edit_profile.text_setting.username.label', + } + + editProfileScreen = element(by.id(this.testID.editProfileScreen)); + editProfileScrollView = element(by.id(this.testID.editProfileScrollView)); + backButton = element(by.id(this.testID.backButton)); + saveButton = element(by.id(this.testID.saveButton)); + editProfileError = element(by.id(this.testID.editProfileError)); + emailInput = element(by.id(this.testID.emailInput)); + emailLabel = element(by.id(this.testID.emailLabel)); + firstNameInput = element(by.id(this.testID.firstNameInput)); + firstNameLabel = element(by.id(this.testID.firstNameLabel)); + lastNameInput = element(by.id(this.testID.lastNameInput)); + lastNameLabel = element(by.id(this.testID.lastNameLabel)); + nicknameInput = element(by.id(this.testID.nicknameInput)); + nicknameLabel = element(by.id(this.testID.nicknameLabel)); + positionInput = element(by.id(this.testID.positionInput)); + positionLabel = element(by.id(this.testID.positionLabel)); + usernameInput = element(by.id(this.testID.usernameInput)); + usernameLabel = element(by.id(this.testID.usernameLabel)); + + getProfilePicture = (userId) => { + const profilePictureItemMatcher = ProfilePicture.getProfilePictureItemMatcher(this.testID.editProfilePicturePrefix, userId); + return element(profilePictureItemMatcher); + } + + toBeVisible = async () => { + await expect(this.editProfileScreen).toBeVisible(); + + return this.editProfileScreen; + } + + open = async () => { + // # Open edit profile screen + await UserProfileScreen.editButton.tap(); + + return this.toBeVisible(); + } + + back = async () => { + await this.backButton.tap(); + await expect(this.editProfileScreen).not.toBeVisible(); + } +} + +const editProfileScreen = new EditProfileScreen(); +export default editProfileScreen; diff --git a/detox/e2e/support/ui/screen/index.js b/detox/e2e/support/ui/screen/index.js index 9759397f1..833a5f06e 100644 --- a/detox/e2e/support/ui/screen/index.js +++ b/detox/e2e/support/ui/screen/index.js @@ -14,6 +14,7 @@ import CreateChannelScreen from './create_channel'; import DisplaySettingsScreen from './display_settings'; import EditChannelScreen from './edit_channel'; import EditPostScreen from './edit_post'; +import EditProfileScreen from './edit_profile'; import GeneralSettingsScreen from './general_settings'; import LoginScreen from './login'; import LongPostScreen from './long_post'; @@ -48,6 +49,7 @@ export { DisplaySettingsScreen, EditChannelScreen, EditPostScreen, + EditProfileScreen, GeneralSettingsScreen, LoginScreen, LongPostScreen, diff --git a/detox/e2e/support/ui/screen/user_profile.js b/detox/e2e/support/ui/screen/user_profile.js index 0f570b865..fe61071e1 100644 --- a/detox/e2e/support/ui/screen/user_profile.js +++ b/detox/e2e/support/ui/screen/user_profile.js @@ -1,10 +1,22 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import { + ProfilePicture, + SettingsSidebar, +} from '@support/ui/component'; + class UserProfileScreen { testID = { + userProfilePicturePrefix: 'user_profile.profile_picture.', userProfileScreen: 'user_profile.screen', userProfileScrollView: 'user_profile.scroll_view', + closeSettingsButton: 'close.settings.button', + editButton: 'user_profile.edit.button', + userProfileBotTag: 'user_profile.bot_tag', + userProfileDisplayName: 'user_profile.display_name', + userProfileGuestTag: 'user_profile.bot_tag', + userProfileUsername: 'user_profile.username', emailLabel: 'user_profile.display_block.email.label', emailValue: 'user_profile.display_block.email.value', firstNameLabel: 'user_profile.display_block.first_name.label', @@ -23,6 +35,12 @@ class UserProfileScreen { userProfileScreen = element(by.id(this.testID.userProfileScreen)); userProfileScrollView = element(by.id(this.testID.userProfileScrollView)); + closeSettingsButton = element(by.id(this.testID.closeSettingsButton)); + editButton = element(by.id(this.testID.editButton)); + userProfileBotTag = element(by.id(this.testID.userProfileBotTag)); + userProfileDisplayName = element(by.id(this.testID.userProfileDisplayName)); + userProfileGuestTag = element(by.id(this.testID.userProfileGuestTag)); + userProfileUsername = element(by.id(this.testID.userProfileUsername)); emailLabel = element(by.id(this.testID.emailLabel)); emailValue = element(by.id(this.testID.emailValue)); firstNameLabel = element(by.id(this.testID.firstNameLabel)); @@ -38,11 +56,28 @@ class UserProfileScreen { additionalOptionsAction = element(by.id(this.testID.additionalOptionsAction)); sendMessageAction = element(by.id(this.testID.sendMessageAction)); + getProfilePicture = (userId) => { + const profilePictureItemMatcher = ProfilePicture.getProfilePictureItemMatcher(this.testID.userProfilePicturePrefix, userId); + return element(profilePictureItemMatcher); + } + toBeVisible = async () => { await expect(this.userProfileScreen).toBeVisible(); return this.userProfileScreen; } + + open = async () => { + // # Open user profile screen + await SettingsSidebar.userInfoAction.tap(); + + return this.toBeVisible(); + } + + close = async () => { + await this.closeSettingsButton.tap(); + await expect(this.userProfileScreen).not.toBeVisible(); + } } const userProfileScreen = new UserProfileScreen(); diff --git a/detox/e2e/test/account_settings/advanced_settings.e2e.js b/detox/e2e/test/account_settings/advanced_settings.e2e.js new file mode 100644 index 000000000..92cc62b91 --- /dev/null +++ b/detox/e2e/test/account_settings/advanced_settings.e2e.js @@ -0,0 +1,49 @@ +// 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 { + AdvancedSettingsScreen, + ChannelScreen, + GeneralSettingsScreen, +} from '@support/ui/screen'; +import { + Channel, + Setup, +} from '@support/server_api'; + +describe('Advanced Settings', () => { + let townSquareChannel; + + beforeAll(async () => { + const {team, user} = await Setup.apiInit(); + + ({channel: townSquareChannel} = await Channel.apiGetChannelByName(team.name, 'town-square')); + + // # Open channel screen + await ChannelScreen.open(user); + }); + + afterAll(async () => { + await ChannelScreen.logout(); + }); + + it('MM-T300 should be able to delete documents and data', async () => { + // # Open advanced settings screen + await ChannelScreen.openSettingsSidebar(); + await GeneralSettingsScreen.open(); + await AdvancedSettingsScreen.open(); + + // # Delete documents and data + await AdvancedSettingsScreen.deleteDocumentsAndData(); + + // * Verify redirects to town square + await ChannelScreen.toBeVisible(); + await expect(ChannelScreen.channelNavBarTitle).toHaveText(townSquareChannel.display_name); + }); +}); diff --git a/detox/e2e/test/account_settings/user_profile.e2e.js b/detox/e2e/test/account_settings/user_profile.e2e.js new file mode 100644 index 000000000..17b61cf4f --- /dev/null +++ b/detox/e2e/test/account_settings/user_profile.e2e.js @@ -0,0 +1,204 @@ +// 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 { + ChannelScreen, + EditProfileScreen, + UserProfileScreen, +} from '@support/ui/screen'; +import { + Channel, + Post, + Setup, + System, +} from '@support/server_api'; + +describe('User Profile', () => { + let testUser; + let townSquareChannel; + + beforeAll(async () => { + // # Enable experimental timezone + System.apiUpdateConfig({DisplaySettings: {ExperimentalTimezone: true}}); + + const {team, user} = await Setup.apiInit(); + testUser = user; + + ({channel: townSquareChannel} = await Channel.apiGetChannelByName(team.name, 'town-square')); + + // # Open channel screen + await ChannelScreen.open(testUser); + }); + + afterAll(async () => { + await ChannelScreen.logout(); + }); + + it('MM-T2350 should be able to view profile', async () => { + const { + firstNameValue, + getProfilePicture, + lastNameValue, + localTimeValue, + nicknameValue, + positionValue, + sendMessageAction, + userProfileDisplayName, + userProfileScrollView, + userProfileUsername, + } = UserProfileScreen; + + // # Open user profile screen + await ChannelScreen.openSettingsSidebar(); + await UserProfileScreen.open(); + + // * Verify user profile details + await expect(getProfilePicture(testUser.id)).toBeVisible(); + await expect(userProfileDisplayName).toHaveText(testUser.username); + await expect(userProfileUsername).toHaveText(`@${testUser.username}`); + await expect(firstNameValue).toHaveText(testUser.first_name); + await expect(lastNameValue).toHaveText(testUser.last_name); + await expect(nicknameValue).toHaveText(testUser.nickname); + await expect(positionValue).toHaveText(testUser.position); + await userProfileScrollView.scrollTo('bottom'); + await expect(localTimeValue).toBeVisible(); + await expect(sendMessageAction).toBeVisible(); + + // # Go back to channel + await UserProfileScreen.close(); + }); + + it('MM-T3151_1 should be able to view first name and last name when ShowFullName is enabled', async () => { + const { + firstNameValue, + lastNameValue, + } = UserProfileScreen; + + // # Enable show full name + System.apiUpdateConfig({PrivacySettings: {ShowFullName: true}}); + + // # Open user profile screen + await ChannelScreen.openSettingsSidebar(); + await UserProfileScreen.open(); + + await expect(firstNameValue).toBeVisible(); + await expect(lastNameValue).toBeVisible(); + + // # Go back to channel + await UserProfileScreen.close(); + }); + + it('MM-T3151_2 should not be able to view first name and last name when ShowFullName is disabled', async () => { + const { + firstNameValue, + lastNameValue, + } = UserProfileScreen; + + // # Enable show full name + System.apiUpdateConfig({PrivacySettings: {ShowFullName: false}}); + + // # Open user profile screen + await ChannelScreen.openSettingsSidebar(); + await UserProfileScreen.open(); + + await expect(firstNameValue).not.toBeVisible(); + await expect(lastNameValue).not.toBeVisible(); + + // # Go back to channel + await UserProfileScreen.close(); + }); + + it('MM-T287 should be able to edit profile from own profile pop-over', async () => { + const { + saveButton, + usernameInput, + } = EditProfileScreen; + const { + postMessage, + getPostListPostItem, + } = ChannelScreen; + + // # Post message + const message = Date.now().toString(); + await postMessage(message); + + // # Open edit profile screen from channel + const {post} = await Post.apiGetLastPostInChannel(townSquareChannel.id); + const {postListPostItemProfilePicture} = await getPostListPostItem(post.id, message, {userId: testUser.id}); + await postListPostItemProfilePicture.tap(); + await UserProfileScreen.toBeVisible(); + await EditProfileScreen.open(); + + // # Update username + const updatedUsername = `${testUser.username}-123`; + await usernameInput.clearText(); + await usernameInput.typeText(updatedUsername); + await saveButton.tap(); + + // * Verify username is updated + await UserProfileScreen.toBeVisible(); + await expect(UserProfileScreen.userProfileUsername).toHaveText(`@${updatedUsername}`); + + // # Go back to channel + await UserProfileScreen.close(); + }); + + it('MM-T290 should display error on invalid username', async () => { + const { + editProfileError, + saveButton, + usernameInput, + } = EditProfileScreen; + + // # Open edit profile screen + await ChannelScreen.openSettingsSidebar(); + await UserProfileScreen.open(); + await EditProfileScreen.open(); + + // # Attempt to save with invalid username + const invalidUsername = `${testUser.username}+new`; + await usernameInput.clearText(); + await usernameInput.typeText(invalidUsername); + await saveButton.tap(); + + // * Verify invalid username error is displayed + await expect(editProfileError).toHaveText('Username must begin with a letter, and contain between 3 to 22 lowercase characters made up of numbers, letters, and the symbols ".", "-", and "_".'); + + // # Go back to channel + await EditProfileScreen.back(); + await UserProfileScreen.close(); + }); + + it('MM-T3472 should be able add nickname to profile', async () => { + const { + editProfileScrollView, + nicknameInput, + saveButton, + } = EditProfileScreen; + + // # Open edit profile screen + await ChannelScreen.openSettingsSidebar(); + await UserProfileScreen.open(); + await EditProfileScreen.open(); + + // # Add nickname + const nickname = Date.now().toString(); + await editProfileScrollView.scrollTo('bottom'); + await nicknameInput.clearText(); + await nicknameInput.typeText(nickname); + await saveButton.tap(); + + // * Verify nickname is added + await UserProfileScreen.toBeVisible(); + await expect(UserProfileScreen.nicknameValue).toHaveText(nickname); + + // # Go back to channel + await UserProfileScreen.close(); + }); +});