Detox/E2E: Create Channel and Channel Post List e2e in Gekidou

This commit is contained in:
Joseph Baylon
2022-04-12 11:50:05 -07:00
parent 9feb3446a8
commit 2e2f49f8c3
54 changed files with 1154 additions and 35 deletions

1
.gitignore vendored
View File

@@ -6,7 +6,6 @@ server.PID
mattermost.keystore
tmp/
.env
env.d.ts
*/**/compass-icons.ttf

View File

@@ -41,7 +41,7 @@ function SendButton({
sendMessage,
}: Props) {
const theme = useTheme();
const sendButtonTestID = `${testID}.send.button`;
const sendButtonTestID = disabled ? `${testID}.send.button.disabled` : `${testID}.send.button`;
const style = getStyleSheet(theme);
const viewStyle = useMemo(() => {

View File

@@ -363,7 +363,7 @@ const PostList = ({
scrollEventThrottle={60}
style={styles.flex}
viewabilityConfig={VIEWABILITY_CONFIG}
testID={testID}
testID={`${testID}.flat_list`}
/>
</PostListRefreshControl>
{showMoreMessages &&

View File

@@ -93,6 +93,7 @@ const SectionItem = ({testID = 'sectionItem', action, actionType, actionValue, l
<Switch
onValueChange={action}
value={selected}
testID={`${testID}.toggled.${selected}`}
/>
);
} else if (actionType === ActionTypes.ARROW) {

View File

@@ -32,6 +32,7 @@ export default function AddTeamSlideUp({otherTeams, canCreateTeams, showTitle =
onPress={onPressCreate}
showButton={canCreateTeams}
showTitle={showTitle}
testID='add_team_slide_up'
title={intl.formatMessage({id: 'mobile.add_team.join_team', defaultMessage: 'Join Another Team'})}
>
<TeamList teams={otherTeams}/>

View File

@@ -1,8 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {RUNNING_E2E} from '@env';
import Emm from '@mattermost/react-native-emm';
import {Alert, Linking, Platform} from 'react-native';
import {Alert, Linking, LogBox, Platform} from 'react-native';
import {Notifications} from 'react-native-notifications';
import {appEntry, pushNotificationEntry, upgradeEntry} from '@actions/remote/entry';
@@ -219,3 +220,9 @@ export const getLaunchPropsFromNotification = async (notification: NotificationW
return launchProps;
};
// Ignore all notifications if running e2e
const isRunningE2e = RUNNING_E2E === 'true';
// eslint-disable-next-line no-console
console.log(`RUNNING_E2E: ${RUNNING_E2E}, isRunningE2e: ${isRunningE2e}`);
LogBox.ignoreAllLogs(isRunningE2e);

View File

@@ -23,9 +23,10 @@ type SlideUpPanelProps = {
initialSnapIndex?: number;
renderContent: () => ReactNode;
snapPoints?: Array<string | number>;
testID?: string;
}
const BottomSheet = ({closeButtonId, componentId, initialSnapIndex = 0, renderContent, snapPoints = ['90%', '50%', 50]}: SlideUpPanelProps) => {
const BottomSheet = ({closeButtonId, componentId, initialSnapIndex = 0, renderContent, snapPoints = ['90%', '50%', 50], testID}: SlideUpPanelProps) => {
const sheetRef = useRef<RNBottomSheet>(null);
const dimensions = useWindowDimensions();
const isTablet = useIsTablet();
@@ -121,6 +122,7 @@ const BottomSheet = ({closeButtonId, componentId, initialSnapIndex = 0, renderCo
width: isTablet ? '100%' : Math.min(dimensions.width, 450),
alignSelf: 'center',
}}
testID={`${testID}.screen`}
>
{renderContent()}
</View>

View File

@@ -66,6 +66,7 @@ export default function DropdownSlideup({
<BottomSheetContent
showButton={false}
showTitle={!isTablet}
testID='dropdown_slideup'
title={intl.formatMessage({id: 'browse_channels.dropdownTitle', defaultMessage: 'Show'})}
>
<SlideUpPanelItem

View File

@@ -144,6 +144,7 @@ const Channel = ({channelId, componentId, displayName, isOwnDirectMessage, membe
keyboardTracker={postDraftRef}
scrollViewNativeID={channelId}
accessoriesContainerID={ACCESSORIES_CONTAINER_NATIVE_ID}
testID='channel.post_draft'
/>
</>
}

View File

@@ -224,9 +224,10 @@ export default function ChannelInfoForm({
<SafeAreaView
edges={['bottom', 'left', 'right']}
style={styles.container}
testID='create_or_edit_channel.screen'
>
<KeyboardAwareScrollView
testID={'edit_channel_info.scrollview'}
testID={'create_or_edit_channel.scrollview'}
// @ts-expect-error legacy ref
ref={scrollViewRef}
@@ -244,7 +245,7 @@ export default function ChannelInfoForm({
<View>
{showSelector && (
<SectionItem
testID='makePrivate'
testID='channel_info_form.make_private'
label={makePrivateLabel}
description={makePrivateDescription}
action={handlePress}
@@ -269,7 +270,7 @@ export default function ChannelInfoForm({
returnKeyType='next'
showErrorIcon={false}
spellCheck={false}
testID='edit_channel_info.displayname.input'
testID='channel_info_form.display_name.input'
value={displayName}
ref={nameInput}
containerStyle={styles.textInput}
@@ -288,7 +289,7 @@ export default function ChannelInfoForm({
returnKeyType='next'
showErrorIcon={false}
spellCheck={false}
testID='edit_channel_info.purpose.input'
testID='channel_info_form.purpose.input'
value={purpose}
ref={purposeInput}
containerStyle={styles.textInput}
@@ -298,6 +299,7 @@ export default function ChannelInfoForm({
style={styles.helpText}
id='channel_modal.descriptionHelp'
defaultMessage='Describe how this channel should be used.'
testID='channel_info_form.purpose.description'
/>
</>
)}
@@ -315,7 +317,7 @@ export default function ChannelInfoForm({
returnKeyType='next'
showErrorIcon={false}
spellCheck={false}
testID='edit_channel_info.header.input'
testID='channel_info_form.header.input'
value={header}
onLayout={onHeaderLayout}
ref={headerInput}
@@ -326,6 +328,7 @@ export default function ChannelInfoForm({
style={styles.helpText}
id='channel_modal.headerHelp'
defaultMessage={'Specify text to appear in the channel header beside the channel name. For example, include frequently used links by typing link text [Link Title](http://example.com).'}
testID='channel_info_form.header.description'
/>
</View>
</TouchableWithoutFeedback>

View File

@@ -58,7 +58,7 @@ const isDirect = (channel?: ChannelModel): boolean => {
};
const makeCloseButton = (icon: ImageResource) => {
return buildNavigationButton(CLOSE_BUTTON_ID, 'close.more_direct_messages.button', icon);
return buildNavigationButton(CLOSE_BUTTON_ID, 'close.create_or_edit_channel.button', icon);
};
const CreateOrEditChannel = ({
@@ -111,7 +111,7 @@ const CreateOrEditChannel = ({
const rightButton = useMemo(() => {
const base = buildNavigationButton(
editing ? EDIT_BUTTON_ID : CREATE_BUTTON_ID,
'edit_channel.save.button',
editing ? 'create_or_edit_channel.save.button' : 'create_or_edit_channel.create.button',
undefined,
editing ? formatMessage({id: 'mobile.edit_channel', defaultMessage: 'Save'}) : formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create'}),
);

View File

@@ -32,7 +32,7 @@ const CopyPermalinkOption = ({teamName, post}: Props) => {
defaultMessage='Copy Link'
onPress={handleCopyLink}
iconName='link-variant'
testID='post.options.copy.permalink'
testID='post_options.copy.permalink.option'
/>
);
};

View File

@@ -25,7 +25,7 @@ const CopyTextOption = ({postMessage}: Props) => {
defaultMessage='Copy Text'
iconName='content-copy'
onPress={handleCopyText}
testID='post.options.copy.text'
testID='post_options.copy.text.option'
/>
);
};

View File

@@ -50,7 +50,7 @@ const DeletePostOption = ({combinedPost, post}: Props) => {
defaultMessage='Delete'
iconName='trash-can-outline'
onPress={onPress}
testID='post.options.delete.post'
testID='post_options.delete.post.option'
isDestructive={true}
/>
);

View File

@@ -48,7 +48,7 @@ const EditOption = ({post, canDelete}: Props) => {
defaultMessage='Edit'
onPress={onPress}
iconName='pencil-outline'
testID='post.options.edit'
testID='post_options.edit.post.option'
/>
);
};

View File

@@ -48,7 +48,7 @@ const FollowThreadOption = ({thread}: FollowThreadOptionProps) => {
<BaseOption
i18nId={id}
defaultMessage={defaultMessage}
testID='post.options.follow.thread'
testID='post_options.follow.thread.option'
iconName={icon}
onPress={handleToggleFollow}
/>

View File

@@ -29,7 +29,7 @@ const MarkAsUnreadOption = ({postId}: Props) => {
defaultMessage='Mark as Unread'
iconName='mark-as-unread'
onPress={onPress}
testID='post.options.mark.unread'
testID='post_options.mark.unread.option'
/>
);
};

View File

@@ -44,7 +44,7 @@ const PinChannelOption = ({isPostPinned, postId}: PinChannelProps) => {
defaultMessage={defaultMessage}
iconName='pin-outline'
onPress={onPress}
testID={`post.options.${key}.channel`}
testID={`post.options.${key}.channel.option`}
/>
);
};

View File

@@ -31,7 +31,7 @@ const ReplyOption = ({post}: Props) => {
defaultMessage='Reply'
iconName='reply-outline'
onPress={handleReply}
testID='post.options.reply'
testID='post_options.reply.post.option'
/>
);
};

View File

@@ -34,7 +34,7 @@ const SaveOption = ({isSaved, postId}: CopyTextProps) => {
defaultMessage={defaultMessage}
iconName='bookmark-outline'
onPress={onHandlePress}
testID={id}
testID={`post.options.${defaultMessage.toLocaleLowerCase()}.channel.option`}
/>
);
};

View File

@@ -140,6 +140,7 @@ const PostOptions = ({
componentId={Screens.POST_OPTIONS}
initialSnapIndex={0}
snapPoints={[((snapPoints + additionalSnapPoints) * ITEM_HEIGHT), 10]}
testID='post_options'
/>
);
};

View File

@@ -37,6 +37,7 @@ const PickReaction = ({openEmojiPicker, width, height}: PickReactionProps) => {
style={[styles.container, {
width, height,
}]}
testID='post_options.reaction_bar.pick_reaction'
>
<CompassIcon
onPress={openEmojiPicker}

View File

@@ -31,8 +31,9 @@ type ReactionProps = {
emoji: string;
iconSize: number;
containerSize: number;
testID?: string;
}
const Reaction = ({onPressReaction, emoji, iconSize, containerSize}: ReactionProps) => {
const Reaction = ({onPressReaction, emoji, iconSize, containerSize, testID}: ReactionProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const handleReactionPressed = useCallback(() => {
@@ -53,6 +54,7 @@ const Reaction = ({onPressReaction, emoji, iconSize, containerSize}: ReactionPro
key={emoji}
onPress={handleReactionPressed}
style={highlightedStyle}
testID={`${testID}.${emoji}`}
>
<View
style={reactionStyle}

View File

@@ -84,6 +84,7 @@ const ReactionBar = ({recentEmojis = [], postId}: QuickReactionProps) => {
emoji={emoji}
iconSize={iconSize}
containerSize={containerSize}
testID='post_options.reaction_bar.reaction'
/>
);
})

View File

@@ -80,6 +80,7 @@ const Reactions = ({initialEmoji, reactions}: Props) => {
componentId={Screens.REACTIONS}
initialSnapIndex={1}
snapPoints={['90%', '50%', 10]}
testID='reactions'
/>
);
};

View File

@@ -53,6 +53,7 @@ const Thread = ({rootPost}: ThreadProps) => {
accessoriesContainerID={THREAD_ACCESSORIES_CONTAINER_NATIVE_ID}
rootId={rootPost!.id}
keyboardTracker={postDraftRef}
testID='thread.post_draft'
/>
</>
}

View File

@@ -44,6 +44,14 @@ module.exports = {
'@websocket': './app/client/websocket',
},
}],
['module:react-native-dotenv', {
moduleName: '@env',
path: '.env',
blacklist: null,
whitelist: null,
safe: false,
allowUndefined: true,
}],
'react-native-reanimated/plugin',
],
exclude: ['**/*.png', '**/*.jpg', '**/*.gif'],

View File

@@ -5,6 +5,7 @@ import {isAndroid} from '@support/utils';
class Alert {
// alert titles
deletePostTitle = isAndroid() ? element(by.text('Delete Post')) : element(by.label('Delete Post')).atIndex(0);
logoutTitle = (serverDisplayName: string) => {
const title = `Are you sure you want to log out of ${serverDisplayName}?`;
@@ -18,6 +19,7 @@ class Alert {
// alert buttons
cancelButton = isAndroid() ? element(by.text('CANCEL')) : element(by.label('Cancel')).atIndex(1);
deleteButton = isAndroid() ? element(by.text('DELETE')) : element(by.label('Delete')).atIndex(0);
logoutButton = isAndroid() ? element(by.text('LOG OUT')) : element(by.label('Log out')).atIndex(1);
removeButton = isAndroid() ? element(by.text('REMOVE')) : element(by.label('Remove')).atIndex(1);
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
class CameraQuickAction {
testID = {
cameraActionSuffix: 'post_draft.quick_actions.camera_action',
cameraActionDisabledSuffix: 'post_draft.quick_actions.camera_action.disabled',
};
getCameraQuickAction = (screenPrefix: string) => {
return element(by.id(`${screenPrefix}${this.testID.cameraActionSuffix}`));
};
getCameraQuickActionDisabled = (screenPrefix: string) => {
return element(by.id(`${screenPrefix}${this.testID.cameraActionDisabledSuffix}`));
};
}
const cameraQuickAction = new CameraQuickAction();
export default cameraQuickAction;

View File

@@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
class FileQuickAction {
testID = {
fileActionSuffix: 'post_draft.quick_actions.file_action',
fileActionDisabledSuffix: 'post_draft.quick_actions.file_action.disabled',
};
getFileQuickAction = (screenPrefix: string) => {
return element(by.id(`${screenPrefix}${this.testID.fileActionSuffix}`));
};
getFileQuickActionDisabled = (screenPrefix: string) => {
return element(by.id(`${screenPrefix}${this.testID.fileActionDisabledSuffix}`));
};
}
const fileQuickAction = new FileQuickAction();
export default fileQuickAction;

View File

@@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
class ImageQuickAction {
testID = {
imageActionSuffix: 'post_draft.quick_actions.image_action',
imageActionDisabledSuffix: 'post_draft.quick_actions.image_action.disabled',
};
getImageQuickAction = (screenPrefix: string) => {
return element(by.id(`${screenPrefix}${this.testID.imageActionSuffix}`));
};
getImageQuickActionDisabled = (screenPrefix: string) => {
return element(by.id(`${screenPrefix}${this.testID.imageActionDisabledSuffix}`));
};
}
const imageQuickAction = new ImageQuickAction();
export default imageQuickAction;

View File

@@ -2,11 +2,29 @@
// See LICENSE.txt for license information.
import Alert from './alert';
import CameraQuickAction from './camera_quick_action';
import FileQuickAction from './file_quick_action';
import ImageQuickAction from './image_quick_action';
import InputQuickAction from './input_quick_action';
import NavigationHeader from './navigation_header';
import PlusMenu from './plus_menu';
import Post from './post';
import PostDraft from './post_draft';
import PostList from './post_list';
import ProfilePicture from './profile_picture';
import SendButton from './send_button';
export {
Alert,
CameraQuickAction,
FileQuickAction,
ImageQuickAction,
InputQuickAction,
NavigationHeader,
PlusMenu,
Post,
PostDraft,
PostList,
ProfilePicture,
SendButton,
};

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
class InputQuickAction {
testID = {
atInputActionSuffix: 'post_draft.quick_actions.at_input_action',
atInputActionDisabledSuffix: 'post_draft.quick_actions.at_input_action.disabled',
slashInputActionSuffix: 'post_draft.quick_actions.slash_input_action',
slashInputActionDisabledSuffix: 'post_draft.quick_actions.slash_input_action.disabled',
};
getAtInputQuickAction = (screenPrefix: string) => {
return element(by.id(`${screenPrefix}${this.testID.atInputActionSuffix}`));
};
getAtInputQuickActionDisabled = (screenPrefix: string) => {
return element(by.id(`${screenPrefix}${this.testID.atInputActionDisabledSuffix}`));
};
getSlashInputQuickAction = (screenPrefix: string) => {
return element(by.id(`${screenPrefix}${this.testID.slashInputActionSuffix}`));
};
getSlashInputQuickActionDisabled = (screenPrefix: string) => {
return element(by.id(`${screenPrefix}${this.testID.slashInputActionDisabledSuffix}`));
};
}
const inputQuickAction = new InputQuickAction();
export default inputQuickAction;

View File

@@ -0,0 +1,109 @@
// 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.',
blockQuote: 'markdown_block_quote',
emoji: 'markdown_emoji',
image: 'markdown_image',
message: 'markdown_text',
postHeaderCommentedOn: 'post_header.commented_on',
postHeaderDateTime: 'post_header.date_time',
postHeaderDisplayName: 'post_header.display_name',
postHeaderBotTag: 'post_header.bot_tag',
postHeaderGuestTag: 'post_header.guest_tag',
postHeaderReply: 'post_header.reply',
postHeaderReplyCount: 'post_header.reply.count',
postPreHeaderText: 'post_pre_header.text',
showLessButton: 'show_more.button.chevron-up',
showMoreButton: 'show_more.button.chevron-down',
table: 'markdown_table',
tableExpandButton: 'markdown_table.expand.button',
thematicBreak: 'markdown_thematic_break',
};
getPost = (postItemSourceTestID: string, postId: string, postMessage: string, postProfileOptions: any = {}) => {
const postItemMatcher = this.getPostItemMatcher(postItemSourceTestID, postId, postMessage);
const postItemBlockQuoteMatcher = by.id(this.testID.blockQuote).withAncestor(postItemMatcher);
const postItemEmojiMatcher = by.id(this.testID.emoji).withAncestor(postItemMatcher);
const postItemImageMatcher = by.id(this.testID.image).withAncestor(postItemMatcher);
const postItemMessageMatcher = by.id(this.testID.message).withAncestor(postItemMatcher);
const postItemPreHeaderTextMatch = by.id(this.testID.postPreHeaderText).withAncestor(postItemMatcher);
const postItemShowLessButtonMatcher = by.id(this.testID.showLessButton).withAncestor(postItemMatcher);
const postItemShowMoreButtonMatcher = by.id(this.testID.showMoreButton).withAncestor(postItemMatcher);
const postItemTableMatcher = by.id(this.testID.table).withAncestor(postItemMatcher);
const postItemTableExpandButtonMatcher = by.id(this.testID.tableExpandButton).withAncestor(postItemMatcher);
const postItemThematicBreakMatcher = by.id(this.testID.thematicBreak).withAncestor(postItemMatcher);
return {
postItem: element(postItemMatcher),
postItemBlockQuote: element(postItemBlockQuoteMatcher),
postItemEmoji: element(postItemEmojiMatcher),
postItemImage: element(postItemImageMatcher),
postItemMessage: element(postItemMessageMatcher),
postItemPreHeaderText: element(postItemPreHeaderTextMatch),
postItemShowLessButton: element(postItemShowLessButtonMatcher),
postItemShowMoreButton: element(postItemShowMoreButtonMatcher),
postItemTable: element(postItemTableMatcher),
postItemTableExpandButton: element(postItemTableExpandButtonMatcher),
postItemThematicBreak: element(postItemThematicBreakMatcher),
...this.getPostHeader(postItemMatcher),
...this.getPostProfilePicture(postItemMatcher, postProfileOptions),
};
};
getPostHeader = (postItemMatcher: Detox.NativeMatcher) => {
const postItemHeaderCommentedOnMatcher = by.id(this.testID.postHeaderCommentedOn).withAncestor(postItemMatcher);
const postItemHeaderDateTimeMatcher = by.id(this.testID.postHeaderDateTime).withAncestor(postItemMatcher);
const postItemHeaderDisplayNameMatcher = by.id(this.testID.postHeaderDisplayName).withAncestor(postItemMatcher);
const postItemHeaderBotTagMatcher = by.id(this.testID.postHeaderBotTag).withAncestor(postItemMatcher);
const postItemHeaderGuestTagMatcher = by.id(this.testID.postHeaderGuestTag).withAncestor(postItemMatcher);
const postItemHeaderReplyMatcher = by.id(this.testID.postHeaderReply).withAncestor(postItemMatcher);
const postItemHeaderReplyCountMatcher = by.id(this.testID.postHeaderReplyCount).withAncestor(postItemMatcher);
return {
postItemHeaderCommentedOn: element(postItemHeaderCommentedOnMatcher),
postItemHeaderDateTime: element(postItemHeaderDateTimeMatcher),
postItemHeaderDisplayName: element(postItemHeaderDisplayNameMatcher),
postItemHeaderBotTag: element(postItemHeaderBotTagMatcher),
postItemHeaderGuestTag: element(postItemHeaderGuestTagMatcher),
postItemHeaderReply: element(postItemHeaderReplyMatcher),
postItemHeaderReplyCount: element(postItemHeaderReplyCountMatcher),
};
};
getPostItemMatcher = (postItemSourceTestID: string, postId: string, postMessage: string) => {
const postTestID = `${postItemSourceTestID}.${postId}`;
const baseMatcher = by.id(postTestID);
return postMessage ? baseMatcher.withDescendant(by.text(postMessage)) : baseMatcher;
};
getPostMessage = (postItemSourceTestID: string) => {
return element(by.id(this.testID.message).withAncestor(by.id(postItemSourceTestID)));
};
getPostProfilePicture = (postItemMatcher: Detox.NativeMatcher, 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),
};
};
}
const post = new Post();
export default post;

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
class PostDraft {
testID = {
postDraftSuffix: 'post_draft',
postDraftArchivedSuffix: 'post_draft.archived',
postDraftReadOnlySuffix: 'post_draft.read_only',
postInputSuffix: 'post_draft.post.input',
};
getPostDraft = (screenPrefix: string) => {
return element(by.id(`${screenPrefix}${this.testID.postDraftSuffix}`));
};
getPostDraftArchived = (screenPrefix: string) => {
return element(by.id(`${screenPrefix}${this.testID.postDraftArchivedSuffix}`));
};
getPostDraftReadOnly = (screenPrefix: string) => {
return element(by.id(`${screenPrefix}${this.testID.postDraftReadOnlySuffix}`));
};
getPostInput = (screenPrefix: string) => {
return element(by.id(`${screenPrefix}${this.testID.postInputSuffix}`));
};
}
const postDraft = new PostDraft();
export default postDraft;

View File

@@ -0,0 +1,83 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Post from './post';
class PostList {
testID: any;
constructor(screenPrefix: string) {
this.testID = {
flatList: `${screenPrefix}post_list.flat_list`,
moreMessagesButton: `${screenPrefix}post_list.more_messages_button`,
newMessagesDivider: `${screenPrefix}post_list.new_messages_line`,
postListPostItem: `${screenPrefix}post_list.post`,
};
}
getFlatList = () => {
return element(by.id(this.testID.flatList));
};
getMoreMessagesButton = () => {
return element(by.id(this.testID.moreMessagesButton));
};
getNewMessagesDivider = () => {
return element(by.id(this.testID.newMessagesDivider));
};
getPost = (postId: string, postMessage: string, postProfileOptions = {}) => {
const {
postItem,
postItemBlockQuote,
postItemEmoji,
postItemHeaderCommentedOn,
postItemHeaderDateTime,
postItemHeaderDisplayName,
postItemHeaderBotTag,
postItemHeaderGuestTag,
postItemHeaderReply,
postItemHeaderReplyCount,
postItemImage,
postItemMessage,
postItemPreHeaderText,
postItemProfilePicture,
postItemProfilePictureUserStatus,
postItemShowLessButton,
postItemShowMoreButton,
postItemTable,
postItemTableExpandButton,
postItemThematicBreak,
} = Post.getPost(this.testID.postListPostItem, postId, postMessage, postProfileOptions);
return {
postListPostItem: postItem,
postListPostItemBlockQuote: postItemBlockQuote,
postListPostItemEmoji: postItemEmoji,
postListPostItemHeaderCommentedOn: postItemHeaderCommentedOn,
postListPostItemHeaderDateTime: postItemHeaderDateTime,
postListPostItemHeaderDisplayName: postItemHeaderDisplayName,
postListPostItemHeaderBotTag: postItemHeaderBotTag,
postListPostItemHeaderGuestTag: postItemHeaderGuestTag,
postListPostItemHeaderReply: postItemHeaderReply,
postListPostItemHeaderReplyCount: postItemHeaderReplyCount,
postListPostItemImage: postItemImage,
postListPostItemMessage: postItemMessage,
postListPostItemPreHeaderText: postItemPreHeaderText,
postListPostItemProfilePicture: postItemProfilePicture,
postListPostItemProfilePictureUserStatus: postItemProfilePictureUserStatus,
postListPostItemShowLessButton: postItemShowLessButton,
postListPostItemShowMoreButton: postItemShowMoreButton,
postListPostItemTable: postItemTable,
postListPostItemTableExpandButton: postItemTableExpandButton,
postListPostItemThematicBreak: postItemThematicBreak,
};
};
getPostMessageAtIndex = (index: number) => {
return Post.getPostMessage(this.testID.postListPostItem).atIndex(index);
};
}
export default PostList;

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: string, userId: string) => {
const profilePictureTestID = `${profilePictureSourcePrefix}${userId}`;
return by.id(profilePictureTestID);
};
getProfilePictureItemUserStatusMatcher(profilePictureItemMatcher: Detox.NativeMatcher, userStatus = 'online') {
const userStatusIconTestID = `${this.testID.userStatusIconPrefix}${userStatus}`;
return by.id(userStatusIconTestID).withAncestor(profilePictureItemMatcher);
}
}
const profilePicture = new ProfilePicture();
export default profilePicture;

View File

@@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
class SendButton {
testID = {
sendButtonSuffix: 'post_draft.send_action.send.button',
sendButtonDisabledSuffix: 'post_draft.send_action.send.button.disabled',
};
getSendButton = (screenPrefix: string) => {
return element(by.id(`${screenPrefix}${this.testID.sendButtonSuffix}`));
};
getSendButtonDisabled = (screenPrefix: string) => {
return element(by.id(`${screenPrefix}${this.testID.sendButtonDisabledSuffix}`));
};
}
const sendButton = new SendButton();
export default sendButton;

View File

@@ -1,13 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {NavigationHeader} from '@support/ui/component';
import {ChannelListScreen} from '@support/ui/screen';
import {
CameraQuickAction,
FileQuickAction,
ImageQuickAction,
InputQuickAction,
NavigationHeader,
PostDraft,
PostList,
SendButton,
} from '@support/ui/component';
import {
ChannelListScreen,
PostOptionsScreen,
ThreadScreen,
} from '@support/ui/screen';
import {timeouts} from '@support/utils';
import {expect} from 'detox';
class ChannelScreen {
testID = {
channelScreenPrefix: 'channel.',
channelScreen: 'channel.screen',
introDisplayName: 'channel_post_list.intro.display_name',
introOptionAddPeopleItem: 'channel_post_list.intro.option_item.add_people',
@@ -20,13 +34,49 @@ class ChannelScreen {
introOptionAddPeopleItem = element(by.id(this.testID.introOptionAddPeopleItem));
introOptionSetHeaderItem = element(by.id(this.testID.introOptionSetHeaderItem));
introOptionChannelDetailsItem = element(by.id(this.testID.introOptionChannelDetailsItem));
// convenience props
backButton = NavigationHeader.backButton;
headerTitle = NavigationHeader.headerTitle;
atInputQuickAction = InputQuickAction.getAtInputQuickAction(this.testID.channelScreenPrefix);
atInputQuickActionDisabled = InputQuickAction.getAtInputQuickActionDisabled(this.testID.channelScreenPrefix);
slashInputQuickAction = InputQuickAction.getSlashInputQuickAction(this.testID.channelScreenPrefix);
slashInputQuickActionDisabled = InputQuickAction.getSlashInputQuickActionDisabled(this.testID.channelScreenPrefix);
fileQuickAction = FileQuickAction.getFileQuickAction(this.testID.channelScreenPrefix);
fileQuickActionDisabled = FileQuickAction.getFileQuickActionDisabled(this.testID.channelScreenPrefix);
imageQuickAction = ImageQuickAction.getImageQuickAction(this.testID.channelScreenPrefix);
imageQuickActionDisabled = ImageQuickAction.getImageQuickActionDisabled(this.testID.channelScreenPrefix);
cameraQuickAction = CameraQuickAction.getCameraQuickAction(this.testID.channelScreenPrefix);
cameraQuickActionDisabled = CameraQuickAction.getCameraQuickActionDisabled(this.testID.channelScreenPrefix);
postDraft = PostDraft.getPostDraft(this.testID.channelScreenPrefix);
postDraftArchived = PostDraft.getPostDraftArchived(this.testID.channelScreenPrefix);
postDraftReadOnly = PostDraft.getPostDraftReadOnly(this.testID.channelScreenPrefix);
postInput = PostDraft.getPostInput(this.testID.channelScreenPrefix);
sendButton = SendButton.getSendButton(this.testID.channelScreenPrefix);
sendButtonDisabled = SendButton.getSendButtonDisabled(this.testID.channelScreenPrefix);
postList = new PostList(this.testID.channelScreenPrefix);
getIntroOptionItemLabel = (introOptionItemTestId: string) => {
return element(by.id(`${introOptionItemTestId}.label`));
};
getMoreMessagesButton = () => {
return this.postList.getMoreMessagesButton();
};
getNewMessagesDivider = () => {
return this.postList.getNewMessagesDivider();
};
getPostListPostItem = (postId: string, text: string, postProfileOptions: any = {}) => {
return this.postList.getPost(postId, text, postProfileOptions);
};
getPostMessageAtIndex = (index: number) => {
return this.postList.getPostMessageAtIndex(index);
};
toBeVisible = async () => {
await waitFor(this.channelScreen).toExist().withTimeout(timeouts.TEN_SEC);
@@ -44,6 +94,48 @@ class ChannelScreen {
await this.backButton.tap();
await expect(this.channelScreen).not.toBeVisible();
};
openPostOptionsFor = async (postId: string, text: string) => {
const {postListPostItem} = this.getPostListPostItem(postId, text);
await expect(postListPostItem).toBeVisible();
// # Open post options
await postListPostItem.longPress();
await PostOptionsScreen.toBeVisible();
};
openReplyThreadFor = async (postId: string, text: string) => {
await this.openPostOptionsFor(postId, text);
// # Open reply thread screen
await PostOptionsScreen.replyPostOption.tap();
await ThreadScreen.toBeVisible();
};
postMessage = async (message: string) => {
// # Post message
await this.postInput.tap();
await this.postInput.replaceText(message);
await this.tapSendButton();
};
tapSendButton = async () => {
// # Tap send button
await this.sendButton.tap();
await expect(this.sendButton).not.toExist();
await expect(this.sendButtonDisabled).toBeVisible();
};
hasPostMessage = async (postId: string, postMessage: string) => {
const {postListPostItem} = this.getPostListPostItem(postId, postMessage);
await expect(postListPostItem).toBeVisible();
};
hasPostMessageAtIndex = async (index: number, postMessage: string) => {
await expect(
this.getPostMessageAtIndex(index),
).toHaveText(postMessage);
};
}
const channelScreen = new ChannelScreen();

View File

@@ -25,6 +25,8 @@ class ChannelListScreen {
headerPlusButton = element(by.id(this.testID.headerPlusButton));
findChannelsInput = element(by.id(this.testID.findChannelsInput));
threadsButton = element(by.id(this.testID.threadsButton));
// convenience props
browseChannelsItem = PlusMenu.browseChannelsItem;
createNewChannelItem = PlusMenu.createNewChannelItem;
openDirectMessageItem = PlusMenu.openDirectMessageItem;

View File

@@ -0,0 +1,80 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
ChannelScreen,
ChannelListScreen,
} from '@support/ui/screen';
import {timeouts} from '@support/utils';
import {expect} from 'detox';
class CreateOrEditChannelScreen {
testID = {
createOrEditChannelScreen: 'create_or_edit_channel.screen',
closeButton: 'close.create_or_edit_channel.button',
createButton: 'create_or_edit_channel.create.button',
saveButton: 'create_or_edit_channel.save.button',
scrollView: 'create_or_edit_channel.scrollview',
makePrivateToggledOff: 'channel_info_form.make_private.toggled.false',
makePrivateToggledOn: 'channel_info_form.make_private.toggled.true',
makePrivateDescription: 'channel_info_form.make_private.description',
displayNameInput: 'channel_info_form.display_name.input',
purposeInput: 'channel_info_form.purpose.input',
purposeDescription: 'channel_info_form.purpose.description',
headerInput: 'channel_info_form.header.input',
headerDescription: 'channel_info_form.header.description',
};
createOrEditChannelScreen = element(by.id(this.testID.createOrEditChannelScreen));
closeButton = element(by.id(this.testID.closeButton));
createButton = element(by.id(this.testID.createButton));
saveButton = element(by.id(this.testID.saveButton));
scrollView = element(by.id(this.testID.scrollView));
makePrivateToggledOff = element(by.id(this.testID.makePrivateToggledOff));
makePrivateToggledOn = element(by.id(this.testID.makePrivateToggledOn));
makePrivateDescription = element(by.id(this.testID.makePrivateDescription));
displayNameInput = element(by.id(this.testID.displayNameInput));
purposeInput = element(by.id(this.testID.purposeInput));
purposeDescription = element(by.id(this.testID.purposeDescription));
headerInput = element(by.id(this.testID.headerInput));
headerDescription = element(by.id(this.testID.headerDescription));
toBeVisible = async () => {
await waitFor(this.createOrEditChannelScreen).toExist().withTimeout(timeouts.TEN_SEC);
return this.createOrEditChannelScreen;
};
openCreateChannel = async () => {
// # Open create channel screen
await ChannelListScreen.headerPlusButton.tap();
await ChannelListScreen.createNewChannelItem.tap();
return this.toBeVisible();
};
openEditChannel = async () => {
// # Open edit channel screen
await ChannelScreen.introOptionSetHeaderItem.tap();
return this.toBeVisible();
};
close = async () => {
await this.closeButton.tap();
await expect(this.createOrEditChannelScreen).not.toBeVisible();
};
toggleMakePrivateOn = async () => {
await this.makePrivateToggledOff.tap();
await expect(this.makePrivateToggledOn).toBeVisible();
};
toggleMakePrivateOff = async () => {
await this.makePrivateToggledOn.tap();
await expect(this.makePrivateToggledOff).toBeVisible();
};
}
const createOrEditChannelScreen = new CreateOrEditChannelScreen();
export default createOrEditChannelScreen;

View File

@@ -22,7 +22,7 @@ class HomeScreen {
accountTab = element(by.id(this.testID.accountTab));
toBeVisible = async () => {
await waitFor(this.channelListTab).toBeVisible().withTimeout(timeouts.TEN_SEC);
await waitFor(this.channelListTab).toExist().withTimeout(timeouts.TEN_SEC);
return this.channelListTab;
};

View File

@@ -6,11 +6,14 @@ import BrowseChannelsScreen from './browse_channels';
import ChannelScreen from './channel';
import ChannelListScreen from './channel_list';
import CreateDirectMessageScreen from './create_direct_message';
import CreateOrEditChannelScreen from './create_or_edit_channel';
import EditServerScreen from './edit_server';
import HomeScreen from './home';
import LoginScreen from './login';
import PostOptionsScreen from './post_options';
import ServerScreen from './server';
import ServerListScreen from './server_list';
import ThreadScreen from './thread';
export {
AccountScreen,
@@ -18,9 +21,12 @@ export {
ChannelScreen,
ChannelListScreen,
CreateDirectMessageScreen,
CreateOrEditChannelScreen,
EditServerScreen,
HomeScreen,
LoginScreen,
PostOptionsScreen,
ServerScreen,
ServerListScreen,
ThreadScreen,
};

View File

@@ -0,0 +1,79 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Alert} from '@support/ui/component';
import {timeouts, wait} from '@support/utils';
import {expect} from 'detox';
class PostOptionsScreen {
testID = {
reactionEmojiPrefix: 'post_options.reaction_bar.reaction.',
postOptionsScreen: 'post_options.screen',
pickReaction: 'post_options.reaction_bar.pick_reaction',
replyPostOption: 'post_options.reply.post.option',
copyLinkOption: 'post_options.copy.permalink.option',
saveChannelOption: 'post_options.save.channel.option',
unsaveChannelOption: 'post_options.unsave.channel.option',
copyTextOption: 'post_options.copy.text.option',
pinChannelOption: 'post_options.pin.channel.option',
unpinChannelOption: 'post_options.unpin.channel.option',
editPostOption: 'post_options.edit.post.option',
deletePostOption: 'post_options.delete.post.option',
followThreadOption: 'post_options.follow.thread.option',
markUnreadOption: 'post_options.mark.unread.option',
};
postOptionsScreen = element(by.id(this.testID.postOptionsScreen));
pickReaction = element(by.id(this.testID.pickReaction));
replyPostOption = element(by.id(this.testID.replyPostOption));
copyLinkOption = element(by.id(this.testID.copyLinkOption));
saveChannelOption = element(by.id(this.testID.saveChannelOption));
unsaveChannelOption = element(by.id(this.testID.unsaveChannelOption));
copyTextOption = element(by.id(this.testID.copyTextOption));
pinChannelOption = element(by.id(this.testID.pinChannelOption));
unpinChannelOption = element(by.id(this.testID.unpinChannelOption));
editPostOption = element(by.id(this.testID.editPostOption));
deletePostOption = element(by.id(this.testID.deletePostOption));
followThreadOption = element(by.id(this.testID.followThreadOption));
markUnreadOption = element(by.id(this.testID.markUnreadOption));
getReactionEmoji = (emojiName: string) => {
return element(by.id(`${this.testID.reactionEmojiPrefix}${emojiName}`));
};
toBeVisible = async () => {
await waitFor(this.postOptionsScreen).toExist().withTimeout(timeouts.TEN_SEC);
return postOptionsScreen;
};
close = async () => {
await this.postOptionsScreen.tap({x: 5, y: 10});
await expect(this.postOptionsScreen).not.toBeVisible();
};
deletePost = async ({confirm = true} = {}) => {
await this.deletePostOption.tap({x: 1, y: 1});
const {
deletePostTitle,
cancelButton,
deleteButton,
} = Alert;
await expect(deletePostTitle).toBeVisible();
await expect(cancelButton).toBeVisible();
await expect(deleteButton).toBeVisible();
if (confirm) {
await deleteButton.tap();
await wait(timeouts.ONE_SEC);
await expect(this.postOptionsScreen).not.toBeVisible();
} else {
await cancelButton.tap();
await wait(timeouts.ONE_SEC);
await expect(this.postOptionsScreen).toBeVisible();
await this.close();
}
};
}
const postOptionsScreen = new PostOptionsScreen();
export default postOptionsScreen;

View File

@@ -0,0 +1,31 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {NavigationHeader} from '@support/ui/component';
import {timeouts} from '@support/utils';
import {expect} from 'detox';
class ThreadScreen {
testID = {
threadScreen: 'channel.screen',
};
threadScreen = element(by.id(this.testID.threadScreen));
// convenience props
backButton = NavigationHeader.backButton;
toBeVisible = async () => {
await waitFor(this.threadScreen).toExist().withTimeout(timeouts.TEN_SEC);
return this.threadScreen;
};
back = async () => {
await this.backButton.tap();
await expect(this.threadScreen).not.toBeVisible();
};
}
const threadScreen = new ThreadScreen();
export default threadScreen;

View File

@@ -77,9 +77,9 @@ describe('Channels - Browse Channels', () => {
await expect(BrowseChannelsScreen.getChannelItemDisplayName(channel.name)).toHaveText(channel.display_name);
// # Tap on the new channel item
await BrowseChannelsScreen.getChannelItem(channel.name).tap();
await BrowseChannelsScreen.getChannelItem(channel.name).multiTap(2);
// * Verify on new channel screen
// * Verify on newly joined channel screen
await ChannelScreen.toBeVisible();
await expect(ChannelScreen.headerTitle).toHaveText(channel.display_name);
await expect(ChannelScreen.introDisplayName).toHaveText(channel.display_name);
@@ -88,7 +88,7 @@ describe('Channels - Browse Channels', () => {
await ChannelScreen.back();
await ChannelListScreen.toBeVisible();
// * Verify new channel is added to channel list
// * Verify newly joined channel is added to channel list
await expect(ChannelListScreen.getChannelListItemDisplayName(channelsCategory, channel.name)).toBeVisible();
});
});

View File

@@ -17,6 +17,7 @@ import {
ChannelScreen,
ChannelListScreen,
CreateDirectMessageScreen,
CreateOrEditChannelScreen,
HomeScreen,
LoginScreen,
ServerScreen,
@@ -142,8 +143,16 @@ describe('Channels - Channel List', () => {
await CreateDirectMessageScreen.close();
});
xit('MM-T4728_6 - should be able to go to create channel screen', async () => {
// NOT YET IMPLEMENTED
it('MM-T4728_6 - should be able to go to create channel screen', async () => {
// # Tap on plus menu button and tap on create new channel item
await ChannelListScreen.headerPlusButton.tap();
await ChannelListScreen.createNewChannelItem.tap();
// * Verify on create channel screen
await CreateOrEditChannelScreen.toBeVisible();
// # Go back to channel list screen
await CreateOrEditChannelScreen.close();
});
xit('MM-T4728_7 - should be able to go to threads screen', async () => {

View File

@@ -0,0 +1,98 @@
// 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 {
Post,
Setup,
} from '@support/server_api';
import {
serverOneUrl,
siteOneUrl,
} from '@support/test_config';
import {
ChannelScreen,
ChannelListScreen,
HomeScreen,
LoginScreen,
PostOptionsScreen,
ServerScreen,
} from '@support/ui/screen';
import {getRandomId} from '@support/utils';
import {expect} from 'detox';
describe('Channels - Channel Post List', () => {
const serverOneDisplayName = 'Server 1';
let testChannel: any;
beforeAll(async () => {
const {channel, user} = await Setup.apiInit(siteOneUrl);
testChannel = channel;
// # Log in to server
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
await LoginScreen.login(user);
});
beforeEach(async () => {
// * Verify on channel list screen
await ChannelListScreen.toBeVisible();
});
afterAll(async () => {
// # Log out
await HomeScreen.logout();
});
it('MM-T4773_1 - should match elements on channel screen', async () => {
// # Open a channel screen
await ChannelScreen.open('channels', testChannel.name);
// * Verify basic elements on channel screen
await expect(ChannelScreen.backButton).toBeVisible();
await expect(ChannelScreen.headerTitle).toHaveText(testChannel.display_name);
await expect(ChannelScreen.introDisplayName).toHaveText(testChannel.display_name);
await expect(ChannelScreen.introOptionAddPeopleItem).toBeVisible();
await expect(ChannelScreen.introOptionSetHeaderItem).toBeVisible();
await expect(ChannelScreen.introOptionChannelDetailsItem).toBeVisible();
await expect(ChannelScreen.postList.getFlatList()).toBeVisible();
await expect(ChannelScreen.postDraft).toBeVisible();
await expect(ChannelScreen.postInput).toBeVisible();
await expect(ChannelScreen.atInputQuickAction).toBeVisible();
await expect(ChannelScreen.slashInputQuickAction).toBeVisible();
await expect(ChannelScreen.fileQuickAction).toBeVisible();
await expect(ChannelScreen.imageQuickAction).toBeVisible();
await expect(ChannelScreen.cameraQuickAction).toBeVisible();
await expect(ChannelScreen.sendButtonDisabled).toBeVisible();
// # Go back to channel list screen
await ChannelScreen.back();
});
it('MM-T4773_2 - should be able to add a message to post list and delete a message from post list', async () => {
// # Open a channel screen and post a message
const message = `Message ${getRandomId()}`;
await ChannelScreen.open('channels', testChannel.name);
await ChannelScreen.postMessage(message);
// * Verify message is added to post list
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem} = ChannelScreen.getPostListPostItem(post.id, message);
await expect(postListPostItem).toExist();
// # Open post options for the message that was just posted, tap delete option and confirm
await ChannelScreen.openPostOptionsFor(post.id, message);
await PostOptionsScreen.deletePost({confirm: true});
// * Verify message is deleted from post list
await expect(postListPostItem).not.toExist();
// # Go back to channel list screen
await ChannelScreen.back();
});
});

View File

@@ -0,0 +1,139 @@
// 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 {Setup} from '@support/server_api';
import {
serverOneUrl,
siteOneUrl,
} from '@support/test_config';
import {
ChannelListScreen,
ChannelScreen,
CreateOrEditChannelScreen,
HomeScreen,
LoginScreen,
ServerScreen,
} from '@support/ui/screen';
import {getRandomId} from '@support/utils';
import {expect} from 'detox';
describe('Channels - Create Channel and Edit Channel Header', () => {
const serverOneDisplayName = 'Server 1';
beforeAll(async () => {
const {user} = await Setup.apiInit(siteOneUrl);
// # Log in to server
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
await LoginScreen.login(user);
});
beforeEach(async () => {
// * Verify on channel list screen
await ChannelListScreen.toBeVisible();
});
afterAll(async () => {
// # Log out
await HomeScreen.logout();
});
it('MM-T4731_1 - should match elements on create channel screen', async () => {
// # Open create channel screen
await CreateOrEditChannelScreen.openCreateChannel();
// * Verify basic elements on create channel screen
await expect(CreateOrEditChannelScreen.closeButton).toBeVisible();
await expect(CreateOrEditChannelScreen.createButton).toBeVisible();
await expect(CreateOrEditChannelScreen.makePrivateToggledOff).toBeVisible();
await expect(CreateOrEditChannelScreen.makePrivateDescription).toHaveText('When a channel is set to private, only invited team members can access and participate in that channel');
await expect(CreateOrEditChannelScreen.displayNameInput).toBeVisible();
await expect(CreateOrEditChannelScreen.purposeInput).toBeVisible();
await expect(CreateOrEditChannelScreen.purposeDescription).toHaveText('Describe how this channel should be used.');
await expect(CreateOrEditChannelScreen.headerInput).toBeVisible();
await expect(CreateOrEditChannelScreen.headerDescription).toHaveText('Specify text to appear in the channel header beside the channel name. For example, include frequently used links by typing link text [Link Title](http://example.com).');
// # Go back to channel list screen
await CreateOrEditChannelScreen.close();
});
it('MM-T4731_2 - should be able to create a public channel and edit the channel header', async () => {
// # Open create channel screen, toggle make private off, fill out channel info, and tap create button
const suffix = getRandomId();
const displayName = `Channel ${suffix}`;
const purpose = `Purpose ${suffix}`;
const header = `Header ${suffix}`;
await CreateOrEditChannelScreen.openCreateChannel();
await expect(CreateOrEditChannelScreen.makePrivateToggledOff).toBeVisible();
await CreateOrEditChannelScreen.displayNameInput.replaceText(displayName);
await CreateOrEditChannelScreen.purposeInput.replaceText(purpose);
await CreateOrEditChannelScreen.headerInput.replaceText(header);
await CreateOrEditChannelScreen.createButton.tap();
// * Verify on newly created public channel
await ChannelScreen.toBeVisible();
await expect(ChannelScreen.headerTitle).toHaveText(displayName);
await expect(ChannelScreen.introDisplayName).toHaveText(displayName);
// # Tap on set header option to edit the channel header
await ChannelScreen.introOptionSetHeaderItem.tap();
// * Verify channel header is correct
await expect(CreateOrEditChannelScreen.headerInput).toHaveValue(header);
// # Edit the channel header, save, and re-open edit channel header screen
await CreateOrEditChannelScreen.headerInput.replaceText(`${header} edit`);
await CreateOrEditChannelScreen.saveButton.tap();
await CreateOrEditChannelScreen.openEditChannel();
// * Verify channel header has new value
await expect(CreateOrEditChannelScreen.headerInput).toHaveValue(`${header} edit`);
// # Go back to channel list screen
await CreateOrEditChannelScreen.close();
await ChannelScreen.back();
});
it('MM-T4731_3 - should be able to create a private channel and edit the channel header', async () => {
// # Open create channel screen, toggle make private on, fill out channel info, and tap create button
const suffix = getRandomId();
const displayName = `Channel ${suffix}`;
const purpose = `Purpose ${suffix}`;
const header = `Header ${suffix}`;
await CreateOrEditChannelScreen.openCreateChannel();
await CreateOrEditChannelScreen.toggleMakePrivateOn();
await CreateOrEditChannelScreen.displayNameInput.replaceText(displayName);
await CreateOrEditChannelScreen.purposeInput.replaceText(purpose);
await CreateOrEditChannelScreen.headerInput.replaceText(header);
await CreateOrEditChannelScreen.createButton.tap();
// * Verify on newly created private channel
await ChannelScreen.toBeVisible();
await expect(ChannelScreen.headerTitle).toHaveText(displayName);
await expect(ChannelScreen.introDisplayName).toHaveText(displayName);
// # Tap on set header option to edit the channel header
await ChannelScreen.introOptionSetHeaderItem.tap();
// * Verify channel header is correct
await expect(CreateOrEditChannelScreen.headerInput).toHaveValue(header);
// # Edit the channel header, save, and re-open edit channel header screen
await CreateOrEditChannelScreen.headerInput.replaceText(`${header} edit`);
await CreateOrEditChannelScreen.saveButton.tap();
await CreateOrEditChannelScreen.openEditChannel();
// * Verify channel header has new value
await expect(CreateOrEditChannelScreen.headerInput).toHaveValue(`${header} edit`);
// # Go back to channel list screen
await CreateOrEditChannelScreen.close();
await ChannelScreen.back();
});
});

View File

@@ -8,6 +8,7 @@
// *******************************************************************
import {
Channel,
Setup,
Team,
User,
@@ -30,10 +31,12 @@ describe('Channels - Create Direct Message', () => {
const serverOneDisplayName = 'Server 1';
const directMessagesCategory = 'direct_messages';
let testTeam: any;
let testUser: any;
beforeAll(async () => {
const {team, user} = await Setup.apiInit(siteOneUrl);
testTeam = team;
testUser = user;
// # Log in to server
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
@@ -75,7 +78,7 @@ describe('Channels - Create Direct Message', () => {
// # Open create direct message screen and search for the new user
await CreateDirectMessageScreen.open();
await CreateDirectMessageScreen.searchInput.replaceText(newUser.username);
await CreateDirectMessageScreen.searchInput.replaceText(newUserDisplayName);
// * Verify search returns the new user item
await expect(CreateDirectMessageScreen.getUserItemDisplayName(newUser.id)).toBeVisible();
@@ -99,7 +102,8 @@ describe('Channels - Create Direct Message', () => {
await ChannelListScreen.toBeVisible();
// * Verify direct message channel for the new user is added to direct message list
await expect(ChannelListScreen.getChannelListItemDisplayName(directMessagesCategory, newUser.id)).toHaveText(newUserDisplayName);
const {channel: directMessageChannel} = await Channel.apiCreateDirectChannel(siteOneUrl, [testUser.id, newUser.id]);
await expect(ChannelListScreen.getChannelListItemDisplayName(directMessagesCategory, directMessageChannel.name)).toHaveText(newUserDisplayName);
});
it('MM-T4730_3 - should be able to create a group message', async () => {
@@ -112,8 +116,8 @@ describe('Channels - Create Direct Message', () => {
// * Verify no group message channel for the new users appears on channel list screen
const firstNewUserDisplayName = firstNewUser.username;
const secondNewUserDisplayName = secondNewUser.username;
const groupDisplayName = `${firstNewUserDisplayName}, ${secondNewUserDisplayName}`;
await expect(ChannelListScreen.getChannelListItemDisplayName(directMessagesCategory, groupDisplayName)).not.toBeVisible();
const groupDisplayName = `${firstNewUserDisplayName}, ${secondNewUserDisplayName}, ${testUser.username}`;
await expect(element(by.text(groupDisplayName))).not.toBeVisible();
// # Open create direct message screen, search for the first new user and tap on the first new user item
await CreateDirectMessageScreen.open();
@@ -143,6 +147,6 @@ describe('Channels - Create Direct Message', () => {
await ChannelListScreen.toBeVisible();
// * Verify group message channel for the other two new users is added to direct message list
await expect(ChannelListScreen.getChannelListItemDisplayName(directMessagesCategory, `${firstNewUser.id}__${secondNewUser.id}`)).toHaveText(groupDisplayName);
await expect(element(by.text(groupDisplayName))).toBeVisible();
});
});

View File

@@ -0,0 +1,130 @@
// 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 {
Channel,
Post,
Setup,
Team,
User,
} from '@support/server_api';
import {
serverOneUrl,
siteOneUrl,
} from '@support/test_config';
import {
BrowseChannelsScreen,
ChannelScreen,
ChannelListScreen,
CreateDirectMessageScreen,
CreateOrEditChannelScreen,
HomeScreen,
LoginScreen,
ServerScreen,
} from '@support/ui/screen';
import {getRandomId} from '@support/utils';
import {expect} from 'detox';
describe('Smoke Test - Channels', () => {
const serverOneDisplayName = 'Server 1';
const channelsCategory = 'channels';
let testChannel: any;
let testTeam: any;
beforeAll(async () => {
const {channel, team, user} = await Setup.apiInit(siteOneUrl);
testChannel = channel;
testTeam = team;
// # Log in to server
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
await LoginScreen.login(user);
});
beforeEach(async () => {
// * Verify on channel list screen
await ChannelListScreen.toBeVisible();
});
afterAll(async () => {
// # Log out
await HomeScreen.logout();
});
it('MM-T4774_1 - should be able to join a new channel and switch to an existing channel', async () => {
// # As admin, create a new channel so that user can join, then open browse channels screen and join the new channel
const {channel} = await Channel.apiCreateChannel(siteOneUrl, {teamId: testTeam.id});
await BrowseChannelsScreen.open();
await BrowseChannelsScreen.searchInput.replaceText(channel.name);
await BrowseChannelsScreen.getChannelItem(channel.name).multiTap(2);
// * Verify on newly joined channel screen
await ChannelScreen.toBeVisible();
await expect(ChannelScreen.headerTitle).toHaveText(channel.display_name);
await expect(ChannelScreen.introDisplayName).toHaveText(channel.display_name);
// # Go back to channel list screen and switch to an existing channel
await ChannelScreen.back();
await ChannelScreen.open(channelsCategory, testChannel.name);
// * Verify on the other channel screen
await ChannelScreen.toBeVisible();
await expect(ChannelScreen.headerTitle).toHaveText(testChannel.display_name);
await expect(ChannelScreen.introDisplayName).toHaveText(testChannel.display_name);
// # Go back to channel list screen
await ChannelScreen.back();
});
it('MM-T4774_2 - should be able to create a channel and create a direct message', async () => {
// # Open create channel screen and create a new channel
const displayName = `Channel ${getRandomId()}`;
await CreateOrEditChannelScreen.openCreateChannel();
await CreateOrEditChannelScreen.displayNameInput.replaceText(displayName);
await CreateOrEditChannelScreen.createButton.tap();
// * Verify on newly created public channel
await ChannelScreen.toBeVisible();
await expect(ChannelScreen.headerTitle).toHaveText(displayName);
await expect(ChannelScreen.introDisplayName).toHaveText(displayName);
// # As admin, create a new user to open direct message with, then go back to channel list screen, open create direct message screen and open direct message with new user
const {user: newUser} = await User.apiCreateUser(siteOneUrl);
const newUserDisplayName = newUser.username;
await Team.apiAddUserToTeam(siteOneUrl, newUser.id, testTeam.id);
await ChannelScreen.back();
await CreateDirectMessageScreen.open();
await CreateDirectMessageScreen.searchInput.replaceText(newUserDisplayName);
await CreateDirectMessageScreen.getUserItem(newUser.id).tap();
await CreateDirectMessageScreen.startButton.tap();
// * Verify on direct message channel screen for the new user
await ChannelScreen.toBeVisible();
await expect(ChannelScreen.headerTitle).toHaveText(newUserDisplayName);
await expect(ChannelScreen.introDisplayName).toHaveText(newUserDisplayName);
// # Go back to channel list screen
await ChannelScreen.back();
});
it('MM-T4774_3 - should be able to post a message in a channel', async () => {
// # Open a channel screen and post a message
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
// * Verify message is posted
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem} = ChannelScreen.getPostListPostItem(post.id, message);
await expect(postListPostItem).toExist();
// # Go back to channel list screen
await ChannelScreen.back();
});
});

43
package-lock.json generated
View File

@@ -56,6 +56,7 @@
"react-native-create-thumbnail": "1.5.1",
"react-native-device-info": "8.7.0",
"react-native-document-picker": "8.1.0",
"react-native-dotenv": "3.4.0",
"react-native-elements": "3.4.2",
"react-native-exception-handler": "2.10.10",
"react-native-fast-image": "8.5.11",
@@ -118,6 +119,7 @@
"@types/react-native": "0.67.3",
"@types/react-native-background-timer": "2.0.0",
"@types/react-native-button": "3.0.1",
"@types/react-native-dotenv": "0.2.0",
"@types/react-native-share": "3.3.3",
"@types/react-native-video": "5.0.13",
"@types/react-syntax-highlighter": "13.5.2",
@@ -5766,6 +5768,12 @@
"@types/react-native": "*"
}
},
"node_modules/@types/react-native-dotenv": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@types/react-native-dotenv/-/react-native-dotenv-0.2.0.tgz",
"integrity": "sha512-ZxX+dU/yoQc0jTk+/NWttkiuXceJyN5FpOSqDl0WynN5GDzxwH7OMruQ47qcY8llo2RD3irjvzJ9BwC8gDiq0A==",
"dev": true
},
"node_modules/@types/react-native-share": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/@types/react-native-share/-/react-native-share-3.3.3.tgz",
@@ -9432,6 +9440,14 @@
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dotenv": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz",
"integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==",
"engines": {
"node": ">=12"
}
},
"node_modules/dtrace-provider": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
@@ -19041,6 +19057,14 @@
}
}
},
"node_modules/react-native-dotenv": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/react-native-dotenv/-/react-native-dotenv-3.4.0.tgz",
"integrity": "sha512-Z/pvS0QGSDHcxnUmJ/uHLgC1/DZL/UpKSOHp3mCKyV3vQd385CBahrEBca8pD3eabrsDjg8xO0uRxnRG3s1lbw==",
"dependencies": {
"dotenv": "^16.0.0"
}
},
"node_modules/react-native-elements": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/react-native-elements/-/react-native-elements-3.4.2.tgz",
@@ -27593,6 +27617,12 @@
"@types/react-native": "*"
}
},
"@types/react-native-dotenv": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@types/react-native-dotenv/-/react-native-dotenv-0.2.0.tgz",
"integrity": "sha512-ZxX+dU/yoQc0jTk+/NWttkiuXceJyN5FpOSqDl0WynN5GDzxwH7OMruQ47qcY8llo2RD3irjvzJ9BwC8gDiq0A==",
"dev": true
},
"@types/react-native-share": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/@types/react-native-share/-/react-native-share-3.3.3.tgz",
@@ -30487,6 +30517,11 @@
"domhandler": "^4.2.0"
}
},
"dotenv": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz",
"integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q=="
},
"dtrace-provider": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
@@ -38027,6 +38062,14 @@
"invariant": "^2.2.4"
}
},
"react-native-dotenv": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/react-native-dotenv/-/react-native-dotenv-3.4.0.tgz",
"integrity": "sha512-Z/pvS0QGSDHcxnUmJ/uHLgC1/DZL/UpKSOHp3mCKyV3vQd385CBahrEBca8pD3eabrsDjg8xO0uRxnRG3s1lbw==",
"requires": {
"dotenv": "^16.0.0"
}
},
"react-native-elements": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/react-native-elements/-/react-native-elements-3.4.2.tgz",

View File

@@ -54,6 +54,7 @@
"react-native-create-thumbnail": "1.5.1",
"react-native-device-info": "8.7.0",
"react-native-document-picker": "8.1.0",
"react-native-dotenv": "3.4.0",
"react-native-elements": "3.4.2",
"react-native-exception-handler": "2.10.10",
"react-native-fast-image": "8.5.11",
@@ -116,6 +117,7 @@
"@types/react-native": "0.67.3",
"@types/react-native-background-timer": "2.0.0",
"@types/react-native-button": "3.0.1",
"@types/react-native-dotenv": "0.2.0",
"@types/react-native-share": "3.3.3",
"@types/react-native-video": "5.0.13",
"@types/react-syntax-highlighter": "13.5.2",

View File

@@ -3,4 +3,5 @@
// So that typescript doesn't complain about importing `@env` through react-native-dotenv
declare module '@env' {
export const RUNNING_E2E: string;
}