From f495e1e48d022559847dfc9d961261df72013091 Mon Sep 17 00:00:00 2001 From: Joseph Baylon Date: Wed, 10 Feb 2021 13:55:00 -0800 Subject: [PATCH] MM-30289 Detox/E2E - Add e2e tests for MM-T3184 and MM-T3186 (#5133) * MM-30289 Detox/E2E - Add e2e tests for MM-T3184 and MM-T3186 * Update detox/e2e/support/server_api/preference.js Co-authored-by: Saturnino Abril * MM-31322: redirect permalink handler (#5136) * invert loadTeam.error test * MM-31322: handle _redirect permalinks In the webapp, [there is support](https://github.com/mattermost/mattermost-webapp/blob/7c5a2e3bfdf7bb66ac88642e3b426152bbc1fceb/components/root/root.jsx#L345-L352) for a special `_redirect` team name in certain contexts that simply means, "the current team". It's used in the context of redirecting to the integrations backstage (not relevant for mobile), for also for resolving post permalinks. This simplifies certain kinds of integrations which no longer have to know a team name to render a permanent link to a post. An example link looks like: http://localhost:8065/_redirect/pl/e3sxrxtwh78jmxawsaqfemxoew This PR adds support for same in mobile, unintentionally missed on the first round of implementation. Fixes: https://mattermost.atlassian.net/browse/MM-31322 * Apply suggestions from code review Co-authored-by: Joseph Baylon * linting issues * Apply suggestions from code review Co-authored-by: Joseph Baylon * revert showPermalink changes * Revert "invert loadTeam.error test" This reverts commit accf6c8fb7c56009a393a6e85084d4702deec9ed. * support _redirect deeplinks * Apply suggestions from code review * Update app/components/post_list/index.js Co-authored-by: Joseph Baylon Co-authored-by: Saturnino Abril Co-authored-by: Jesse Hallam --- .../markdown/markdown_link/index.js | 2 + .../markdown/markdown_link/markdown_link.js | 9 +- app/components/post_list/index.js | 2 + app/components/post_list/post_list.js | 12 +- app/utils/url.js | 2 + detox/e2e/support/server_api/channel.js | 38 +++++ detox/e2e/support/server_api/preference.js | 18 +++ .../e2e/support/ui/component/channels_list.js | 9 ++ .../e2e/support/ui/component/main_sidebar.js | 4 + detox/e2e/test/messaging/permalink.e2e.js | 87 ++++++++++ detox/e2e/test/smoke_test/channels.e2e.js | 151 ++++++++++++++++++ 11 files changed, 329 insertions(+), 5 deletions(-) create mode 100644 detox/e2e/test/messaging/permalink.e2e.js create mode 100644 detox/e2e/test/smoke_test/channels.e2e.js diff --git a/app/components/markdown/markdown_link/index.js b/app/components/markdown/markdown_link/index.js index e30878f02..881c7fa90 100644 --- a/app/components/markdown/markdown_link/index.js +++ b/app/components/markdown/markdown_link/index.js @@ -5,6 +5,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getConfig, getCurrentUrl} from '@mm-redux/selectors/entities/general'; +import {getCurrentTeam} from '@mm-redux/selectors/entities/teams'; import {handleSelectChannelByName} from 'app/actions/views/channel'; import MarkdownLink from './markdown_link'; @@ -13,6 +14,7 @@ function mapStateToProps(state) { return { serverURL: getCurrentUrl(state), siteURL: getConfig(state).SiteURL, + currentTeamName: getCurrentTeam(state)?.name, }; } diff --git a/app/components/markdown/markdown_link/markdown_link.js b/app/components/markdown/markdown_link/markdown_link.js index 67aa5ca77..be3ea8070 100644 --- a/app/components/markdown/markdown_link/markdown_link.js +++ b/app/components/markdown/markdown_link/markdown_link.js @@ -15,7 +15,7 @@ import {getCurrentServerUrl} from '@init/credentials'; import BottomSheet from '@utils/bottom_sheet'; import {errorBadChannel} from '@utils/draft'; import {preventDoubleTap} from '@utils/tap'; -import {matchDeepLink, normalizeProtocol, tryOpenURL} from '@utils/url'; +import {matchDeepLink, normalizeProtocol, tryOpenURL, PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url'; import mattermostManaged from 'app/mattermost_managed'; @@ -29,6 +29,7 @@ export default class MarkdownLink extends PureComponent { onPermalinkPress: PropTypes.func, serverURL: PropTypes.string, siteURL: PropTypes.string.isRequired, + currentTeamName: PropTypes.string, }; static defaultProps = { @@ -61,7 +62,11 @@ export default class MarkdownLink extends PureComponent { const {intl} = this.context; this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, errorBadChannel.bind(null, intl)); } else if (match.type === DeepLinkTypes.PERMALINK) { - onPermalinkPress(match.postId, match.teamName); + if (match.teamName === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) { + onPermalinkPress(match.postId, this.props.currentTeamName); + } else { + onPermalinkPress(match.postId, match.teamName); + } } } else { const onError = () => { diff --git a/app/components/post_list/index.js b/app/components/post_list/index.js index 535602439..3c1580a8f 100644 --- a/app/components/post_list/index.js +++ b/app/components/post_list/index.js @@ -8,6 +8,7 @@ import {closePermalink, showPermalink} from '@actions/views/permalink'; import {getConfig, getCurrentUrl} from '@mm-redux/selectors/entities/general'; import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {makePreparePostIdsForPostList, START_OF_NEW_MESSAGES} from '@mm-redux/utils/post_list'; +import {getCurrentTeam} from '@mm-redux/selectors/entities/teams'; import {handleSelectChannelByName, refreshChannelWithRetry} from 'app/actions/views/channel'; import {setDeepLinkURL} from 'app/actions/views/root'; @@ -30,6 +31,7 @@ function makeMapStateToProps() { serverURL: getCurrentUrl(state), siteURL: getConfig(state).SiteURL, theme: getTheme(state), + currentTeamName: getCurrentTeam(state)?.name, }; }; } diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index 85c800d13..ee1960682 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -16,7 +16,7 @@ import Post from 'app/components/post'; import {DeepLinkTypes, ListTypes, NavigationTypes} from '@constants'; import mattermostManaged from 'app/mattermost_managed'; import {makeExtraData} from 'app/utils/list_view'; -import {matchDeepLink} from 'app/utils/url'; +import {matchDeepLink, PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from 'app/utils/url'; import telemetry from 'app/telemetry'; import DateHeader from './date_header'; @@ -71,6 +71,7 @@ export default class PostList extends PureComponent { location: PropTypes.string, scrollViewNativeID: PropTypes.string, showMoreMessagesButton: PropTypes.bool, + currentTeamName: PropTypes.string, }; static defaultProps = { @@ -81,6 +82,7 @@ export default class PostList extends PureComponent { siteURL: '', postIds: [], showMoreMessagesButton: false, + currentTeamName: '', }; static contextTypes = { @@ -181,7 +183,7 @@ export default class PostList extends PureComponent { }; handleDeepLink = (url) => { - const {serverURL, siteURL} = this.props; + const {serverURL, siteURL, currentTeamName} = this.props; const match = matchDeepLink(url, serverURL, siteURL); @@ -190,7 +192,11 @@ export default class PostList extends PureComponent { const {intl} = this.context; this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, errorBadChannel(intl)); } else if (match.type === DeepLinkTypes.PERMALINK) { - this.handlePermalinkPress(match.postId, match.teamName); + if (match.teamName === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) { + this.handlePermalinkPress(match.postId, currentTeamName); + } else { + this.handlePermalinkPress(match.postId, match.teamName); + } } } else { const {formatMessage} = this.context.intl; diff --git a/app/utils/url.js b/app/utils/url.js index e4b1618ec..3917564f5 100644 --- a/app/utils/url.js +++ b/app/utils/url.js @@ -102,6 +102,8 @@ export function getScheme(url) { return match && match[1]; } +export const PERMALINK_GENERIC_TEAM_NAME_REDIRECT = '_redirect'; + export function matchDeepLink(url, serverURL, siteURL) { if (!url || (!serverURL && !siteURL)) { return null; diff --git a/detox/e2e/support/server_api/channel.js b/detox/e2e/support/server_api/channel.js index f5643d3d6..0e231a8f4 100644 --- a/detox/e2e/support/server_api/channel.js +++ b/detox/e2e/support/server_api/channel.js @@ -40,6 +40,42 @@ export const apiCreateChannel = async ({teamId = null, type = 'O', prefix = 'cha } }; +/** + * Create a direct message channel. + * See https://api.mattermost.com/#tag/channels/paths/~1channels~1direct/post + * @param {Array} userIds - the two user IDs to be in the direct message + */ +export const apiCreateDirectChannel = async (userIds = []) => { + try { + const response = await client.post( + '/api/v4/channels/direct', + userIds, + ); + + return {channel: response.data}; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Create a group message channel. + * See https://api.mattermost.com/#tag/channels/paths/~1channels~1group/post + * @param {Array} userIds - user IDs to be in the group message channel + */ +export const apiCreateGroupChannel = async (userIds = []) => { + try { + const response = await client.post( + '/api/v4/channels/group', + userIds, + ); + + return {channel: response.data}; + } catch (err) { + return getResponseFromError(err); + } +}; + /** * Get a channel by name and team name. * See https://api.mattermost.com/#tag/channels/paths/~1teams~1name~1{team_name}~1channels~1name~1{channel_name}/get @@ -129,6 +165,8 @@ function generateRandomChannel(teamId, type, prefix) { export const Channel = { apiAddUserToChannel, apiCreateChannel, + apiCreateDirectChannel, + apiCreateGroupChannel, apiDeleteUserFromChannel, apiGetChannelByName, apiGetChannelsForUser, diff --git a/detox/e2e/support/server_api/preference.js b/detox/e2e/support/server_api/preference.js index a84e7cfba..4a1c13f2d 100644 --- a/detox/e2e/support/server_api/preference.js +++ b/detox/e2e/support/server_api/preference.js @@ -35,6 +35,23 @@ export const apiSaveUserPreferences = async (userId, preferences = []) => { } }; +/** + * Save the user's favorite channel preference. + * @param {string} userId - the user ID + * @param {string} channelId - the channel id to be favorited + * @return {string} returns {status} on success or {error, status} on error + */ +export const apiSaveFavoriteChannelPreference = (userId, channelId) => { + const preference = { + user_id: userId, + category: 'favorite_channel', + name: channelId, + value: 'true', + }; + + return apiSaveUserPreferences(userId, [preference]); +}; + /** * Save the user's teams order preference. * @param {string} userId - the user ID @@ -54,6 +71,7 @@ export const apiSaveTeamsOrderPreference = (userId, orderedTeamIds = []) => { export const Preference = { apiSaveUserPreferences, + apiSaveFavoriteChannelPreference, apiSaveTeamsOrderPreference, }; diff --git a/detox/e2e/support/ui/component/channels_list.js b/detox/e2e/support/ui/component/channels_list.js index b541f1869..ff34da000 100644 --- a/detox/e2e/support/ui/component/channels_list.js +++ b/detox/e2e/support/ui/component/channels_list.js @@ -1,8 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import SearchBar from './search_bar'; + class ChannelsList { testID = { + channelsListPrefix: 'main.sidebar.channels_list.', channelsList: 'main.sidebar.channels_list.list', channelItem: 'main.sidebar.channels_list.list.channel_item', channelItemDisplayName: 'main.sidebar.channels_list.list.channel_item.display_name', @@ -22,6 +25,12 @@ class ChannelsList { switchTeamsButtonBadgeUnreadCount = element(by.id(this.testID.switchTeamsButtonBadgeUnreadCount)); switchTeamsButtonBadgeUnreadIndicator = element(by.id(this.testID.switchTeamsButtonBadgeUnreadIndicator)); + // convenience props + searchBar = SearchBar.getSearchBar(this.testID.channelsListPrefix); + searchInput = SearchBar.getSearchInput(this.testID.channelsListPrefix); + cancelButton = SearchBar.getCancelButton(this.testID.channelsListPrefix); + clearButton = SearchBar.getClearButton(this.testID.channelsListPrefix); + getChannelItem = (channelId, displayName) => { const channelItemTestID = `${this.testID.channelItem}.${channelId}`; const baseMatcher = by.id(channelItemTestID); diff --git a/detox/e2e/support/ui/component/main_sidebar.js b/detox/e2e/support/ui/component/main_sidebar.js index cf650be01..92344e6d8 100644 --- a/detox/e2e/support/ui/component/main_sidebar.js +++ b/detox/e2e/support/ui/component/main_sidebar.js @@ -18,6 +18,10 @@ class MainSidebar { openMoreDirectMessagesButton = element(by.id(this.testID.openMoreDirectMessagesButton)); // convenience props + searchBar = ChannelsList.searchBar; + searchInput = ChannelsList.searchInput; + cancelButton = ChannelsList.cancelButton; + clearButton = ChannelsList.clearButton; channelsList = ChannelsList.channelsList; filteredChannelsList = ChannelsList.filteredChannelsList; switchTeamsButton = ChannelsList.switchTeamsButton; diff --git a/detox/e2e/test/messaging/permalink.e2e.js b/detox/e2e/test/messaging/permalink.e2e.js new file mode 100644 index 000000000..f30dd2e1a --- /dev/null +++ b/detox/e2e/test/messaging/permalink.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 {MainSidebar} from '@support/ui/component'; +import {ChannelScreen, PermalinkScreen} from '@support/ui/screen'; +import {User, Setup, Channel, Post} from '@support/server_api'; + +describe('Permalink', () => { + let testUser; + let testChannel; + let townSquareChannel; + + beforeAll(async () => { + const {user, team, channel} = await Setup.apiInit(); + testUser = user; + testChannel = channel; + ({channel: townSquareChannel} = await Channel.apiGetChannelByName(team.name, 'town-square')); + + // # Open channel screen + await ChannelScreen.open(user); + }); + + afterAll(async () => { + await ChannelScreen.logout(); + }); + + const expectPermalinkTargetMessage = async (permalinkTargetPost, permalinkTargetChannel) => { + // # Post a message in the test channel referencing the given permalink. + const permalinkLabel = `permalink-${Date.now().toString()}`; + const permalinkMessage = `[${permalinkLabel}](/_redirect/pl/${permalinkTargetPost.id})`; + await Post.apiCreatePost({ + channelId: testChannel.id, + message: permalinkMessage, + }); + + // # Go to test channel + await ChannelScreen.openMainSidebar(); + await MainSidebar.getChannelByDisplayName(testChannel.display_name).tap(); + await ChannelScreen.toBeVisible(); + + // # Tap the channel permalink + await element(by.text(permalinkLabel)).tap({x: 5, y: 10}); + + // * Verify permalink post list has the expected target message + await PermalinkScreen.toBeVisible(); + const {postListPostItem: permalinkPostItem} = await PermalinkScreen.getPostListPostItem(permalinkTargetPost.id, permalinkTargetPost.message); + await expect(permalinkPostItem).toBeVisible(); + + // # Dismiss the permalink screen by jumping to recent messages + await PermalinkScreen.jumpToRecentMessages(); + + // * Verify user is on channel where message is posted + await expect(ChannelScreen.channelNavBarTitle).toHaveText(permalinkTargetChannel.display_name); + const {postListPostItem: channelPostItem} = await ChannelScreen.getPostListPostItem(permalinkTargetPost.id, permalinkTargetPost.message); + await expect(channelPostItem).toBeVisible(); + }; + + it('MM-T3805_1 should support _redirect to public channel post', async () => { + // # Post a test message in a public channel + const permalinkTargetMessage = 'post in Town Square'; + const permalinkTargetPost = await Post.apiCreatePost({ + channelId: townSquareChannel.id, + message: permalinkTargetMessage, + }); + + await expectPermalinkTargetMessage(permalinkTargetPost.post, townSquareChannel); + }); + + it('MM-T3805_2 should support _redirect to DM post', async () => { + // # Post a test message in a DM + const {user: dmOtherUser} = await User.apiCreateUser({prefix: 'testchannel-1'}); + const {channel: directMessageChannel} = await Channel.apiCreateDirectChannel([testUser.id, dmOtherUser.id]); + const permalinkTargetMessage = 'post in DM'; + const permalinkTargetPost = await Post.apiCreatePost({ + channelId: directMessageChannel.id, + message: permalinkTargetMessage, + }); + + await expectPermalinkTargetMessage(permalinkTargetPost.post, directMessageChannel); + }); +}); diff --git a/detox/e2e/test/smoke_test/channels.e2e.js b/detox/e2e/test/smoke_test/channels.e2e.js new file mode 100644 index 000000000..84a32c327 --- /dev/null +++ b/detox/e2e/test/smoke_test/channels.e2e.js @@ -0,0 +1,151 @@ +// 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} from '@support/ui/component'; +import {ChannelScreen} from '@support/ui/screen'; +import { + Channel, + Post, + Preference, + Setup, + Team, + User, +} from '@support/server_api'; + +describe('Channels', () => { + let testMessage; + let unreadChannel; + let favoriteChannel; + let publicChannel; + let privateChannel; + let nonJoinedChannel; + let directMessageChannel; + let dmOtherUser; + let nonDmOtherUser; + const { + channelNavBarTitle, + closeMainSidebar, + openMainSidebar, + } = ChannelScreen; + const { + getChannelByDisplayName, + getFilteredChannelByDisplayName, + hasChannelDisplayNameAtIndex, + hasFilteredChannelDisplayNameAtIndex, + searchInput, + } = MainSidebar; + + beforeAll(async () => { + const {user, channel, team} = await Setup.apiInit(); + unreadChannel = channel; + testMessage = `Mention @${user.username}`; + await Post.apiCreatePost({ + channelId: unreadChannel.id, + message: testMessage, + }); + + ({channel: favoriteChannel} = await Channel.apiCreateChannel({type: 'O', prefix: '4-favorite-channel', teamId: team.id})); + await Channel.apiAddUserToChannel(user.id, favoriteChannel.id); + await Preference.apiSaveFavoriteChannelPreference(user.id, favoriteChannel.id); + + ({channel: publicChannel} = await Channel.apiCreateChannel({type: 'O', prefix: '3-public-channel', teamId: team.id})); + await Channel.apiAddUserToChannel(user.id, publicChannel.id); + + ({channel: privateChannel} = await Channel.apiCreateChannel({type: 'P', prefix: '2-private-channel', teamId: team.id})); + await Channel.apiAddUserToChannel(user.id, privateChannel.id); + + ({channel: nonJoinedChannel} = await Channel.apiCreateChannel({type: 'O', prefix: '1-non-joined-channel', teamId: team.id})); + + ({user: dmOtherUser} = await User.apiCreateUser({prefix: 'testchannel-1'})); + ({channel: directMessageChannel} = await Channel.apiCreateDirectChannel([user.id, dmOtherUser.id])); + await Post.apiCreatePost({ + channelId: directMessageChannel.id, + message: testMessage, + }); + + ({user: nonDmOtherUser} = await User.apiCreateUser({prefix: 'testchannel-2'})); + await Team.apiAddUserToTeam(nonDmOtherUser.id, team.id); + + // # Open channel screen + await ChannelScreen.open(user); + }); + + afterAll(async () => { + await ChannelScreen.logout(); + }); + + it('MM-T3184 should display unfiltered channels list', async () => { + // # Open main sidebar + await openMainSidebar(); + + // * Verify order when all channels are unread + await hasChannelDisplayNameAtIndex(0, privateChannel.display_name); + await hasChannelDisplayNameAtIndex(1, publicChannel.display_name); + await hasChannelDisplayNameAtIndex(2, favoriteChannel.display_name); + await hasChannelDisplayNameAtIndex(3, unreadChannel.display_name); + await hasChannelDisplayNameAtIndex(4, dmOtherUser.username); + await hasChannelDisplayNameAtIndex(5, 'Off-Topic'); + await hasChannelDisplayNameAtIndex(6, 'Town Square'); + await expect(element(by.id(nonJoinedChannel.display_name))).not.toBeVisible(); + await expect(element(by.id(nonDmOtherUser.username))).not.toBeVisible(); + + // # Visit private, public, favorite, and direct message channels + await getChannelByDisplayName(privateChannel.display_name).tap(); + await openMainSidebar(); + await getChannelByDisplayName(publicChannel.display_name).tap(); + await openMainSidebar(); + await getChannelByDisplayName(favoriteChannel.display_name).tap(); + await openMainSidebar(); + await getChannelByDisplayName(dmOtherUser.username).tap(); + await openMainSidebar(); + + // * Verify order when all channels are read except for unread channel + await hasChannelDisplayNameAtIndex(0, unreadChannel.display_name); + await hasChannelDisplayNameAtIndex(1, favoriteChannel.display_name); + await hasChannelDisplayNameAtIndex(2, publicChannel.display_name); + await hasChannelDisplayNameAtIndex(3, 'Off-Topic'); + await hasChannelDisplayNameAtIndex(4, 'Town Square'); + await hasChannelDisplayNameAtIndex(5, privateChannel.display_name); + await hasChannelDisplayNameAtIndex(6, dmOtherUser.username); + await expect(element(by.id(nonJoinedChannel.display_name))).not.toBeVisible(); + await expect(element(by.id(nonDmOtherUser.username))).not.toBeVisible(); + + // # Close main sidebar + await closeMainSidebar(); + }); + + it('MM-T3186 should display filtered channels list and be able to change channels', async () => { + // # Open main sidebar + await openMainSidebar(); + + // # Enter search term + await searchInput.typeText('channel'); + await searchInput.tapBackspaceKey(); + + // * Verify order when channels list is filtered + await hasFilteredChannelDisplayNameAtIndex(0, unreadChannel.display_name); + await hasFilteredChannelDisplayNameAtIndex(1, dmOtherUser.username); + await hasFilteredChannelDisplayNameAtIndex(2, privateChannel.display_name); + await hasFilteredChannelDisplayNameAtIndex(3, publicChannel.display_name); + await hasFilteredChannelDisplayNameAtIndex(4, favoriteChannel.display_name); + await hasFilteredChannelDisplayNameAtIndex(5, nonDmOtherUser.username); + await hasFilteredChannelDisplayNameAtIndex(6, nonJoinedChannel.display_name); + await expect(element(by.text('Off-Topic'))).not.toBeVisible(); + await expect(element(by.text('Town Square'))).not.toBeVisible(); + + // # Tap a channel from filtered list + await getFilteredChannelByDisplayName(unreadChannel.display_name).tap(); + + // * Verify filtered channel opens + await expect(channelNavBarTitle).toHaveText(unreadChannel.display_name); + + // # Close main sidebar + await closeMainSidebar(); + }); +});