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();
+ });
+});