MM-30427 Detox/E2E: Add e2e for MM-T3197, MM-T3225, MM-T3239, MM-T3274 (#5230)

This commit is contained in:
Joseph Baylon 2021-03-22 11:16:40 -07:00 committed by GitHub
parent 5d8632971b
commit 78c05270ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 299 additions and 3 deletions

View file

@ -39,6 +39,7 @@ export default class Emoji extends React.PureComponent {
textStyle: CustomPropTypes.Style,
unicode: PropTypes.string,
customEmojiStyle: CustomPropTypes.Style,
testID: PropTypes.string,
};
static defaultProps = {
@ -55,6 +56,7 @@ export default class Emoji extends React.PureComponent {
imageUrl,
literal,
unicode,
testID,
textStyle,
} = this.props;
@ -67,7 +69,13 @@ export default class Emoji extends React.PureComponent {
}
if (displayTextOnly) {
return <Text style={textStyle}>{literal}</Text>;
return (
<Text
style={textStyle}
testID={testID}
>
{literal}
</Text>);
}
const width = size;
@ -84,7 +92,10 @@ export default class Emoji extends React.PureComponent {
}, '');
return (
<Text style={[textStyle, {fontSize: size}]}>
<Text
style={[textStyle, {fontSize: size}]}
testID={testID}
>
{code}
</Text>
);
@ -101,6 +112,7 @@ export default class Emoji extends React.PureComponent {
source={{uri: imageUrl}}
onError={this.onError}
resizeMode={FastImage.resizeMode.contain}
testID={testID}
/>
);
}

View file

@ -259,6 +259,7 @@ export default class Markdown extends PureComponent {
<Emoji
emojiName={emojiName}
literal={literal}
testID='markdown_emoji'
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
/>
);

View file

@ -59,6 +59,7 @@ export default class MarkdownEmoji extends PureComponent {
<Emoji
emojiName={emojiName}
literal={literal}
testID='markdown_emoji'
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
/>
);

View file

