MM-30424 Detox/E2E: Add e2e for MM-T3237, MM-T3238, MM-T3251, MM-T3257, MM-T3258 (#5188)

This commit is contained in:
Joseph Baylon 2021-03-03 08:23:32 -08:00 committed by GitHub
parent 3445e5bcaa
commit 141b5199ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 767 additions and 92 deletions

View file

@ -115,6 +115,7 @@ const AtMentionItem = (props: AtMentionItemProps) => {
size={24}
status={null}
showStatus={false}
testID={`${testID}.profile_picture`}
/>
</View>
<BotTag

View file

@ -206,6 +206,7 @@ export default class ChannelMention extends PureComponent {
<ChannelMentionItem
channelId={item}
onPress={this.completeMention}
testID={`autocomplete.channel_mention.item.${item}`}
/>
);
};

View file

@ -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}
>
<Text style={style.rowDisplayName}>{'@' + displayName}</Text>
<BotTag
@ -98,6 +100,7 @@ const ChannelMentionItem = (props) => {
style={margins}
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
type={'native'}
testID={testID}
>
<View style={style.row}>
<CompassIcon

View file

@ -28,6 +28,7 @@ exports[`Avatars should match snapshot for overflow 1`] = `
<Connect(ProfilePicture)
showStatus={false}
size={24}
testID="avatars.profile_picture"
userId="user1"
/>
</View>
@ -49,6 +50,7 @@ exports[`Avatars should match snapshot for overflow 1`] = `
<Connect(ProfilePicture)
showStatus={false}
size={24}
testID="avatars.profile_picture"
userId="user2"
/>
</View>
@ -70,6 +72,7 @@ exports[`Avatars should match snapshot for overflow 1`] = `
<Connect(ProfilePicture)
showStatus={false}
size={24}
testID="avatars.profile_picture"
userId="user3"
/>
</View>
@ -147,6 +150,7 @@ exports[`Avatars should match snapshot for single avatar 1`] = `
<Connect(ProfilePicture)
showStatus={false}
size={24}
testID="avatars.profile_picture"
userId="user1"
/>
</View>

View file

@ -108,6 +108,7 @@ export default class Avatars extends PureComponent<AvatarsProps> {
userId={userId}
size={ViewTypes.AVATAR_LIST_PICTURE_SIZE}
showStatus={false}
testID='avatars.profile_picture'
/>
</View>
))}

View file

@ -92,6 +92,7 @@ class ChannelIntro extends PureComponent {
iconSize={48}
statusBorderWidth={2}
statusSize={25}
testID='channel_intro.profile_picture'
/>
</TouchableWithFeedback>
));

View file

@ -29,6 +29,7 @@ exports[`UserListRow should match snapshot 1`] = `
<Connect(ProfilePicture)
iconSize={24}
size={32}
testID="custom_list.user_item.21345.profile_picture"
userId="21345"
/>
</View>
@ -164,6 +165,7 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
<Connect(ProfilePicture)
iconSize={24}
size={32}
testID="custom_list.user_item.21345.profile_picture"
userId="21345"
/>
</View>
@ -297,6 +299,7 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
<Connect(ProfilePicture)
iconSize={24}
size={32}
testID="custom_list.user_item.21345.profile_picture"
userId="21345"
/>
</View>
@ -443,6 +446,7 @@ exports[`UserListRow should match snapshot for guest user 1`] = `
<Connect(ProfilePicture)
iconSize={24}
size={32}
testID="custom_list.user_item.21345.profile_picture"
userId="21345"
/>
</View>

View file

