MM-35670 Detox/E2E: Add e2e for mark post unread and read (#5396)

This commit is contained in:
Joseph Baylon 2021-05-20 08:16:52 -07:00 committed by GitHub
parent 025b019840
commit d464f102fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 373 additions and 51 deletions

View file

@ -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}
>
<View style={[styles.container, styles.roundBorder]}>
<View style={styles.iconContainer}>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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};
};
/**

View file

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

View file

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

View file

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

View file

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

View file

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