From d464f102fb5b7b354e0b676ef74cc816a6a1ac93 Mon Sep 17 00:00:00 2001 From: Joseph Baylon Date: Thu, 20 May 2021 08:16:52 -0700 Subject: [PATCH] MM-35670 Detox/E2E: Add e2e for mark post unread and read (#5396) --- .../more_messages_button.js | 4 +- .../post_list/new_messages_divider.js | 3 + app/components/post_list/post_list.js | 2 + detox/e2e/support/server_api/channel.js | 77 +++++--- detox/e2e/support/server_api/common.js | 4 +- detox/e2e/support/server_api/ldap.js | 10 +- detox/e2e/support/server_api/plugin.js | 16 +- detox/e2e/support/server_api/post.js | 21 +++ detox/e2e/support/server_api/system.js | 7 +- detox/e2e/support/server_api/user.js | 4 +- detox/e2e/support/ui/component/post_list.js | 10 ++ detox/e2e/support/ui/screen/channel.js | 8 + .../remove_user_from_channel.e2e.js | 4 +- detox/e2e/test/messaging/mark_as_read.e2e.js | 87 +++++++++ .../e2e/test/messaging/mark_as_unread.e2e.js | 167 ++++++++++++++++++ 15 files changed, 373 insertions(+), 51 deletions(-) create mode 100644 detox/e2e/test/messaging/mark_as_read.e2e.js create mode 100644 detox/e2e/test/messaging/mark_as_unread.e2e.js diff --git a/app/components/post_list/more_messages_button/more_messages_button.js b/app/components/post_list/more_messages_button/more_messages_button.js index cd33e3545..e82d6b6b4 100644 --- a/app/components/post_list/more_messages_button/more_messages_button.js +++ b/app/components/post_list/more_messages_button/more_messages_button.js @@ -53,6 +53,7 @@ export default class MoreMessageButton extends React.PureComponent { registerScrollEndIndexListener: PropTypes.func.isRequired, resetUnreadMessageCount: PropTypes.func.isRequired, deepLinkURL: PropTypes.string, + testID: PropTypes.string, }; static contextTypes = { @@ -295,7 +296,7 @@ export default class MoreMessageButton extends React.PureComponent { } render() { - const {theme, loadingPosts} = this.props; + const {theme, loadingPosts, testID} = this.props; const styles = getStyleSheet(theme); const {moreText} = this.state; @@ -309,6 +310,7 @@ export default class MoreMessageButton extends React.PureComponent { onPress={this.onMoreMessagesPress} underlayColor={underlayColor} style={styles.roundBorder} + testID={testID} > diff --git a/app/components/post_list/new_messages_divider.js b/app/components/post_list/new_messages_divider.js index a06f6abfc..61d8e79d0 100644 --- a/app/components/post_list/new_messages_divider.js +++ b/app/components/post_list/new_messages_divider.js @@ -19,6 +19,7 @@ function NewMessagesDivider(props) { id='posts_view.newMsg' defaultMessage='New Messages' style={style.text} + testID={props.testID} /> ); @@ -28,6 +29,7 @@ function NewMessagesDivider(props) { id='mobile.posts_view.moreMsg' defaultMessage='More New Messages Above' style={style.text} + testID={props.testID} /> ); } @@ -47,6 +49,7 @@ NewMessagesDivider.propTypes = { moreMessages: PropTypes.bool, style: ViewPropTypes.style, theme: PropTypes.object, + testID: PropTypes.string, }; const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index 625c131e1..966d2eba8 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -317,6 +317,7 @@ export default class PostList extends PureComponent { index={index} theme={theme} moreMessages={moreNewMessages && checkForPostId} + testID={`${testID}.new_messages_divider`} /> ); } else if (PostListUtils.isDateLine(item)) { @@ -544,6 +545,7 @@ export default class PostList extends PureComponent { scrollToIndex={this.scrollToIndex} registerViewableItemsListener={this.registerViewableItemsListener} registerScrollEndIndexListener={this.registerScrollEndIndexListener} + testID={`${testID}.more_messages_button`} /> } diff --git a/detox/e2e/support/server_api/channel.js b/detox/e2e/support/server_api/channel.js index 26aa6aac7..0d65a55e2 100644 --- a/detox/e2e/support/server_api/channel.js +++ b/detox/e2e/support/server_api/channel.js @@ -98,25 +98,6 @@ export const apiCreateGroupChannel = async (userIds = []) => { } }; -/** - * Remove user from channel. - * See https://api.mattermost.com/#operation/RemoveUserFromChannel - * @param {string} channelId - The channel ID - * @param {string} userId - The user ID to be removed from channel - * @return {Object} returns {status} on success or {error, status} on error - */ -export const apiDeleteUserFromChannel = async (channelId, userId) => { - try { - const response = await client.delete( - `/api/v4/channels/${channelId}/members/${userId}`, - ); - - return {status: response.status}; - } catch (err) { - return getResponseFromError(err); - } -}; - /** * Get a channel by name. * See https://api.mattermost.com/#operation/GetChannelByName @@ -168,6 +149,60 @@ export const apiGetChannelsForUser = async (userId, teamId) => { } }; +/** + * Get unread messages. + * See https://api.mattermost.com/#operation/GetChannelUnread + * @param {string} userId - The user ID to perform view actions for + * @param {string} channelId - The channel ID that is being viewed + * @return {Object} returns response on success or {error, status} on error + */ +export const apiGetUnreadMessages = async (userId, channelId) => { + try { + return await client.get(`/api/v4/users/${userId}/channels/${channelId}/unread`); + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Remove user from channel. + * See https://api.mattermost.com/#operation/RemoveUserFromChannel + * @param {string} channelId - The channel ID + * @param {string} userId - The user ID to be removed from channel + * @return {Object} returns {status} on success or {error, status} on error + */ +export const apiRemoveUserFromChannel = async (channelId, userId) => { + try { + const response = await client.delete( + `/api/v4/channels/${channelId}/members/${userId}`, + ); + + return {status: response.status}; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * View channel. + * See https://api.mattermost.com/#operation/ViewChannel + * @param {string} userId - The user ID to perform view actions for + * @param {string} channelId - The channel ID that is being viewed + * @return {Object} returns {viewed} on success or {error, status} on error + */ +export const apiViewChannel = async (userId, channelId) => { + try { + const response = await client.post( + `/api/v4/channels/members/${userId}/view`, + {channel_id: channelId}, + ); + + return {viewed: response.data}; + } catch (err) { + return getResponseFromError(err); + } +}; + function generateRandomChannel(teamId, type, prefix) { const randomId = getRandomId(); @@ -186,9 +221,11 @@ export const Channel = { apiCreateChannel, apiCreateDirectChannel, apiCreateGroupChannel, - apiDeleteUserFromChannel, apiGetChannelByName, apiGetChannelsForUser, + apiGetUnreadMessages, + apiRemoveUserFromChannel, + apiViewChannel, }; export default Channel; diff --git a/detox/e2e/support/server_api/common.js b/detox/e2e/support/server_api/common.js index 5cfc0aa39..1b30d3c08 100644 --- a/detox/e2e/support/server_api/common.js +++ b/detox/e2e/support/server_api/common.js @@ -34,13 +34,11 @@ export const apiUploadFile = async (name, absFilePath, requestOptions = {}) => { formData.append(name, fs.createReadStream(absFilePath)); try { - const response = await client.request({ + return await client.request({ ...requestOptions, data: formData, headers: formData.getHeaders(), }); - - return response; } catch (err) { return getResponseFromError(err); } diff --git a/detox/e2e/support/server_api/ldap.js b/detox/e2e/support/server_api/ldap.js index 6f8684aab..45010fec1 100644 --- a/detox/e2e/support/server_api/ldap.js +++ b/detox/e2e/support/server_api/ldap.js @@ -21,13 +21,11 @@ import {getResponseFromError} from './common'; /** * Synchronize any user attribute changes in the configured AD/LDAP server with Mattermost. * See https://api.mattermost.com/#operation/SyncLdap - * @return {string} returns {status} on success or {error, status} on error + * @return {string} returns response on success or {error, status} on error */ export const apiLDAPSync = async () => { try { - const response = await client.post('/api/v4/ldap/sync'); - - return response; + return await client.post('/api/v4/ldap/sync'); } catch (err) { return getResponseFromError(err); } @@ -36,13 +34,13 @@ export const apiLDAPSync = async () => { /** * Test the current AD/LDAP configuration to see if the AD/LDAP server can be contacted successfully. * See https://api.mattermost.com/#operation/TestLdap - * @return {string} returns {status} on success or {error, status} on error + * @return {Object} returns {status} on success or {error, status} on error */ export const apiLDAPTest = async () => { try { const response = await client.post('/api/v4/ldap/test'); - return response.data; + return {status: response.status}; } catch (err) { return getResponseFromError(err); } diff --git a/detox/e2e/support/server_api/plugin.js b/detox/e2e/support/server_api/plugin.js index 7c81d899d..c18264f60 100644 --- a/detox/e2e/support/server_api/plugin.js +++ b/detox/e2e/support/server_api/plugin.js @@ -57,9 +57,7 @@ export const apiDisableNonPrepackagedPlugins = async () => { */ export const apiDisablePluginById = async (pluginId) => { try { - const response = await client.post(`/api/v4/plugins/${encodeURIComponent(pluginId)}/disable`); - - return response; + return await client.post(`/api/v4/plugins/${encodeURIComponent(pluginId)}/disable`); } catch (err) { return getResponseFromError(err); } @@ -73,9 +71,7 @@ export const apiDisablePluginById = async (pluginId) => { */ export const apiEnablePluginById = async (pluginId) => { try { - const response = await client.post(`/api/v4/plugins/${encodeURIComponent(pluginId)}/enable`); - - return response; + return await client.post(`/api/v4/plugins/${encodeURIComponent(pluginId)}/enable`); } catch (err) { return getResponseFromError(err); } @@ -121,9 +117,7 @@ export const apiInstallPluginFromUrl = async (pluginDownloadUrl, force = false) */ export const apiRemovePluginById = async (pluginId) => { try { - const response = await client.delete(`/api/v4/plugins/${encodeURIComponent(pluginId)}`); - - return response; + return await client.delete(`/api/v4/plugins/${encodeURIComponent(pluginId)}`); } catch (err) { return getResponseFromError(err); } @@ -138,9 +132,7 @@ export const apiRemovePluginById = async (pluginId) => { export const apiUploadPlugin = async (filename) => { try { const absFilePath = path.resolve(__dirname, `../../support/fixtures/${filename}`); - const response = await apiUploadFile('plugin', absFilePath, {url: '/api/v4/plugins', method: 'POST'}); - - return response; + return await apiUploadFile('plugin', absFilePath, {url: '/api/v4/plugins', method: 'POST'}); } catch (err) { return getResponseFromError(err); } diff --git a/detox/e2e/support/server_api/post.js b/detox/e2e/support/server_api/post.js index d40bb5c1e..144e1a5e1 100644 --- a/detox/e2e/support/server_api/post.js +++ b/detox/e2e/support/server_api/post.js @@ -74,10 +74,31 @@ export const apiGetLastPostInChannel = async (channelId) => { return {post: posts[0]}; }; +/** + * Patch a post. + * See https://api.mattermost.com/#operation/PatchPost + * @param {string} postId - the post ID + * @param {Object} postData - data to partially update a post + * @return {Object} returns {post} on success or {error, status} on error + */ +export const apiPatchPost = async (postId, postData) => { + try { + const response = await client.put( + `/api/v4/posts/${postId}/patch`, + postData, + ); + + return {post: response.data}; + } catch (err) { + return getResponseFromError(err); + } +}; + export const Post = { apiCreatePost, apiGetLastPostInChannel, apiGetPostsInChannel, + apiPatchPost, }; export default Post; diff --git a/detox/e2e/support/server_api/system.js b/detox/e2e/support/server_api/system.js index 8509a642e..e3fbfc9c5 100644 --- a/detox/e2e/support/server_api/system.js +++ b/detox/e2e/support/server_api/system.js @@ -42,8 +42,7 @@ export const apiCheckSystemHealth = async () => { */ export const apiEmailTest = async () => { try { - const response = await client.post('/api/v4/email/test'); - return response; + return await client.post('/api/v4/email/test'); } catch (err) { return getResponseFromError(err); } @@ -174,9 +173,7 @@ export const apiUpdateConfig = async (newConfig = {}) => { */ export const apiUploadLicense = async () => { const absFilePath = path.resolve(__dirname, '../../support/fixtures/mattermost-license.txt'); - const response = await apiUploadFile('license', absFilePath, {url: '/api/v4/license', method: 'POST'}); - - return response; + return apiUploadFile('license', absFilePath, {url: '/api/v4/license', method: 'POST'}); }; /** diff --git a/detox/e2e/support/server_api/user.js b/detox/e2e/support/server_api/user.js index c181ade09..f08561b48 100644 --- a/detox/e2e/support/server_api/user.js +++ b/detox/e2e/support/server_api/user.js @@ -155,14 +155,14 @@ export const apiLogin = async (user) => { /** * Logout from the Mattermost server. * See https://api.mattermost.com/#operation/Logout - * @return {Object} returns data on success + * @return {Object} returns {status} on success */ export const apiLogout = async () => { const response = await client.post('/api/v4/users/logout'); client.defaults.headers.Cookie = ''; - return response.data; + return {status: response.status}; }; /** diff --git a/detox/e2e/support/ui/component/post_list.js b/detox/e2e/support/ui/component/post_list.js index d2695a65e..3002183ed 100644 --- a/detox/e2e/support/ui/component/post_list.js +++ b/detox/e2e/support/ui/component/post_list.js @@ -6,10 +6,20 @@ import Post from './post'; class PostList { constructor(screenPrefix) { this.testID = { + moreMessagesButton: `${screenPrefix}post_list.more_messages_button`, + newMessagesDivider: `${screenPrefix}post_list.new_messages_divider`, postListPostItem: `${screenPrefix}post_list.post`, }; } + getMoreMessagesButton = () => { + return element(by.id(this.testID.moreMessagesButton)); + } + + getNewMessagesDivider = () => { + return element(by.id(this.testID.newMessagesDivider)); + } + getPost = (postId, postMessage, postProfileOptions = {}) => { const { postItem, diff --git a/detox/e2e/support/ui/screen/channel.js b/detox/e2e/support/ui/screen/channel.js index 6ee0479fb..fcb548ea3 100644 --- a/detox/e2e/support/ui/screen/channel.js +++ b/detox/e2e/support/ui/screen/channel.js @@ -69,6 +69,14 @@ class ChannelScreen { postList = new PostList(this.testID.channelScreenPrefix); + getMoreMessagesButton = () => { + return this.postList.getMoreMessagesButton(); + } + + getNewMessagesDivider = () => { + return this.postList.getNewMessagesDivider(); + } + getLongPostItem = (postId, text, postProfileOptions = {}) => { return LongPostScreen.getPost(postId, text, postProfileOptions); } diff --git a/detox/e2e/test/channel_moderation/remove_user_from_channel.e2e.js b/detox/e2e/test/channel_moderation/remove_user_from_channel.e2e.js index 24950656a..6758c8a8b 100644 --- a/detox/e2e/test/channel_moderation/remove_user_from_channel.e2e.js +++ b/detox/e2e/test/channel_moderation/remove_user_from_channel.e2e.js @@ -69,7 +69,7 @@ describe('Channel Moderation', () => { await ChannelInfoScreen.close(); // # Remove user while on channel - await Channel.apiDeleteUserFromChannel(testChannel.id, testUser.id); + await Channel.apiRemoveUserFromChannel(testChannel.id, testUser.id); // * Verify user is prompted removal alert const removeFromChannelTitleText = `Removed from ${testChannel.display_name}`; @@ -112,7 +112,7 @@ describe('Channel Moderation', () => { await goToChannel(townSquareChannel.display_name); // # Remove user while on channel - await Channel.apiDeleteUserFromChannel(testChannel.id, testOtherUser.id); + await Channel.apiRemoveUserFromChannel(testChannel.id, testOtherUser.id); // * Verify channel is not in the channels list anymore await openMainSidebar(); diff --git a/detox/e2e/test/messaging/mark_as_read.e2e.js b/detox/e2e/test/messaging/mark_as_read.e2e.js new file mode 100644 index 000000000..e43626079 --- /dev/null +++ b/detox/e2e/test/messaging/mark_as_read.e2e.js @@ -0,0 +1,87 @@ +// 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 jestExpect from 'expect'; + +import {MainSidebar} from '@support/ui/component'; +import {ChannelScreen} from '@support/ui/screen'; +import { + Channel, + Post, + Setup, +} from '@support/server_api'; + +describe('Mark As Read', () => { + const { + closeMainSidebar, + goToChannel, + openMainSidebar, + } = ChannelScreen; + const {hasChannelDisplayNameAtIndex} = MainSidebar; + let testChannel; + let testUser; + + beforeAll(async () => { + const {channel, user} = await Setup.apiInit(); + testChannel = channel; + testUser = user; + + // # Open channel screen + await ChannelScreen.open(testUser); + }); + + afterAll(async () => { + await ChannelScreen.logout(); + }); + + it('MM-T119 should mark message as read on mobile when message was read on webapp or api', async () => { + // # Post a new message to channel by sysadmin + await Post.apiCreatePost({ + channelId: testChannel.id, + message: Date.now().toString(), + }); + + // * Verify channel has unread message + await openMainSidebar(); + await expect(element(by.text('UNREADS'))).toBeVisible(); + await hasChannelDisplayNameAtIndex(0, testChannel.display_name); + + // # View channel on webapp or api + await closeMainSidebar(); + await Channel.apiViewChannel(testUser.id, testChannel.id); + + // * Verify message is read + await openMainSidebar(); + await expect(element(by.text('UNREADS'))).not.toBeVisible(); + await expect(element(by.text('PUBLIC CHANNELS'))).toBeVisible(); + await hasChannelDisplayNameAtIndex(0, testChannel.display_name); + + // # Go back to channel + await closeMainSidebar(); + }); + + it('MM-T120 should mark message as read on webapp or api when message was read on mobile', async () => { + // # Post a new message to channel by sysadmin + await Post.apiCreatePost({ + channelId: testChannel.id, + message: Date.now().toString(), + }); + + // * Verify unread messages on webapp or api is 1 + const beforeVisit = await Channel.apiGetUnreadMessages(testUser.id, testChannel.id); + jestExpect(beforeVisit.data.msg_count).toEqual(1); + + // # Visit channel on mobile + await goToChannel(testChannel.display_name); + + // * Verify unread messages on webapp or api is 0 + const afterVisit = await Channel.apiGetUnreadMessages(testUser.id, testChannel.id); + jestExpect(afterVisit.data.msg_count).toEqual(0); + }); +}); diff --git a/detox/e2e/test/messaging/mark_as_unread.e2e.js b/detox/e2e/test/messaging/mark_as_unread.e2e.js new file mode 100644 index 000000000..15825f14b --- /dev/null +++ b/detox/e2e/test/messaging/mark_as_unread.e2e.js @@ -0,0 +1,167 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// ******************************************************************* +// - [#] indicates a test step (e.g. # Go to a screen) +// - [*] indicates an assertion (e.g. * Check the title) +// - Use element testID when selecting an element. Create one if none. +// ******************************************************************* + +import { + MainSidebar, + PostOptions, +} from '@support/ui/component'; +import {ChannelScreen} from '@support/ui/screen'; +import { + Channel, + Post, + Setup, + Team, + User, +} from '@support/server_api'; + +describe('Mark as Unread', () => { + const { + closeMainSidebar, + getNewMessagesDivider, + goToChannel, + mainSidebarDrawerButtonBadgeUnreadIndicator, + openMainSidebar, + openTeamSidebar, + openPostOptionsFor, + postMessage, + } = ChannelScreen; + const { + getTeamByDisplayName, + hasChannelDisplayNameAtIndex, + switchTeamsButtonBadgeUnreadIndicator, + } = MainSidebar; + let testChannel; + let townSquareChannel; + let testTeam1; + let testTeam2; + let testUser; + + beforeAll(async () => { + const {channel, team, user} = await Setup.apiInit({teamOptions: {prefix: 'team-a'}}); + testChannel = channel; + testTeam1 = team; + testUser = user; + + ({channel: townSquareChannel} = await Channel.apiGetChannelByName(testTeam1.id, 'town-square')); + + ({team: testTeam2} = await Team.apiCreateTeam({prefix: 'team-b'})); + await Team.apiAddUserToTeam(testUser.id, testTeam2.id); + + // # Open channel screen + await ChannelScreen.open(testUser); + }); + + afterAll(async () => { + await ChannelScreen.logout(); + }); + + it('MM-T245 should be able to mark a post as unread', async () => { + // # Switch channels + await goToChannel(testChannel.display_name); + await goToChannel(townSquareChannel.display_name); + + // # Post a message + const message = Date.now().toString(); + await postMessage(message); + + // # Mark post as unread + const {post} = await Post.apiGetLastPostInChannel(townSquareChannel.id); + await openPostOptionsFor(post.id, message); + await PostOptions.markUnreadAction.tap(); + + // * Verify new messages divider is visible + await expect(getNewMessagesDivider()).toBeVisible(); + + // * Verify drawer button badge unread indicator + await expect(mainSidebarDrawerButtonBadgeUnreadIndicator).toExist(); + + // * Verify team item badge unread indicator + await openTeamSidebar(); + const {teamItemBadgeUnreadIndicator} = await MainSidebar.getTeam(testTeam1.id); + await expect(teamItemBadgeUnreadIndicator).toExist(); + + // # Switch teams + await getTeamByDisplayName(testTeam2.display_name).tap(); + + // * Verify switch teams button badge unread indicator + await openMainSidebar(); + await expect(switchTeamsButtonBadgeUnreadIndicator).toExist(); + + // # Go back to channel + await closeMainSidebar(); + }); + + it('MM-T258 should maintain unread state on app reopen', async () => { + // # Go to team 1 test channel + await openTeamSidebar(); + await getTeamByDisplayName(testTeam1.display_name).tap(); + await goToChannel(testChannel.display_name); + + // # Post a message + const message = Date.now().toString(); + await postMessage(message); + + // # Mark post as unread and switch channels + const {post} = await Post.apiGetLastPostInChannel(testChannel.id); + await openPostOptionsFor(post.id, message); + await PostOptions.markUnreadAction.tap(); + await goToChannel(townSquareChannel.display_name); + + // * Verify channel has unread message + await openMainSidebar(); + await expect(element(by.text('UNREADS'))).toBeVisible(); + await hasChannelDisplayNameAtIndex(0, testChannel.display_name); + + // # Send app to home and relaunch + await closeMainSidebar(); + await device.sendToHome(); + await device.launchApp({newInstance: false}); + + // * Verify unread state is maintained + await openMainSidebar(); + await expect(element(by.text('UNREADS'))).toBeVisible(); + await hasChannelDisplayNameAtIndex(0, testChannel.display_name); + + // # Go back to channel + await closeMainSidebar(); + }); + + it('MM-T245 should be able to mark a DM post as unread', async () => { + const {user: dmOtherUser} = await User.apiCreateUser(); + await Team.apiAddUserToTeam(dmOtherUser.id, testTeam1.id); + const {channel: directMessageChannel} = await Channel.apiCreateDirectChannel([testUser.id, dmOtherUser.id]); + + // # Post DM from other user + const message = Date.now().toString(); + await User.apiLogin(dmOtherUser); + const {post} = await Post.apiCreatePost({ + channelId: directMessageChannel.id, + message, + }); + + // # Switch channels + await goToChannel(testChannel.display_name); + await goToChannel(dmOtherUser.username); + + // # Mark DM post as unread + await openPostOptionsFor(post.id, message); + await PostOptions.markUnreadAction.tap(); + + // * Verify new messages divider is visible + await expect(getNewMessagesDivider()).toBeVisible(); + + // * Verify channel has unread message + await openMainSidebar(); + await expect(element(by.text('UNREADS'))).toBeVisible(); + await hasChannelDisplayNameAtIndex(0, dmOtherUser.username); + + // # Go back to channel + await closeMainSidebar(); + }); +});