@ -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 (
<View style={style.container}>
@ -82,6 +83,7 @@ export default class UserListRow extends React.PureComponent {
userId={id}
size={32}
iconSize={24}
testID={profilePictureTestID}
/>
</View>
<View

View file

@ -15,6 +15,7 @@ export default class FormattedTime extends React.PureComponent {
children: PropTypes.func,
hour12: PropTypes.bool,
style: CustomPropTypes.Style,
testID: PropTypes.string,
};
getFormattedTime = () => {
@ -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 <Text style={style}>{formattedTime}</Text>;
return (
<Text
style={style}
testID={testID}
>
{formattedTime}
</Text>
);
}
}

View file

@ -47,6 +47,7 @@ exports[`PostHeader should match snapshot when just a base post 1`] = `
"paddingVertical": 2,
}
}
testID="post_header.display_name"
>
John Smith
</Text>
@ -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
</Text>
@ -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
</Text>
@ -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"
/>
</View>
<FormattedTime
@ -308,6 +315,7 @@ exports[`PostHeader should match snapshot when post is from system message 1`] =
"opacity": 0.5,
}
}
testID="post_header.date_time"
timeZone=""
value={0}
/>
@ -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
</Text>
@ -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
</Text>
@ -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
</Text>
@ -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
</Text>
@ -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
</Text>
@ -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}
/>

View file

@ -165,6 +165,7 @@ export default class PostHeader extends PureComponent {
style={style.displayName}
ellipsizeMode={'tail'}
numberOfLines={1}
testID='post_header.display_name'
>
{name}
</Text>
@ -177,6 +178,7 @@ export default class PostHeader extends PureComponent {
id='post_info.system'
defaultMessage='System'
style={[style.displayName]}
testID='post_header.display_name'
/>
</View>
);
@ -191,6 +193,7 @@ export default class PostHeader extends PureComponent {
style={style.displayName}
ellipsizeMode={'tail'}
numberOfLines={1}
testID='post_header.display_name'
>
{displayName}
</Text>
@ -204,6 +207,7 @@ export default class PostHeader extends PureComponent {
id='channel_loader.someone'
defaultMessage='Someone'
style={style.displayName}
testID='post_header.display_name'
/>
</View>
);
@ -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'
/>
);
}

View file

@ -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'
/>
);

View file

@ -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 (
<View style={[style.container, containerStyle]}>
<View
style={[style.container, containerStyle]}
testID={`${testID}.${user.id}`}
>
{image}
{(showStatus || edit) && (user && !user.is_bot) &&
<View style={[style.statusWrapper, statusStyle, {borderRadius: this.props.statusSize / 2}]}>

View file

@ -51,6 +51,7 @@ export default class UserInfo extends PureComponent {
id={i18nId}
defaultMessage={defaultMessage}
style={style.label}
testID={`user_status.label.${status}`}
/>
);
}

View file

@ -47,6 +47,7 @@ export default class UserInfo extends PureComponent {
iconSize={28}
showStatus={false}
userId={user.id}
testID={`${testID}.profile_picture`}
/>
<View style={style.wrapper}>
<View>

View file

@ -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"
/>
`;

View file

@ -50,6 +50,7 @@ export default class UserStatus extends PureComponent {
<CompassIcon
name={iconName}
style={{fontSize: size, color: iconColor}}
testID={`user_status.icon.${status}`}
/>
);
}

View file

@ -17,6 +17,7 @@ exports[`edit_profile should match snapshot 1`] = `
size={153}
statusBorderWidth={6}
statusSize={36}
testID="edit_profile.profile_picture"
/>
</View>
`;

View file

@ -525,6 +525,7 @@ export default class EditProfile extends PureComponent {
edit={!profilePictureDisabled}
imageUri={uri}
profileImageRemove={profileImageRemove}
testID='edit_profile.profile_picture'
/>
);

View file

