diff --git a/app/components/custom_list/channel_list_row/channel_list_row.js b/app/components/custom_list/channel_list_row/channel_list_row.js index b2086d387..6736407bc 100644 --- a/app/components/custom_list/channel_list_row/channel_list_row.js +++ b/app/components/custom_list/channel_list_row/channel_list_row.js @@ -41,6 +41,10 @@ export default class ChannelListRow extends React.PureComponent { ); } + const testID = this.props.testID; + const itemTestID = `${testID}.${this.props.id}`; + const channelDisplayNameTestID = `${testID}.display_name`; + return ( - + - + {this.props.channel.display_name} diff --git a/app/screens/channel_info/__snapshots__/channel_info.test.js.snap b/app/screens/channel_info/__snapshots__/channel_info.test.js.snap index 09342b100..d9e8a6c2a 100644 --- a/app/screens/channel_info/__snapshots__/channel_info.test.js.snap +++ b/app/screens/channel_info/__snapshots__/channel_info.test.js.snap @@ -24,6 +24,7 @@ exports[`channelInfo should match snapshot 1`] = ` "flex": 1, } } + testID="channel_info.scroll_view" > Promise; selectPenultimateChannel: (channelId: string) => Promise; teamId: string; + testID?: string; theme: Theme; viewArchivedChannels: boolean; } @@ -129,7 +130,7 @@ export default class Archive extends PureComponent { }); render() { - const {canArchive, canUnarchive, theme} = this.props; + const {canArchive, canUnarchive, testID, theme} = this.props; if (!canArchive && !canUnarchive) { return null; @@ -142,9 +143,10 @@ export default class Archive extends PureComponent { action={this.handleUnarchive} defaultMessage='Unarchive Channel' icon='archive-arrow-up-outline' + rightArrow={false} + testID={testID} textId={t('mobile.routes.channelInfo.unarchive_channel')} theme={theme} - rightArrow={false} /> ); } else { @@ -154,10 +156,11 @@ export default class Archive extends PureComponent { defaultMessage='Archive Channel' iconColor='#CA3B27' icon='archive-outline' + rightArrow={false} + testID={testID} textId={t('mobile.routes.channelInfo.delete_channel')} textColor='#CA3B27' theme={theme} - rightArrow={false} /> ); } diff --git a/app/screens/channel_info/channel_info.js b/app/screens/channel_info/channel_info.js index 742423b24..9fcc3af88 100644 --- a/app/screens/channel_info/channel_info.js +++ b/app/screens/channel_info/channel_info.js @@ -186,6 +186,7 @@ export default class ChannelInfo extends PureComponent { {Boolean(currentChannel?.id) && diff --git a/app/screens/channel_info/leave/leave.tsx b/app/screens/channel_info/leave/leave.tsx index 120b2c6de..1553de9a1 100644 --- a/app/screens/channel_info/leave/leave.tsx +++ b/app/screens/channel_info/leave/leave.tsx @@ -26,6 +26,7 @@ interface LeaveProps { isFavorite: boolean; isGroupMessage: boolean; isPublic: boolean; + testID?: string; theme: Theme; } @@ -91,7 +92,7 @@ export default class Leave extends PureComponent { }); render() { - const {canLeave, isDirectMessage, isGroupMessage, theme} = this.props; + const {canLeave, isDirectMessage, isGroupMessage, testID, theme} = this.props; if (!canLeave && !isDirectMessage && !isGroupMessage) { return null; @@ -115,10 +116,11 @@ export default class Leave extends PureComponent { defaultMessage={defaultMessage} icon='close' iconColor='#CA3B27' + rightArrow={false} + testID={testID} textColor='#CA3B27' textId={i18nId} theme={theme} - rightArrow={false} /> ); } else { @@ -128,10 +130,11 @@ export default class Leave extends PureComponent { defaultMessage='Leave Channel' icon='exit-to-app' iconColor='#CA3B27' + rightArrow={false} + testID={testID} textColor='#CA3B27' textId={t('navbar.leave')} theme={theme} - rightArrow={false} /> ); } diff --git a/app/screens/more_channels/__snapshots__/more_channels.test.js.snap b/app/screens/more_channels/__snapshots__/more_channels.test.js.snap index dd92518b9..11e8e307d 100644 --- a/app/screens/more_channels/__snapshots__/more_channels.test.js.snap +++ b/app/screens/more_channels/__snapshots__/more_channels.test.js.snap @@ -57,7 +57,9 @@ exports[`MoreChannels should match snapshot 1`] = ` value="" /> - + ); } @@ -438,11 +440,15 @@ export default class MoreChannels extends PureComponent { let channelDropdown; if (canShowArchivedChannels) { channelDropdown = ( - + {typeOfChannels === 'public' ? publicChannelsText : archivedChannelsText} {' '} @@ -492,6 +498,7 @@ export default class MoreChannels extends PureComponent { onLoadMore={more} onRowPress={this.onSelectChannel} renderItem={this.renderItem} + testID='more_channels.custom_list' theme={theme} /> diff --git a/detox/e2e/support/ui/component/alert.js b/detox/e2e/support/ui/component/alert.js index 0385d08c7..ab007dd8d 100644 --- a/detox/e2e/support/ui/component/alert.js +++ b/detox/e2e/support/ui/component/alert.js @@ -5,11 +5,14 @@ import {isAndroid} from '@support/utils'; class Alert { // alert titles + archivePublicChannelTitle = isAndroid() ? element(by.text('Archive Public Channel')) : element(by.label('Archive Public Channel')).atIndex(0); deletePostTitle = isAndroid() ? element(by.text('Delete Post')) : element(by.label('Delete Post')).atIndex(0); // alert buttons cancelButton = isAndroid() ? element(by.text('CANCEL')) : element(by.label('Cancel')).atIndex(0); deleteButton = isAndroid() ? element(by.text('DELETE')) : element(by.label('Delete')).atIndex(0); + noButton = isAndroid() ? element(by.text('NO')) : element(by.label('No')).atIndex(0); + yesButton = isAndroid() ? element(by.text('YES')) : element(by.label('Yes')).atIndex(0); } const alert = new Alert(); diff --git a/detox/e2e/support/ui/component/bottom_sheet.js b/detox/e2e/support/ui/component/bottom_sheet.js new file mode 100644 index 000000000..95cfc4fc0 --- /dev/null +++ b/detox/e2e/support/ui/component/bottom_sheet.js @@ -0,0 +1,17 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {isAndroid} from '@support/utils'; + +class BottomSheet { + // bottom sheet titles + showTitle = isAndroid() ? element(by.text('Show')) : element(by.label('Show')).atIndex(0); + + // bottom sheet options + archivedChannelsOption = isAndroid() ? element(by.text('Archived Channels')) : element(by.label('Archived Channels')).atIndex(0); + cancelOption = isAndroid() ? element(by.text('Cancel')) : element(by.label('Cancel')).atIndex(0); + publicChannelsOption = isAndroid() ? element(by.text('Public Channels')) : element(by.label('Public Channels')).atIndex(0); +} + +const bottomSheet = new BottomSheet(); +export default bottomSheet; diff --git a/detox/e2e/support/ui/component/index.js b/detox/e2e/support/ui/component/index.js index 9b413d7ef..ec59e1615 100644 --- a/detox/e2e/support/ui/component/index.js +++ b/detox/e2e/support/ui/component/index.js @@ -3,6 +3,7 @@ import Alert from './alert'; import Autocomplete from './autocomplete'; +import BottomSheet from './bottom_sheet'; import CameraQuickAction from './camera_quick_action'; import EditChannelInfo from './edit_channel_info'; import FileQuickAction from './file_quick_action'; @@ -23,6 +24,7 @@ import SettingsSidebar from './settings_sidebar'; export { Alert, Autocomplete, + BottomSheet, CameraQuickAction, EditChannelInfo, FileQuickAction, diff --git a/detox/e2e/support/ui/screen/channel_info.js b/detox/e2e/support/ui/screen/channel_info.js index 219c68348..d5dcb5eef 100644 --- a/detox/e2e/support/ui/screen/channel_info.js +++ b/detox/e2e/support/ui/screen/channel_info.js @@ -1,12 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {Alert} from '@support/ui/component'; import {ChannelScreen} from '@support/ui/screen'; import {timeouts, wait} from '@support/utils'; class ChannelInfoScreen { testID = { channelInfoScreen: 'channel_info.screen', + channelInfoScrollView: 'channel_info.scroll_view', closeChannelInfoButton: 'close.channel_info.button', channelIconGMMemberCount: 'channel_icon.gm_member_count', favoritePreferenceAction: 'channel_info.favorite.action', @@ -24,9 +26,12 @@ class ChannelInfoScreen { addMembersAction: 'channel_info.add_members.action', convertPrivateAction: 'channel_info.convert_private.action', editChannelAction: 'channel_info.edit_channel.action', + leaveAction: 'channel_info.leave.action', + archiveAction: 'channel_info.archive.action', } channelInfoScreen = element(by.id(this.testID.channelInfoScreen)); + channelInfoScrollView = element(by.id(this.testID.channelInfoScrollView)); closeChannelInfoButton = element(by.id(this.testID.closeChannelInfoButton)); channelIconGMMemberCount = element(by.id(this.testID.channelIconGMMemberCount)); favoritePreferenceAction = element(by.id(this.testID.favoritePreferenceAction)); @@ -44,6 +49,8 @@ class ChannelInfoScreen { addMembersAction = element(by.id(this.testID.addMembersAction)); convertPrivateAction = element(by.id(this.testID.convertPrivateAction)); editChannelAction = element(by.id(this.testID.editChannelAction)); + leaveAction = element(by.id(this.testID.leaveAction)); + archiveAction = element(by.id(this.testID.archiveAction)); toBeVisible = async () => { await wait(timeouts.TWO_SEC); @@ -64,6 +71,24 @@ class ChannelInfoScreen { await this.closeChannelInfoButton.tap(); await expect(this.channelInfoScreen).not.toBeVisible(); } + + archiveChannel = async (confirm = true) => { + await this.channelInfoScrollView.scrollTo('bottom'); + await this.archiveAction.tap(); + const { + archivePublicChannelTitle, + noButton, + yesButton, + } = Alert; + await expect(archivePublicChannelTitle).toBeVisible(); + if (confirm) { + yesButton.tap(); + } else { + noButton.tap(); + } + await wait(timeouts.ONE_SEC); + await expect(this.channelInfoScreen).not.toBeVisible(); + } } const channelInfoScreen = new ChannelInfoScreen(); diff --git a/detox/e2e/support/ui/screen/more_channels.js b/detox/e2e/support/ui/screen/more_channels.js index a755c2921..51c96d988 100644 --- a/detox/e2e/support/ui/screen/more_channels.js +++ b/detox/e2e/support/ui/screen/more_channels.js @@ -1,16 +1,59 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {MainSidebar} from '@support/ui/component'; +import { + BottomSheet, + MainSidebar, + SearchBar, +} from '@support/ui/component'; class MoreChannelsScreen { testID = { moreChannelsScreen: 'more_channels.screen', - publicChannelCreateButton: 'public_channel.create.button', + moreChannelsScreenPrefix: 'more_channels.', + closeMoreChannelsButton: 'close.more_channels.button', + createButton: 'more_channels.create.button', + channelDropdown: 'more_channels.channel.dropdown', + channelDropdownArchived: 'more_channels.channel.dropdown.archived', + channelDropdownPublic: 'more_channels.channel.dropdown.public', + channelsList: 'more_channels.custom_list', + channelItem: 'more_channels.custom_list.channel_item', + channelItemDisplayName: 'more_channels.custom_list.channel_item.display_name', } moreChannelsScreen = element(by.id(this.testID.moreChannelsScreen)); - publicChannelCreateButton = element(by.id(this.testID.publicChannelCreateButton)); + closeMoreChannelsButton = element(by.id(this.testID.closeMoreChannelsButton)); + createButton = element(by.id(this.testID.createButton)); + channelDropdown = element(by.id(this.testID.channelDropdown)); + channelDropdownArchived = element(by.id(this.testID.channelDropdownArchived)); + channelDropdownPublic = element(by.id(this.testID.channelDropdownPublic)); + channelsList = element(by.id(this.testID.channelsList)); + + // convenience props + searchBar = SearchBar.getSearchBar(this.testID.moreChannelsScreenPrefix); + searchInput = SearchBar.getSearchInput(this.testID.moreChannelsScreenPrefix); + cancelButton = SearchBar.getCancelButton(this.testID.moreChannelsScreenPrefix); + clearButton = SearchBar.getClearButton(this.testID.moreChannelsScreenPrefix); + + getChannel = (channelId, displayName) => { + const channelItemTestID = `${this.testID.channelItem}.${channelId}`; + const baseMatcher = by.id(channelItemTestID); + const channelItemMatcher = displayName ? baseMatcher.withDescendant(by.text(displayName)) : baseMatcher; + const channelItemDisplayNameMatcher = by.id(this.testID.channelItemDisplayName).withAncestor(channelItemMatcher); + + return { + channelItem: element(channelItemMatcher), + channelItemDisplayName: element(channelItemDisplayNameMatcher), + }; + } + + getChannelByDisplayName = (displayName) => { + return element(by.text(displayName).withAncestor(by.id(this.testID.channelsList))); + } + + getChannelDisplayNameAtIndex = (index) => { + return element(by.id(this.testID.channelItemDisplayName)).atIndex(index); + } toBeVisible = async () => { await expect(this.moreChannelsScreen).toBeVisible(); @@ -24,6 +67,29 @@ class MoreChannelsScreen { return this.toBeVisible(); } + + close = async () => { + await this.closeMoreChannelsButton.tap(); + await expect(this.moreChannelsScreen).not.toBeVisible(); + } + + showArchivedChannels = async () => { + await this.channelDropdown.tap(); + await BottomSheet.archivedChannelsOption.tap(); + await expect(this.channelDropdownArchived).toBeVisible(); + } + + showPublicChannels = async () => { + await this.channelDropdown.tap(); + await BottomSheet.publicChannelsOption.tap(); + await expect(this.channelDropdownPublic).toBeVisible(); + } + + hasChannelDisplayNameAtIndex = async (index, channelDisplayName) => { + await expect( + this.getChannelDisplayNameAtIndex(index), + ).toHaveText(channelDisplayName); + } } const moreChannelsScreen = new MoreChannelsScreen(); diff --git a/detox/e2e/test/channels/archived_channels.e2e.js b/detox/e2e/test/channels/archived_channels.e2e.js new file mode 100644 index 000000000..d7379da3e --- /dev/null +++ b/detox/e2e/test/channels/archived_channels.e2e.js @@ -0,0 +1,83 @@ +// 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, + ChannelInfoScreen, + MoreChannelsScreen, +} from '@support/ui/screen'; +import { + Channel, + Setup, + System, +} from '@support/server_api'; + +describe('Archived Channels', () => { + let archivedChannel; + let nonArchivedChannel; + + beforeAll(async () => { + // # Enable experimental view archived channels + await System.apiUpdateConfig({TeamSettings: {ExperimentalViewArchivedChannels: true}}); + + const {channel, team, user} = await Setup.apiInit(); + archivedChannel = channel; + + ({channel: nonArchivedChannel} = await Channel.apiCreateChannel({type: 'O', prefix: 'non-archived-channel', teamId: team.id})); + + // # Open channel screen + await ChannelScreen.open(user); + }); + + afterAll(async () => { + await ChannelScreen.logout(); + }); + + it('MM-T3618 should display archived channels list', async () => { + const {openMainSidebar} = ChannelScreen; + const { + getChannelByDisplayName, + hasChannelDisplayNameAtIndex, + searchInput, + showArchivedChannels, + showPublicChannels, + } = MoreChannelsScreen; + + // # Archive channel + await openMainSidebar(); + await MainSidebar.getChannelByDisplayName(archivedChannel.display_name).tap(); + await ChannelInfoScreen.open(); + await ChannelInfoScreen.archiveChannel(); + + // # Open more channels screen + await openMainSidebar(); + await MoreChannelsScreen.open(); + + // * Verify only non archived channels are displayed in public channels list + await showPublicChannels(); + await hasChannelDisplayNameAtIndex(0, nonArchivedChannel.display_name); + await expect(getChannelByDisplayName(archivedChannel.display_name)).not.toBeVisible(); + await searchInput.typeText(nonArchivedChannel.display_name); + await hasChannelDisplayNameAtIndex(0, nonArchivedChannel.display_name); + await expect(getChannelByDisplayName(archivedChannel.display_name)).not.toBeVisible(); + + // * Verify only archived channels are displayed in archived channels list + await searchInput.clearText(); + await showArchivedChannels(); + await hasChannelDisplayNameAtIndex(0, archivedChannel.display_name); + await expect(getChannelByDisplayName(nonArchivedChannel.display_name)).not.toBeVisible(); + await searchInput.typeText(archivedChannel.display_name); + await hasChannelDisplayNameAtIndex(0, archivedChannel.display_name); + await expect(getChannelByDisplayName(nonArchivedChannel.display_name)).not.toBeVisible(); + + // # Close more channels screen + await MoreChannelsScreen.close(); + }); +}); diff --git a/detox/e2e/test/channels/create_public_channel.e2e.js b/detox/e2e/test/channels/create_public_channel.e2e.js index 43b88a528..86922ae5a 100644 --- a/detox/e2e/test/channels/create_public_channel.e2e.js +++ b/detox/e2e/test/channels/create_public_channel.e2e.js @@ -39,7 +39,7 @@ describe('Channels', () => { await expect(element(by.text('No more channels to join'))).toBeVisible(); // # Tap to create new channel - await MoreChannelsScreen.publicChannelCreateButton.tap(); + await MoreChannelsScreen.createButton.tap(); // * Expect a new screen to create a new public channel const createChannelScreen = await CreateChannelScreen.toBeVisible();