diff --git a/app/components/emoji/emoji.js b/app/components/emoji/emoji.js
index a21c39eac..80f62c39b 100644
--- a/app/components/emoji/emoji.js
+++ b/app/components/emoji/emoji.js
@@ -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 {literal};
+ return (
+
+ {literal}
+ );
}
const width = size;
@@ -84,7 +92,10 @@ export default class Emoji extends React.PureComponent {
}, '');
return (
-
+
{code}
);
@@ -101,6 +112,7 @@ export default class Emoji extends React.PureComponent {
source={{uri: imageUrl}}
onError={this.onError}
resizeMode={FastImage.resizeMode.contain}
+ testID={testID}
/>
);
}
diff --git a/app/components/markdown/markdown.js b/app/components/markdown/markdown.js
index 6b4785a5f..a7863ada0 100644
--- a/app/components/markdown/markdown.js
+++ b/app/components/markdown/markdown.js
@@ -259,6 +259,7 @@ export default class Markdown extends PureComponent {
);
diff --git a/app/components/markdown/markdown_emoji/markdown_emoji.js b/app/components/markdown/markdown_emoji/markdown_emoji.js
index 22dd9cbe4..3161f942a 100644
--- a/app/components/markdown/markdown_emoji/markdown_emoji.js
+++ b/app/components/markdown/markdown_emoji/markdown_emoji.js
@@ -59,6 +59,7 @@ export default class MarkdownEmoji extends PureComponent {
);
diff --git a/app/screens/channel_add_members/channel_add_members.js b/app/screens/channel_add_members/channel_add_members.js
index b99dcfe2e..c0bb24675 100644
--- a/app/screens/channel_add_members/channel_add_members.js
+++ b/app/screens/channel_add_members/channel_add_members.js
@@ -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}
/>
diff --git a/app/screens/search/__snapshots__/search.test.js.snap b/app/screens/search/__snapshots__/search.test.js.snap
index 4b959440c..448c6b91f 100644
--- a/app/screens/search/__snapshots__/search.test.js.snap
+++ b/app/screens/search/__snapshots__/search.test.js.snap
@@ -156,6 +156,7 @@ exports[`Search should match snapshot 1`] = `
"flex": 1,
}
}
+ testID="search.results_list"
updateCellsBatchingPeriod={50}
windowSize={21}
/>
diff --git a/app/screens/search/search.js b/app/screens/search/search.js
index b1843cd55..a15a048d1 100644
--- a/app/screens/search/search.js
+++ b/app/screens/search/search.js
@@ -757,6 +757,7 @@ export default class Search extends PureComponent {
scrollEventThrottle={60}
ListFooterComponent={this.renderFooter}
onViewableItemsChanged={this.onViewableItemsChanged}
+ testID='search.results_list'
/>
{
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),
diff --git a/detox/e2e/support/ui/component/post_list.js b/detox/e2e/support/ui/component/post_list.js
index 7e05f25b9..d2695a65e 100644
--- a/detox/e2e/support/ui/component/post_list.js
+++ b/detox/e2e/support/ui/component/post_list.js
@@ -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,
diff --git a/detox/e2e/support/ui/screen/channel_add_members.js b/detox/e2e/support/ui/screen/channel_add_members.js
new file mode 100644
index 000000000..1e1a7f527
--- /dev/null
+++ b/detox/e2e/support/ui/screen/channel_add_members.js
@@ -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;
diff --git a/detox/e2e/support/ui/screen/index.js b/detox/e2e/support/ui/screen/index.js
index ad6c3be1d..0f74ca234 100644
--- a/detox/e2e/support/ui/screen/index.js
+++ b/detox/e2e/support/ui/screen/index.js
@@ -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,
diff --git a/detox/e2e/support/ui/screen/long_post.js b/detox/e2e/support/ui/screen/long_post.js
index f27ba9fa3..72c4aa99c 100644
--- a/detox/e2e/support/ui/screen/long_post.js
+++ b/detox/e2e/support/ui/screen/long_post.js
@@ -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,
diff --git a/detox/e2e/support/ui/screen/search.js b/detox/e2e/support/ui/screen/search.js
index fb7320992..10f1ab677 100644
--- a/detox/e2e/support/ui/screen/search.js
+++ b/detox/e2e/support/ui/screen/search.js
@@ -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:';
diff --git a/detox/e2e/support/ui/screen/search_result_post.js b/detox/e2e/support/ui/screen/search_result_post.js
index 9acc75463..0c36db4f2 100644
--- a/detox/e2e/support/ui/screen/search_result_post.js
+++ b/detox/e2e/support/ui/screen/search_result_post.js
@@ -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,
diff --git a/detox/e2e/test/smoke_test/channel_add_members.e2e.js b/detox/e2e/test/smoke_test/channel_add_members.e2e.js
new file mode 100644
index 000000000..4c6185cc0
--- /dev/null
+++ b/detox/e2e/test/smoke_test/channel_add_members.e2e.js
@@ -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();
+ });
+});
diff --git a/detox/e2e/test/smoke_test/manage_members.e2e.js b/detox/e2e/test/smoke_test/channel_manage_members.e2e.js
similarity index 98%
rename from detox/e2e/test/smoke_test/manage_members.e2e.js
rename to detox/e2e/test/smoke_test/channel_manage_members.e2e.js
index ad90204c7..880881a4c 100644
--- a/detox/e2e/test/smoke_test/manage_members.e2e.js
+++ b/detox/e2e/test/smoke_test/channel_manage_members.e2e.js
@@ -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,
diff --git a/detox/e2e/test/smoke_test/message_posting.e2e.js b/detox/e2e/test/smoke_test/message_posting.e2e.js
index c7b4016f7..ffb31365f 100644
--- a/detox/e2e/test/smoke_test/message_posting.e2e.js
+++ b/detox/e2e/test/smoke_test/message_posting.e2e.js
@@ -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();
+ });
});
diff --git a/detox/e2e/test/smoke_test/search.e2e.js b/detox/e2e/test/smoke_test/search.e2e.js
index 2681f5715..da7e92eb5 100644
--- a/detox/e2e/test/smoke_test/search.e2e.js
+++ b/detox/e2e/test/smoke_test/search.e2e.js
@@ -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) {
diff --git a/detox/e2e/test/smoke_test/select_team.e2e.js b/detox/e2e/test/smoke_test/select_team.e2e.js
new file mode 100644
index 000000000..0a27cbd49
--- /dev/null
+++ b/detox/e2e/test/smoke_test/select_team.e2e.js
@@ -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);
+ });
+});