@ -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 {

View file

@ -40,6 +40,7 @@ exports[`Search RecentItem should match snapshot and respond to events 1`] = `
"width": 50,
}
}
testID="search.recent_item.test.remove.button"
>
<CompassIcon
color="rgba(61,60,64,0.6)"

View file

@ -52,6 +52,7 @@ export default class RecentItem extends PureComponent {
<TouchableOpacity
onPress={this.handleRemove}
style={style.recentRemove}
testID={`${testID}.remove.button`}
>
<CompassIcon
name='close-circle-outline'

View file

@ -57,6 +57,7 @@ export default class ClockDisplay extends ClockDisplayBase {
}),
value: 'false',
checked: newMilitaryTime === 'false',
testID: 'clock_display_settings.normal_clock.action',
}, {
label: intl.formatMessage({
id: 'user.settings.display.militaryClock',
@ -64,6 +65,7 @@ export default class ClockDisplay extends ClockDisplayBase {
}),
value: 'true',
checked: newMilitaryTime === 'true',
testID: 'clock_display_settings.military_clock.action',
}];
return (
@ -73,7 +75,10 @@ export default class ClockDisplay extends ClockDisplayBase {
visible={showModal}
onRequestClose={this.closeModal}
>
<View style={style.modalOverlay}>
<View
style={style.modalOverlay}
testID='clock_display_settings.clock.modal'
>
<View style={style.modal}>
<View style={style.modalBody}>
<View style={style.modalTitleContainer}>
@ -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'
/>
</TouchableOpacity>
<View style={style.modalFooterButtonSpacer}/>
@ -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'
/>
</TouchableOpacity>
</View>
@ -131,7 +138,10 @@ export default class ClockDisplay extends ClockDisplayBase {
const style = getStyleSheet(theme);
return (
<View style={style.container}>
<View
style={style.container}
testID='clock_display_settings.screen'
>
<StatusBar/>
<View style={style.wrapper}>
{this.renderClockDisplayModal(style)}

View file

@ -26,6 +26,7 @@ export default class ClockDisplay extends ClockDisplayBase {
<SafeAreaView
edges={['left', 'right']}
style={style.container}
testID='clock_display_settings.screen'
>
<StatusBar/>
<View style={style.wrapper}>
@ -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}
/>
<View style={style.divider}/>
@ -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}
/>
</Section>

View file

@ -14,6 +14,7 @@ exports[`DisplaySettings should match snapshot 1`] = `
"flex": 1,
}
}
testID="display_settings.screen"
>
<Connect(StatusBar) />
<View
@ -42,6 +43,7 @@ exports[`DisplaySettings should match snapshot 1`] = `
onPress={[Function]}
separator={false}
showArrow={true}
testID="display_settings.clock_display.action"
theme={
Object {
"awayIndicator": "#ffbc42",
@ -98,6 +100,7 @@ exports[`DisplaySettings should match snapshot on Tablet devices 1`] = `
"flex": 1,
}
}
testID="display_settings.screen"
>
<Connect(StatusBar) />
<View
@ -165,6 +168,7 @@ exports[`DisplaySettings should match snapshot on Tablet devices 1`] = `
onPress={[Function]}
separator={false}
showArrow={true}
testID="display_settings.clock_display.action"
theme={
Object {
"awayIndicator": "#ffbc42",

View file

@ -100,6 +100,7 @@ export default class DisplaySettings extends PureComponent {
onPress={this.goToTimezoneSettings}
separator={false}
showArrow={showArrow}
testID='display_settings.timezone.action'
theme={theme}
/>
);
@ -124,6 +125,7 @@ export default class DisplaySettings extends PureComponent {
<SafeAreaView
edges={['left', 'right']}
style={style.container}
testID='display_settings.screen'
>
<StatusBar/>
<View style={style.wrapper}>
@ -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}

View file

@ -36,6 +36,7 @@ exports[`user_profile should match snapshot 1`] = `
size={153}
statusBorderWidth={6}
statusSize={36}
testID="user_profile.profile_picture"
userId="4hzdnk6mg7gepe7yze6m3domnc"
/>
<Text

View file

@ -344,6 +344,7 @@ export default class UserProfile extends PureComponent {
iconSize={104}
statusBorderWidth={6}
statusSize={36}
testID='user_profile.profile_picture'
/>
{this.getDisplayName()}
<Text style={style.username}>{`@${user.username}`}</Text>

View file

@ -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;

View file

@ -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) => {

View file

@ -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);
}

View file

@ -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,

View file

@ -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),
};
}
}

View file

@ -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,
};
}

View file

@ -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;

View file

@ -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)),
};
}
}

View file

@ -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();

View file

@ -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) => {

View file

@ -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;

View file

@ -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;

View file

@ -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,

View file

@ -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,
};
}

View file

@ -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',

View file

@ -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}`;

View file

@ -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) => {

View file

@ -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 () => {

View file

@ -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 () => {

View file

@ -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 () => {

View file

@ -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) => {

View file

@ -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,
};
}

View file

@ -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) => {

View file

@ -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);
}

View file

@ -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);
});
});

View file

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

View file

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