diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/pages/ChannelListPage.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/pages/ChannelListPage.kt index 77b4f0e3e2c..c754fb45d39 100644 --- a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/pages/ChannelListPage.kt +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/pages/ChannelListPage.kt @@ -52,6 +52,7 @@ class ChannelListPage { companion object { val mute = By.desc("Mute") + val unmute = By.desc("Unmute") val more = By.desc("More") } } @@ -60,7 +61,9 @@ class ChannelListPage { companion object { val avatar = By.res("Stream_ChannelAvatar") - val name = By.res("Stream_ChannelName") + val name get() = By.res("Stream_ChannelName") + + fun name(text: String): BySelector = name.text(text) val messagePreview = By.res("Stream_MessagePreview") val deliveryStatusIsRead = Message.deliveryStatusIsRead val deliveryStatusIsPending = Message.deliveryStatusIsPending diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/pages/MessageListPage.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/pages/MessageListPage.kt index ddcd52cdac4..cdc8f6f11f5 100644 --- a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/pages/MessageListPage.kt +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/pages/MessageListPage.kt @@ -195,7 +195,10 @@ open class MessageListPage { val flag get() = By.res("Stream_ContextMenu_Flag Message") val pin get() = By.res("Stream_ContextMenu_Pin to this Chat") val unpin get() = By.res("Stream_ContextMenu_Unpin from this Chat") + val muteUser get() = By.res("Stream_ContextMenu_Mute User") + val unmuteUser get() = By.res("Stream_ContextMenu_Unmute User") val block get() = By.res("Stream_ContextMenu_Block user") + val unblock get() = By.res("Stream_ContextMenu_Unblock user") val delete get() = By.res("Stream_ContextMenu_Delete Message") val showMoreReactions = By.desc("Show more reactions") val ok = By.text("OK") diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobot.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobot.kt index 5314e9c0925..76ec0b2b274 100644 --- a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobot.kt +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobot.kt @@ -17,6 +17,7 @@ package io.getstream.chat.android.compose.robots import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector import io.getstream.chat.android.compose.pages.ChannelInfoPage import io.getstream.chat.android.compose.pages.ChannelListPage import io.getstream.chat.android.compose.pages.CreatePollPage @@ -37,6 +38,7 @@ import io.getstream.chat.android.e2e.test.uiautomator.device import io.getstream.chat.android.e2e.test.uiautomator.findObjects import io.getstream.chat.android.e2e.test.uiautomator.isDisplayed import io.getstream.chat.android.e2e.test.uiautomator.longPress +import io.getstream.chat.android.e2e.test.uiautomator.seconds import io.getstream.chat.android.e2e.test.uiautomator.sleep import io.getstream.chat.android.e2e.test.uiautomator.swipeDown import io.getstream.chat.android.e2e.test.uiautomator.swipeUp @@ -87,6 +89,11 @@ class UserRobot { return this } + fun openChannel(channelName: String): UserRobot { + ChannelListPage.ChannelList.Channel.name(channelName).waitToAppearAndClick() + return this + } + fun openContextMenu(messageCellIndex: Int = 0): UserRobot { val messages = MessageList.messages.waitToAppearBottomUp() val message = if (messages.size < messageCellIndex + 1) messages.last() else messages[messageCellIndex] @@ -202,6 +209,17 @@ class UserRobot { return this } + /** + * Opens the message menu of the message with [text] and waits for [option] to show. The open + * menu rebuilds its options when the own user state lands, so a moderation option that flips + * appears without reopening the menu. + */ + internal fun openContextMenuWithOption(text: String, option: BySelector): UserRobot { + openContextMenu(text) + option.waitDisplayed(timeOutMillis = 15.seconds) + return this + } + fun flagMessage(text: String): UserRobot { openContextMenu(text) ContextMenu.flag.waitToAppearAndClick() @@ -213,6 +231,30 @@ class UserRobot { return this } + fun muteMessageAuthor(text: String): UserRobot { + openContextMenu(text) + ContextMenu.muteUser.waitToAppearAndClick() + return this + } + + fun unmuteMessageAuthor(text: String): UserRobot { + openContextMenuWithOption(text, ContextMenu.unmuteUser) + ContextMenu.unmuteUser.waitToAppearAndClick() + return this + } + + fun blockMessageAuthor(text: String): UserRobot { + openContextMenu(text) + ContextMenu.block.waitToAppearAndClick() + return this + } + + fun unblockMessageAuthor(text: String): UserRobot { + openContextMenuWithOption(text, ContextMenu.unblock) + ContextMenu.unblock.waitToAppearAndClick() + return this + } + fun pinMessage(messageCellIndex: Int = 0): UserRobot { openContextMenu(messageCellIndex) ContextMenu.pin.waitToAppearAndClick() @@ -402,6 +444,16 @@ class UserRobot { return this } + fun tapOnMuteSwipeAction(): UserRobot { + ChannelListPage.ChannelList.SwipeActions.mute.waitToAppearAndClick() + return this + } + + fun tapOnUnmuteSwipeAction(): UserRobot { + ChannelListPage.ChannelList.SwipeActions.unmute.waitToAppearAndClick() + return this + } + fun tapOnLeaveGroup(): UserRobot { ChannelListPage.ChannelMenu.leaveGroup.waitToAppearAndClick() return this @@ -441,7 +493,9 @@ class UserRobot { return this } - fun searchForMessage(text: String): UserRobot { + /** Types into the channel list header search input; the app's search mode decides whether + * messages or channels are searched. */ + fun search(text: String): UserRobot { ChannelListPage.Header.searchField.waitToAppear().typeText(text) return this } diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotChannelListAsserts.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotChannelListAsserts.kt index 5e8c911217d..aad8b3e0545 100644 --- a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotChannelListAsserts.kt +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotChannelListAsserts.kt @@ -25,6 +25,7 @@ import io.getstream.chat.android.e2e.test.robots.ParticipantRobot import io.getstream.chat.android.e2e.test.uiautomator.appContext import io.getstream.chat.android.e2e.test.uiautomator.isDisplayed import io.getstream.chat.android.e2e.test.uiautomator.waitDisplayed +import io.getstream.chat.android.e2e.test.uiautomator.waitForCount import io.getstream.chat.android.e2e.test.uiautomator.waitForText import io.getstream.chat.android.e2e.test.uiautomator.waitToDisappear import org.junit.Assert.assertEquals @@ -80,11 +81,34 @@ fun UserRobot.assertFailedMessageDeliveryStatusInPreview(): UserRobot { return this } +fun UserRobot.assertChannelIsMuted(isMuted: Boolean): UserRobot { + if (isMuted) { + assertTrue(Channel.mutedIcon.waitDisplayed()) + } else { + assertFalse(Channel.mutedIcon.waitToDisappear().isDisplayed()) + } + return this +} + fun UserRobot.assertChannelListIsEmpty(): UserRobot { assertFalse(ChannelList.channels.waitToDisappear().isDisplayed()) return this } +fun UserRobot.assertChannelCount(count: Int): UserRobot { + assertEquals(count, ChannelList.channels.waitForCount(count).size) + return this +} + +fun UserRobot.assertChannelWithName(name: String, isDisplayed: Boolean = true): UserRobot { + if (isDisplayed) { + assertTrue(Channel.name(name).waitDisplayed()) + } else { + assertFalse(Channel.name(name).waitToDisappear().isDisplayed()) + } + return this +} + fun UserRobot.assertMessagePreviewTimestamp(isDisplayed: Boolean = true): UserRobot { if (isDisplayed) { assertTrue(Channel.timestamp.waitDisplayed()) diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotMessageListAsserts.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotMessageListAsserts.kt index 46dd859379c..d6252a095f7 100644 --- a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotMessageListAsserts.kt +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotMessageListAsserts.kt @@ -24,6 +24,7 @@ import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.pages.MessageListPage import io.getstream.chat.android.compose.pages.MessageListPage.Composer import io.getstream.chat.android.compose.pages.MessageListPage.MessageList.Message +import io.getstream.chat.android.compose.pages.MessageListPage.MessageList.Message.ContextMenu import io.getstream.chat.android.compose.pages.ThreadPage import io.getstream.chat.android.e2e.test.mockserver.MessageDeliveryStatus import io.getstream.chat.android.e2e.test.mockserver.ReactionType @@ -552,3 +553,33 @@ fun UserRobot.assertFlagMessageDialog(isDisplayed: Boolean): UserRobot { assertVisibility(MessageListPage.FlagMessageDialog.body, isDisplayed) return this } + +/** + * Opens the message menu of the message with [messageText] and asserts the mute option it offers + * for the author. The sample has no muted users list, so the option label is the only place the + * mute state is visible. + */ +fun UserRobot.assertMuteMessageAuthorOption(messageText: String, isAuthorMuted: Boolean): UserRobot { + val expectedOption = if (isAuthorMuted) ContextMenu.unmuteUser else ContextMenu.muteUser + val oppositeOption = if (isAuthorMuted) ContextMenu.muteUser else ContextMenu.unmuteUser + openContextMenuWithOption(messageText, expectedOption) + assertTrue(expectedOption.isDisplayed()) + assertFalse(oppositeOption.isDisplayed()) + pressBack() + return this +} + +/** + * Opens the message menu of the message with [messageText] and asserts the block option it offers + * for the author. The sample has no blocked users list, so the option label is the only place the + * block state is visible. + */ +fun UserRobot.assertBlockMessageAuthorOption(messageText: String, isAuthorBlocked: Boolean): UserRobot { + val expectedOption = if (isAuthorBlocked) ContextMenu.unblock else ContextMenu.block + val oppositeOption = if (isAuthorBlocked) ContextMenu.block else ContextMenu.unblock + openContextMenuWithOption(messageText, expectedOption) + assertTrue(expectedOption.isDisplayed()) + assertFalse(oppositeOption.isDisplayed()) + pressBack() + return this +} diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ChannelActionsTests.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ChannelActionsTests.kt index 04b8092100f..29c6426f4c0 100644 --- a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ChannelActionsTests.kt +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ChannelActionsTests.kt @@ -17,6 +17,7 @@ package io.getstream.chat.android.compose.tests import io.getstream.chat.android.compose.robots.assertChannelActionsSheetForGroupChannel +import io.getstream.chat.android.compose.robots.assertChannelIsMuted import io.getstream.chat.android.compose.robots.assertChannelListIsEmpty import io.getstream.chat.android.compose.robots.assertGroupChannelInfoScreen import io.getstream.chat.android.compose.sample.ui.InitTestActivity @@ -110,4 +111,42 @@ class ChannelActionsTests : StreamTestCase() { userRobot.assertChannelListIsEmpty() } } + + @AllureId("11560") + @Test + fun test_userMutesChannelFromTheSwipeAction() { + step("GIVEN user logs in") { + userRobot.login().waitForChannelListToLoad() + } + step("WHEN user swipes the channel and taps on the mute action") { + userRobot + .swipeChannel() + .tapOnMuteSwipeAction() + } + step("THEN the channel shows the muted icon") { + userRobot.assertChannelIsMuted(isMuted = true) + } + } + + @AllureId("11567") + @Test + fun test_userUnmutesChannelFromTheSwipeAction() { + step("GIVEN user logs in") { + userRobot.login().waitForChannelListToLoad() + } + step("AND user mutes the channel from the swipe action") { + userRobot + .swipeChannel() + .tapOnMuteSwipeAction() + .assertChannelIsMuted(isMuted = true) + } + step("WHEN user swipes the channel and taps on the unmute action") { + userRobot + .swipeChannel() + .tapOnUnmuteSwipeAction() + } + step("THEN the channel shows no muted icon") { + userRobot.assertChannelIsMuted(isMuted = false) + } + } } diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ChannelSearchTests.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ChannelSearchTests.kt new file mode 100644 index 00000000000..4e2c0335380 --- /dev/null +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ChannelSearchTests.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.tests + +import io.getstream.chat.android.compose.robots.assertChannelCount +import io.getstream.chat.android.compose.robots.assertChannelWithName +import io.getstream.chat.android.compose.sample.ui.InitTestActivity +import io.qameta.allure.kotlin.Allure.step +import io.qameta.allure.kotlin.AllureId +import org.junit.Test + +/** + * Covers the channel search wired into the channel list header. It needs the app started with the + * channel search flag, which configures the channel list with `SearchMode.Channels`. Message + * search, the default mode, is covered by [SearchTests]. + */ +class ChannelSearchTests : StreamTestCase() { + + override fun initTestActivity() = InitTestActivity.UserLogin + override var useChannelSearch = true + + private val channelName = "Blue Team" + private val searchQuery = "Blue" + + @AllureId("5934") + @Test + fun test_userSearchesForChannel() { + step("GIVEN channels exist, one with a searchable name") { + backendRobot.generateChannels(channelsCount = 3, channelNames = listOf(channelName)) + } + step("AND user logs in") { + userRobot + .login() + .waitForChannelListToLoad() + .assertChannelCount(3) + } + step("WHEN user searches for the channel by its name") { + userRobot.search(searchQuery) + } + step("THEN only the matching channel is shown") { + userRobot + .assertChannelCount(1) + .assertChannelWithName(channelName) + } + } +} diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ModerationTests.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ModerationTests.kt index 9bc5d76b905..d2cdd798dba 100644 --- a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ModerationTests.kt +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ModerationTests.kt @@ -16,22 +16,32 @@ package io.getstream.chat.android.compose.tests +import io.getstream.chat.android.compose.robots.assertBlockMessageAuthorOption +import io.getstream.chat.android.compose.robots.assertChannelCount +import io.getstream.chat.android.compose.robots.assertChannelWithName import io.getstream.chat.android.compose.robots.assertFlagMessageDialog import io.getstream.chat.android.compose.robots.assertMessage +import io.getstream.chat.android.compose.robots.assertMuteMessageAuthorOption import io.getstream.chat.android.compose.sample.ui.InitTestActivity +import io.getstream.chat.android.e2e.test.robots.ParticipantRobot import io.qameta.allure.kotlin.Allure.step import io.qameta.allure.kotlin.AllureId import org.junit.Test /** - * Covers flagging another user's message from the message menu. The option is absent on the user's - * own messages. + * Covers the moderation options the message menu offers for another user's message: flagging the + * message, and muting or blocking its author. The options are absent on the user's own messages. */ class ModerationTests : StreamTestCase() { override fun initTestActivity() = InitTestActivity.UserLogin private val sampleText = "Test" + // Seeded messages alternate authors starting with the user, so with two seeded + // messages the one with text "2" belongs to the participant. + private val participantMessageText = "2" + private val groupChannelName = "1" + @AllureId("11572") @Test fun test_userFlagsMessage() { @@ -56,4 +66,131 @@ class ModerationTests : StreamTestCase() { .assertMessage(sampleText) } } + + @AllureId("11563") + @Test + fun test_userMutesMessageAuthor() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND participant sends the message") { + participantRobot.sendMessage(sampleText) + } + step("WHEN user mutes the message author") { + userRobot.muteMessageAuthor(sampleText) + } + step("THEN the message menu offers to unmute the author") { + userRobot.assertMuteMessageAuthorOption(sampleText, isAuthorMuted = true) + } + } + + @AllureId("11566") + @Test + fun test_userUnmutesMessageAuthor() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND participant sends the message") { + participantRobot.sendMessage(sampleText) + } + step("AND user mutes the message author") { + userRobot.muteMessageAuthor(sampleText) + } + step("WHEN user unmutes the message author") { + userRobot.unmuteMessageAuthor(sampleText) + } + step("THEN the message menu offers to mute the author again") { + userRobot.assertMuteMessageAuthorOption(sampleText, isAuthorMuted = false) + } + } + + @AllureId("6071") + @Test + fun test_userBlocksMessageAuthor() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND participant sends the message") { + participantRobot.sendMessage(sampleText) + } + step("WHEN user blocks the message author") { + userRobot.blockMessageAuthor(sampleText) + } + step("THEN the message menu offers to unblock the author") { + userRobot.assertBlockMessageAuthorOption(sampleText, isAuthorBlocked = true) + } + } + + @AllureId("11575") + @Test + fun test_userUnblocksMessageAuthor() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND participant sends the message") { + participantRobot.sendMessage(sampleText) + } + step("AND user blocks the message author") { + userRobot.blockMessageAuthor(sampleText) + } + step("WHEN user unblocks the message author") { + userRobot.unblockMessageAuthor(sampleText) + } + step("THEN the message menu offers to block the author again") { + userRobot.assertBlockMessageAuthorOption(sampleText, isAuthorBlocked = false) + } + } + + @AllureId("6070") + @Test + fun test_userBlocksUserInDirectMessageChannel() { + step("GIVEN a direct message channel with the participant exists") { + backendRobot.generateChannels(channelsCount = 1, messagesCount = 2, withDirectMessageChannel = true) + } + step("AND user opens the direct message channel") { + userRobot.login().openChannel(ParticipantRobot.name) + } + step("WHEN user blocks the participant") { + userRobot.blockMessageAuthor(participantMessageText) + } + step("THEN the message menu offers to unblock the participant") { + userRobot.assertBlockMessageAuthorOption(participantMessageText, isAuthorBlocked = true) + } + } + + @AllureId("6072") + @Test + fun test_directMessageChannelDisappears_whenUserBlocksParticipant() { + step("GIVEN a direct message channel with the participant exists") { + backendRobot.generateChannels(channelsCount = 1, messagesCount = 2, withDirectMessageChannel = true) + } + step("AND user sees it in the channel list") { + userRobot + .login() + .assertChannelCount(2) + .assertChannelWithName(ParticipantRobot.name) + } + step("WHEN user blocks the participant in the direct message channel") { + userRobot + .openChannel(ParticipantRobot.name) + .blockMessageAuthor(participantMessageText) + .moveToChannelListFromMessageList() + } + step("THEN the direct message channel disappears from the channel list") { + userRobot + .assertChannelCount(1) + .assertChannelWithName(ParticipantRobot.name, isDisplayed = false) + } + step("WHEN user unblocks the participant from the group channel") { + userRobot + .openChannel(groupChannelName) + .unblockMessageAuthor(participantMessageText) + .moveToChannelListFromMessageList() + } + step("THEN the direct message channel is shown in the channel list again") { + userRobot + .assertChannelCount(2) + .assertChannelWithName(ParticipantRobot.name) + } + } } diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/SearchTests.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/SearchTests.kt index 15b7dfb7a3f..a5df737da60 100644 --- a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/SearchTests.kt +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/SearchTests.kt @@ -24,8 +24,8 @@ import io.qameta.allure.kotlin.AllureId import org.junit.Test /** - * Covers the message search wired into the channel list header. The sample configures the channel - * list with `SearchMode.Messages` only, so channel search is not reachable. + * Covers the message search wired into the channel list header, the sample's default search mode. + * Channel search, behind the channel search flag, is covered by [ChannelSearchTests]. */ class SearchTests : StreamTestCase() { @@ -44,7 +44,7 @@ class SearchTests : StreamTestCase() { step("WHEN user searches for the message on the channel list") { userRobot .moveToChannelListFromMessageList() - .searchForMessage(sampleText) + .search(sampleText) } step("THEN the message is shown in the search results") { userRobot.assertMessageInChannelPreview(sampleText, fromCurrentUser = false) @@ -63,7 +63,7 @@ class SearchTests : StreamTestCase() { step("AND user searches for the message on the channel list") { userRobot .moveToChannelListFromMessageList() - .searchForMessage(sampleText) + .search(sampleText) .assertMessageInChannelPreview(sampleText, fromCurrentUser = false) } step("WHEN user taps on the search result") { diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/StreamTestCase.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/StreamTestCase.kt index 3c75881af3d..13b77ea10ec 100644 --- a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/StreamTestCase.kt +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/StreamTestCase.kt @@ -42,6 +42,9 @@ abstract class StreamTestCase { lateinit var mockServer: MockServer open var useMockServer = true + + /** Makes the channel list header search for channels instead of messages. */ + open var useChannelSearch = false val userRobot = UserRobot() lateinit var backendRobot: BackendRobot lateinit var participantRobot: ParticipantRobot @@ -93,6 +96,7 @@ abstract class StreamTestCase { testContext.packageManager.getLaunchIntentForPackage(packageName)?.let { it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) it.putExtra("InitTestActivity", initTestActivity()) + it.putExtra("CHANNEL_SEARCH", useChannelSearch) if (useMockServer) { it.putExtra("BASE_URL", mockServer.url) } diff --git a/stream-chat-android-compose-sample/src/e2e/java/io/getstream/chat/android/compose/sample/ui/StartupActivity.kt b/stream-chat-android-compose-sample/src/e2e/java/io/getstream/chat/android/compose/sample/ui/StartupActivity.kt index bf918943564..5703db1ca49 100644 --- a/stream-chat-android-compose-sample/src/e2e/java/io/getstream/chat/android/compose/sample/ui/StartupActivity.kt +++ b/stream-chat-android-compose-sample/src/e2e/java/io/getstream/chat/android/compose/sample/ui/StartupActivity.kt @@ -49,6 +49,13 @@ class StartupActivity : AppCompatActivity() { customSettings().isComposerLinkPreviewEnabled = true } + // Assigned on every harness launch, with or without a mock server, so a flag left by + // a previous test cannot leak into the next one. A notification launch carries no + // extra and keeps the persisted value. + if (intent.hasExtra("CHANNEL_SEARCH")) { + customSettings().isChannelSearchEnabled = intent.getBooleanExtra("CHANNEL_SEARCH", false) + } + val initTestActivity = intent.getSerializableExtra("InitTestActivity") as? InitTestActivity if (initTestActivity != null) { startActivity(initTestActivity.createIntent(this@StartupActivity)) diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/data/CustomSettings.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/data/CustomSettings.kt index c27561ebfc6..d1b4752cda6 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/data/CustomSettings.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/data/CustomSettings.kt @@ -39,6 +39,7 @@ class CustomSettings(private val context: Context) { var isSystemAttachmentPickerEnabled: Boolean by booleanPref(SystemAttachmentPicker) var isLocationSharingEnabled: Boolean by booleanPref(LocationSharing) var isLocalUnreadCountEnabled: Boolean by booleanPref(LocalUnreadCount) + var isChannelSearchEnabled: Boolean by booleanPref(ChannelSearch) private fun booleanPref(key: String, default: Boolean = false) = object : ReadWriteProperty { @@ -57,5 +58,6 @@ private const val ComposerFloatingStyle = "composer_floating_style" private const val SystemAttachmentPicker = "system_attachment_picker" private const val LocationSharing = "location_sharing" private const val LocalUnreadCount = "local_unread_count" +private const val ChannelSearch = "channel_search" fun Context.customSettings(): CustomSettings = CustomSettings(this) diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt index 0db23e4b61b..f79d3c2637a 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt @@ -215,7 +215,7 @@ class ChannelsActivity : ComponentActivity() { ChannelsScreen( viewModelFactory = channelsViewModelFactory, isShowingHeader = true, - searchMode = SearchMode.Messages, + searchMode = if (settings.isChannelSearchEnabled) SearchMode.Channels else SearchMode.Messages, onChannelClick = ::openMessages, onSearchMessageItemClick = ::openMessages, onBackPressed = ::finish, diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/chats/ChatsActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/chats/ChatsActivity.kt index c79296f6c75..02dc2499e89 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/chats/ChatsActivity.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/chats/ChatsActivity.kt @@ -183,7 +183,7 @@ class ChatsActivity : ComponentActivity() { } }, title = stringResource(id = R.string.app_name), - searchMode = SearchMode.Messages, + searchMode = if (settings.isChannelSearchEnabled) SearchMode.Channels else SearchMode.Messages, listContentMode = listContentMode, onBackPress = ::finish, onListTopBarAvatarClick = { diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/login/CustomLoginActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/login/CustomLoginActivity.kt index 8637fe73634..d50dd478cfc 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/login/CustomLoginActivity.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/login/CustomLoginActivity.kt @@ -144,6 +144,9 @@ class CustomLoginActivity : AppCompatActivity() { var isLocalUnreadCountEnabled by remember { mutableStateOf(settings.isLocalUnreadCountEnabled) } + var isChannelSearchEnabled by remember { + mutableStateOf(settings.isChannelSearchEnabled) + } val isLoginButtonEnabled = apiKeyText.isNotEmpty() && userIdText.isNotEmpty() && @@ -219,6 +222,15 @@ class CustomLoginActivity : AppCompatActivity() { settings.isLocalUnreadCountEnabled = it }, ), + FeatureFlag( + label = stringResource(R.string.custom_login_flag_channel_search_label), + description = stringResource(R.string.custom_login_flag_channel_search_description), + value = isChannelSearchEnabled, + onValueChange = { + isChannelSearchEnabled = it + settings.isChannelSearchEnabled = it + }, + ), ) CustomLoginInputField( diff --git a/stream-chat-android-compose-sample/src/main/res/values/strings.xml b/stream-chat-android-compose-sample/src/main/res/values/strings.xml index a31921dcedd..c077d6f75c0 100644 --- a/stream-chat-android-compose-sample/src/main/res/values/strings.xml +++ b/stream-chat-android-compose-sample/src/main/res/values/strings.xml @@ -47,6 +47,8 @@ Show the Pin/Unpin Chat action in the channel options menu Local unread count Track the unread count locally for channels with read events disabled. Also shows livestream channels in the channel list + Channel search + Search for channels instead of messages from the channel list header Pinned Messages diff --git a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/robots/BackendRobot.kt b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/robots/BackendRobot.kt index df5cad2d1da..0d4d5f19231 100644 --- a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/robots/BackendRobot.kt +++ b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/robots/BackendRobot.kt @@ -21,6 +21,7 @@ import junit.framework.TestCase.fail import org.json.JSONObject import java.net.HttpURLConnection import java.net.URL +import java.net.URLEncoder public class BackendRobot( private val mockServer: MockServer, @@ -32,14 +33,24 @@ public class BackendRobot( repliesCount: Int = 0, messagesText: String? = null, repliesText: String? = null, + channelNames: List = emptyList(), + withDirectMessageChannel: Boolean = false, ): BackendRobot { waitForMockServerToStart() val messagesTextQueryParam = if (messagesText != null) "messages_text=$messagesText&" else "" val repliesTextQueryParam = if (repliesText != null) "replies_text=$repliesText&" else "" + val channelNamesQueryParam = if (channelNames.isNotEmpty()) { + "channel_names=${URLEncoder.encode(channelNames.joinToString(","), "UTF-8")}&" + } else { + "" + } + val dmQueryParam = if (withDirectMessageChannel) "dm=true&" else "" mockServer.postRequest( "mock?" + messagesTextQueryParam + repliesTextQueryParam + + channelNamesQueryParam + + dmQueryParam + "channels=$channelsCount&" + "messages=$messagesCount&" + "replies=$repliesCount",