@ -73,6 +73,7 @@ export default class ChannelAddMembers extends PureComponent {
text: context.intl.formatMessage({id: 'integrations.add', defaultMessage: 'Add'}),
color: props.theme.sidebarHeaderTextColor,
showAsAction: 'always',
testID: 'channel_add_members.add.button',
};
setButtons(props.componentId, {
@ -369,6 +370,7 @@ export default class ChannelAddMembers extends PureComponent {
onLoadMore={this.getProfiles}
onRowPress={this.handleSelectProfile}
renderItem={this.renderItem}
testID='channel_add_members.custom_list'
theme={theme}
/>
</SafeAreaView>

View file

@ -156,6 +156,7 @@ exports[`Search should match snapshot 1`] = `
"flex": 1,
}
}
testID="search.results_list"
updateCellsBatchingPeriod={50}
windowSize={21}
/>

View file

@ -757,6 +757,7 @@ export default class Search extends PureComponent {
scrollEventThrottle={60}
ListFooterComponent={this.renderFooter}
onViewableItemsChanged={this.onViewableItemsChanged}
testID='search.results_list'
/>
</SafeAreaView>
<Autocomplete

View file

@ -6,6 +6,7 @@ import ProfilePicture from './profile_picture';
class Post {
testID = {
postProfilePicturePrefix: 'post_profile_picture.profile_picture.',
emoji: 'markdown_emoji',
image: 'markdown_image',
message: 'markdown_text',
postHeaderDateTime: 'post_header.date_time',
@ -19,6 +20,7 @@ class Post {
getPost = (postItemSourceTestID, postId, postMessage, postProfileOptions = {}) => {
const postItemMatcher = this.getPostItemMatcher(postItemSourceTestID, postId, postMessage);
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);
@ -27,6 +29,7 @@ class Post {
return {
postItem: element(postItemMatcher),
postItemEmoji: element(postItemEmojiMatcher),
postItemImage: element(postItemImageMatcher),
postItemMessage: element(postItemMessageMatcher),
postItemPreHeaderText: element(postItemPreHeaderTextMatch),

View file

@ -13,6 +13,7 @@ class PostList {
getPost = (postId, postMessage, postProfileOptions = {}) => {
const {
postItem,
postItemEmoji,
postItemHeaderDateTime,
postItemHeaderDisplayName,
postItemHeaderGuestTag,
@ -28,6 +29,7 @@ class PostList {
return {
postListPostItem: postItem,
postListPostItemEmoji: postItemEmoji,
postListPostItemHeaderDateTime: postItemHeaderDateTime,
postListPostItemHeaderDisplayName: postItemHeaderDisplayName,
postListPostItemHeaderGuestTag: postItemHeaderGuestTag,

View file

@ -0,0 +1,104 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
Alert,
MainSidebar,
SearchBar,
} from '@support/ui/component';
import {timeouts, wait} from '@support/utils';
class ChannelAddMembersScreen {
testID = {
channelAddMembersScreenPrefix: 'channel_add_members.',
channelAddMembersScreen: 'channel_add_members.screen',
addButton: 'channel_add_members.add.button',
backButton: 'screen.back.button',
usersList: 'channel_add_members.custom_list',
userItem: 'channel_add_members.custom_list.user_item',
userItemDisplayUsername: 'channel_add_members.custom_list.user_item.display_username',
}
channelAddMembersScreen = element(by.id(this.testID.channelAddMembersScreen));
addButton = element(by.id(this.testID.addButton));
backButton = element(by.id(this.testID.backButton));
usersList = element(by.id(this.testID.usersList));
// convenience props
searchBar = SearchBar.getSearchBar(this.testID.channelAddMembersScreenPrefix);
searchInput = SearchBar.getSearchInput(this.testID.channelAddMembersScreenPrefix);
cancelButton = SearchBar.getCancelButton(this.testID.channelAddMembersScreenPrefix);
clearButton = SearchBar.getClearButton(this.testID.channelAddMembersScreenPrefix);
getUser = (userId, diplayUsername) => {
const userItemTestID = `${this.testID.userItem}.${userId}`;
const baseMatcher = by.id(userItemTestID);
const userItemMatcher = diplayUsername ? baseMatcher.withDescendant(by.text(diplayUsername)) : baseMatcher;
const userItemUsernameDisplayMatcher = by.id(this.testID.userItemDisplayUsername).withAncestor(userItemMatcher);
return {
userItem: element(userItemMatcher),
userItemUsernameDisplay: element(userItemUsernameDisplayMatcher),
};
}
getUserAtIndex = (index) => {
return element(by.id(this.testID.userItem).withAncestor(by.id(this.testID.usersList))).atIndex(index);
}
getUserByDisplayUsername = (displayUsername) => {
return element(by.text(displayUsername).withAncestor(by.id(this.testID.usersList)));
}
getDisplayUsernameAtIndex = (index) => {
return element(by.id(this.testID.userItemDisplayUsername)).atIndex(index);
}
toBeVisible = async () => {
await expect(this.channelAddMembersScreen).toBeVisible();
return this.channelAddMembersScreen;
}
open = async () => {
// # Open more direct messages screen
await MainSidebar.openChannelMembersButton.tap();
return this.toBeVisible();
}
back = async () => {
await this.backButton.tap();
await expect(this.channelAddMembersScreen).not.toBeVisible();
}
removeMembers = async (displayUsernameList, confirm = true) => {
displayUsernameList.forEach(async (displayUsername) => {
await this.getUserByDisplayUsername(`@${displayUsername}`).tap();
});
await wait(timeouts.ONE_SEC);
await this.removeButton.tap();
const {
removeMembersTitle,
noButton,
yesButton,
} = Alert;
await expect(removeMembersTitle).toBeVisible();
if (confirm) {
yesButton.tap();
} else {
noButton.tap();
}
await wait(timeouts.ONE_SEC);
await expect(this.channelAddMembersScreen).not.toBeVisible();
}
hasUserDisplayUsernameAtIndex = async (index, displayUsername) => {
await expect(
this.getDisplayUsernameAtIndex(index),
).toHaveText(displayUsername);
}
}
const channelAddMembersScreen = new ChannelAddMembersScreen();
export default channelAddMembersScreen;

View file

@ -3,6 +3,7 @@
import AddReactionScreen from './add_reaction';
import ChannelInfoScreen from './channel_info';
import ChannelAddMembersScreen from './channel_add_members';
import ChannelMembersScreen from './channel_members';
import ChannelNotificationPreferenceScreen from './channel_notification_preference';
import ChannelScreen from './channel';
@ -33,6 +34,7 @@ import ThreadScreen from './thread';
export {
AddReactionScreen,
ChannelInfoScreen,
ChannelAddMembersScreen,
ChannelMembersScreen,
ChannelNotificationPreferenceScreen,
ChannelScreen,

View file

@ -14,6 +14,7 @@ class LongPostScreen {
getPost = (postId, postMessage, postProfileOptions = {}) => {
const {
postItem,
postItemEmoji,
postItemHeaderDateTime,
postItemHeaderDisplayName,
postItemHeaderGuestTag,
@ -28,6 +29,7 @@ class LongPostScreen {
return {
longPostItem: postItem,
longPostItemEmoji: postItemEmoji,
longPostItemHeaderDateTime: postItemHeaderDateTime,
longPostItemHeaderDisplayName: postItemHeaderDisplayName,
longPostItemHeaderGuestTag: postItemHeaderGuestTag,

View file

@ -20,6 +20,7 @@ class SearchScreen {
searchOnSection: 'search.on_section',
searchAfterSection: 'search.after_section',
searchBeforeSection: 'search.before_section',
searchResultsList: 'search.results_list',
}
searchScreen = element(by.id(this.testID.searchScreen));
@ -28,6 +29,7 @@ class SearchScreen {
searchOnSection = element(by.id(this.testID.searchOnSection));
searchAfterSection = element(by.id(this.testID.searchAfterSection));
searchBeforeSection = element(by.id(this.testID.searchBeforeSection));
searchResultsList = element(by.id(this.testID.searchResultsList));
searchFromModifier = 'from:';
searchInModifier = 'in:';

View file

@ -11,6 +11,7 @@ class SearchResultPostScreen {
getPost = (postId, postMessage, postProfileOptions = {}) => {
const {
postItem,
postItemEmoji,
postItemHeaderDateTime,
postItemHeaderDisplayName,
postItemHeaderGuestTag,
@ -25,6 +26,7 @@ class SearchResultPostScreen {
return {
searchResultPostItem: postItem,
searchResultPostItemEmoji: postItemEmoji,
searchResultPostItemHeaderDateTime: postItemHeaderDateTime,
searchResultPostItemHeaderDisplayName: postItemHeaderDisplayName,
searchResultPostItemHeaderGuestTag: postItemHeaderGuestTag,

View file

@ -0,0 +1,63 @@
// 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 {
ChannelAddMembersScreen,
ChannelInfoScreen,
ChannelScreen,
} from '@support/ui/screen';
import {
Setup,
Team,
User,
} from '@support/server_api';
describe('Channel Add Members', () => {
let testChannel;
let testLastUser;
beforeAll(async () => {
const {channel, team, user} = await Setup.apiInit();
testChannel = channel;
[...Array(100).keys()].forEach(async (key) => {
const {user: testOtherUser} = await User.apiCreateUser({prefix: `a-${key}-`});
await Team.apiAddUserToTeam(testOtherUser.id, team.id);
});
({user: testLastUser} = await User.apiCreateUser({prefix: 'z-'}));
await Team.apiAddUserToTeam(testLastUser.id, team.id);
// # Open channel screen
await ChannelScreen.open(user);
});
afterAll(async () => {
await ChannelScreen.logout();
});
it('MM-T3197 should be able load users in long add members list', async () => {
const {
addMembersAction,
channelInfoScrollView,
} = ChannelInfoScreen;
// # Open add members screen
await ChannelScreen.goToChannel(testChannel.display_name);
await ChannelInfoScreen.open();
await channelInfoScrollView.scrollTo('bottom');
await addMembersAction.tap();
// * Verify user can scroll down multiple times until last user is seen
await waitFor(ChannelAddMembersScreen.getUserByDisplayUsername(`@${testLastUser.username}`)).toBeVisible().whileElement(by.id(ChannelAddMembersScreen.testID.usersList)).scroll(500, 'down');
await ChannelAddMembersScreen.back();
// # Close channel info screen
await ChannelInfoScreen.close();
});
});

View file

@ -20,7 +20,7 @@ import {
} from '@support/server_api';
import {getRandomId} from '@support/utils';
describe('Manage Members', () => {
describe('Channel Manage Members', () => {
const searchTerm = getRandomId();
const {
removeMembers,

View file

@ -78,4 +78,15 @@ describe('Message Posting', () => {
const {postListPostItemImage} = await getPostListPostItem(post.id);
await expect(postListPostItemImage).toBeVisible();
});
it('MM-T3225 should be able to post a jumbo emoji', async () => {
// # Post a jumbo emoji
const message = ':fox_face:';
await postMessage(message, {quickReplace: true});
// * Verify message is posted
const {post} = await Post.apiGetLastPostInChannel(testChannel.id);
const {postListPostItem} = await getPostListPostItem(post.id);
await expect(postListPostItem).toBeVisible();
});
});

View file

@ -33,6 +33,7 @@ describe('Search', () => {
getSearchResultPostItem,
searchFromModifier,
searchInModifier,
searchInput,
} = SearchScreen;
let testMessage;
let testPartialSearchTerm;
@ -174,6 +175,36 @@ describe('Search', () => {
// # Go back to channel
await SearchScreen.cancel();
});
it('MM-T3239 should be able to scroll through long list of search results', async () => {
// # Post messages
const keyword = 'qa';
const firstMessage = `${Date.now().toString()} ${keyword} first`;
const firstPost = await Post.apiCreatePost({
channelId: testChannel.id,
message: firstMessage,
});
[...Array(50).keys()].forEach(async () => {
const message = `${Date.now().toString()} ${keyword}`;
await Post.apiCreatePost({
channelId: testChannel.id,
message,
});
});
// # Perform search on keyword
await SearchScreen.open();
await searchInput.clearText();
await searchInput.typeText(keyword);
await searchInput.tapReturnKey();
// * Verify user can scroll down multiple times until first matching post is seen
const {searchResultPostItem} = await getSearchResultPostItem(firstPost.post.id, firstMessage);
await waitFor(searchResultPostItem).toBeVisible().whileElement(by.id(SearchScreen.testID.searchResultsList)).scroll(1000, 'down');
// # Go back to channel
await SearchScreen.cancel();
});
});
async function postMessageAndSearchFrom(testMessage, testUser, atMentionSuggestionList, testPartialSearchTerm) {

View file

@ -0,0 +1,56 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *******************************************************************
// - [#] indicates a test step (e.g. # Go to a screen)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element testID when selecting an element. Create one if none.
// *******************************************************************
import {
ChannelScreen,
LoginScreen,
SelectTeamScreen,
} from '@support/ui/screen';
import {
Channel,
Team,
User,
} from '@support/server_api';
describe('Select Team', () => {
const {getTeamByDisplayName} = SelectTeamScreen;
let testTeam;
beforeAll(async () => {
await Team.apiPatchTeams({allow_open_invite: false});
const {user} = await User.apiCreateUser();
const {team} = await Team.apiCreateTeam();
testTeam = team;
await Team.apiPatchTeam(testTeam.id, {allow_open_invite: true});
// # Login
await LoginScreen.open();
await LoginScreen.login(user);
});
afterAll(async () => {
await ChannelScreen.logout();
await Team.apiPatchTeams({allow_open_invite: true});
});
it('MM-T3274 user with no teams should be able to select a team', async () => {
// * Verify select team screen is displayed
await SelectTeamScreen.toBeVisible();
// # Tap on team to join
await waitFor(getTeamByDisplayName(testTeam.display_name)).toBeVisible().whileElement(by.id(SelectTeamScreen.testID.teamsList)).scroll(500, 'down');
const team = await getTeamByDisplayName(testTeam.display_name);
await team.tap();
// * Verify redirect to default channel of joined team
const {channel} = await Channel.apiGetChannelByName(testTeam.name, 'town-square');
await ChannelScreen.toBeVisible();
await expect(ChannelScreen.channelNavBarTitle).toHaveText(channel.display_name);
});
});