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 4792e814994..68bbd5355ab 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 @@ -27,7 +27,7 @@ class ChannelListPage { companion object { val userAvatar = By.res("Stream_UserAvatar") val searchField = By.res("Stream_SearchInput") - val createChannelButton = "Stream_CreateChannelIcon" + val createChannelButton = By.res("Stream_CreateChannelIcon") } } @@ -38,11 +38,18 @@ class ChannelListPage { val channelList = By.res("Stream_ChannelList") } + class SwipeActions { + + companion object { + val mute = By.desc("Mute") + val more = By.desc("More") + } + } + class Channel { companion object { val avatar = By.res("Stream_ChannelAvatar") - val initialsAvatar = By.res("Stream_InitialsAvatar") val name = By.res("Stream_ChannelName") val messagePreview = By.res("Stream_MessagePreview") val deliveryStatusIsRead = Message.deliveryStatusIsRead @@ -59,4 +66,13 @@ class ChannelListPage { } } } + + class ChannelMenu { + + companion object { + val viewInfo = By.res("Stream_ContextMenu_View info") + val leaveGroup = By.res("Stream_ContextMenu_Leave group") + val deleteGroup = By.res("Stream_ContextMenu_Delete Group") + } + } } 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 ab9812cc765..23493e6ed2e 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 @@ -32,18 +32,10 @@ open class MessageListPage { } } - class MembersList { - - companion object { - val members get() = By.res("Stream_MembersList") - } - } - class AttachmentPicker { companion object { val view get() = By.res("Stream_AttachmentsPicker") - val sendButton get() = By.res("Stream_AttachmentPickerSendButton") val filesTab get() = By.res("Stream_AttachmentPickerFilesTab") val mediaCaptureTab get() = By.res("Stream_AttachmentPickerMediaCaptureTab") val pollsTab get() = By.res("Stream_AttachmentPickerPollsTab") @@ -64,7 +56,7 @@ open class MessageListPage { val sendButton get() = By.res("Stream_ComposerSendButton") val cooldownIndicator get() = By.res("Stream_ComposerCooldownIndicator") val saveButton get() = By.res("Stream_ComposerSaveButton") - val recordAudioButton get() = By.res("Stream_ComposerAudioRecordingButton") + val recordAudioButton get() = By.res("Stream_ComposerRecordAudioButton") val commandSuggestionList get() = By.res("Stream_CommandSuggestionList") val commandSuggestionListTitle get() = By.res("Stream_CommandSuggestionListTitle") val userSuggestion get() = By.res("Stream_SuggestionItem") @@ -89,14 +81,15 @@ open class MessageListPage { class MessageList { companion object { - val messageList get() = By.res("Stream_MessageList") + val messageList get() = By.res("Stream_Messages") val messages get() = By.res("Stream_MessageCell") val dateSeparator get() = By.res("Stream_MessageDateSeparator") val unreadMessagesBadge get() = By.res("Stream_UnreadMessagesBadge") val typingIndicator get() = By.res("Stream_MessageListTypingIndicator") val scrollToBottomButton get() = By.res("Stream_ScrollToBottomButton") val scrollToBottomButtonUnreadCount get() = By.res("Stream_ScrollToBottomButtonUnreadCount") - val systemMessage get() = By.res("Stream_SystemMessage") + val scrollToFirstUnreadButton get() = By.res("Stream_ScrollToFirstUnreadButton") + val scrollToFirstUnreadDismissIcon get() = By.res("Stream_ScrollToFirstUnreadButton_Dismiss") } class Message { @@ -123,7 +116,6 @@ open class MessageListPage { val fileImage get() = By.res("Stream_FileAttachmentImage") val fileName get() = By.res("Stream_FileAttachmentName") val fileSize get() = By.res("Stream_FileAttachmentSize") - val fileDownloadButton get() = By.res("Stream_FileAttachmentDownloadButton") val columnWithMultipleFileAttachments get() = By.res("Stream_MultipleFileAttachmentsColumn") val giphy get() = By.res("Stream_GiphyContent") val linkPreviewImage get() = By.res("Stream_LinkAttachmentPreview") @@ -135,6 +127,7 @@ open class MessageListPage { companion object { val reactions get() = By.res("Stream_MessageReaction") + val reactionAuthor get() = By.res("Stream_ReactionAuthor") fun reaction(type: ReactionType): BySelector = By.res("Stream_MessageReaction_${type.reaction}") @@ -164,6 +157,7 @@ open class MessageListPage { val unpin get() = By.res("Stream_ContextMenu_Unpin from this Chat") val block get() = By.res("Stream_ContextMenu_Block 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 c0209679c03..c758e61a39f 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 @@ -189,6 +189,24 @@ class UserRobot { return this } + fun copyMessage(messageCellIndex: Int = 0): UserRobot { + openContextMenu(messageCellIndex) + ContextMenu.copy.waitToAppear().click() + return this + } + + fun pinMessage(messageCellIndex: Int = 0): UserRobot { + openContextMenu(messageCellIndex) + ContextMenu.pin.waitToAppear().click() + return this + } + + fun unpinMessage(messageCellIndex: Int = 0): UserRobot { + openContextMenu(messageCellIndex) + ContextMenu.unpin.waitToAppear().click() + return this + } + fun clearComposer(): UserRobot { Composer.inputField.waitToAppear().clear() return this @@ -200,6 +218,22 @@ class UserRobot { return this } + fun tapOnMessageReaction(): UserRobot { + Message.Reactions.reactions.waitToAppear().click() + return this + } + + /** + * Adds or removes a reaction through the extended reactions picker sheet. Selecting a type + * the user has already reacted with removes that reaction. + */ + fun toggleReactionUsingExtendedPicker(type: ReactionType, messageCellIndex: Int = 0): UserRobot { + openContextMenu(messageCellIndex) + ContextMenu.showMoreReactions.waitToAppear().click() + ContextMenu.ReactionsView.reaction(type).waitToAppear().click() + return this + } + fun deleteReaction(type: ReactionType, usingContextMenu: Boolean = true, messageCellIndex: Int = 0): UserRobot { if (usingContextMenu) { addReaction(type, messageCellIndex) @@ -254,6 +288,16 @@ class UserRobot { return this } + fun tapOnScrollToFirstUnreadButton(): UserRobot { + MessageList.scrollToFirstUnreadButton.waitToAppear().click() + return this + } + + fun dismissUnreadIndicator(): UserRobot { + MessageList.scrollToFirstUnreadDismissIcon.waitToAppear().click() + return this + } + fun sendMessageInThread( text: String, alsoSendInChannel: Boolean = false, @@ -310,6 +354,37 @@ class UserRobot { return this } + fun openChannelMenu(channelCellIndex: Int = 0): UserRobot { + ChannelListPage.ChannelList.channels.wait().findObjects()[channelCellIndex].longPress() + return this + } + + /** Swipes a channel item right to left to reveal the swipe actions behind it. */ + fun swipeChannel(channelCellIndex: Int = 0): UserRobot { + val percent = 0.5f + val channel = ChannelListPage.ChannelList.channels.wait().findObjects()[channelCellIndex] + val rect = channel.visibleBounds + val startX = (rect.right - (rect.width() * 0.1)).toInt() + device.swipe( + startX, // startX + rect.centerY(), // startY + (startX - (rect.width() * percent)).toInt(), // endX + rect.centerY(), // endY + 20, // steps + ) + return this + } + + fun tapOnMoreSwipeAction(): UserRobot { + ChannelListPage.ChannelList.SwipeActions.more.waitToAppear().click() + return this + } + + fun tapOnViewChannelInfo(): UserRobot { + ChannelListPage.ChannelMenu.viewInfo.waitToAppear().click() + return this + } + fun swipeMessage(messageCellIndex: Int = 0): UserRobot { val percent = 0.5f val message = MessageList.messages.waitToAppearBottomUp(withIndex = messageCellIndex) 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 ec45f91483e..0b9d9793726 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 @@ -16,8 +16,11 @@ package io.getstream.chat.android.compose.robots +import androidx.test.uiautomator.By import io.getstream.chat.android.compose.pages.ChannelListPage.ChannelList.Channel +import io.getstream.chat.android.compose.pages.ChannelListPage.ChannelMenu 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.waitForText @@ -25,12 +28,28 @@ import io.getstream.chat.android.e2e.test.uiautomator.waitToDisappear import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue +import io.getstream.chat.android.ui.common.R as UiCommonR fun UserRobot.assertChannelAvatar(): UserRobot { assertTrue(Channel.avatar.isDisplayed()) return this } +fun UserRobot.assertChannelActionsSheetForGroupChannel(): UserRobot { + assertTrue(ChannelMenu.viewInfo.waitDisplayed()) + assertTrue(ChannelMenu.leaveGroup.isDisplayed()) + assertTrue(ChannelMenu.deleteGroup.isDisplayed()) + return this +} + +fun UserRobot.assertGroupChannelInfoScreen(): UserRobot { + assertTrue(By.text(appContext.getString(UiCommonR.string.stream_ui_channel_info_group_title)).waitDisplayed()) + assertTrue( + By.text(appContext.getString(UiCommonR.string.stream_ui_channel_info_option_pinned_messages)).isDisplayed(), + ) + return this +} + fun UserRobot.assertMessageInChannelPreview(text: String, fromCurrentUser: Boolean? = null): UserRobot { val expectedPreview = when (fromCurrentUser) { true -> "You: $text" 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 cf11fc63dec..b34d3f44a94 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 @@ -17,6 +17,7 @@ package io.getstream.chat.android.compose.robots import android.annotation.SuppressLint +import android.content.ClipboardManager import androidx.test.uiautomator.By import androidx.test.uiautomator.BySelector import io.getstream.chat.android.compose.R @@ -314,6 +315,54 @@ fun UserRobot.assertAlsoInTheChannelLabelInThread(): UserRobot { return this } +fun UserRobot.assertUnreadSeparator(unreadCount: Int, isDisplayed: Boolean = true): UserRobot { + if (isDisplayed) { + val expectedText = appContext.resources.getQuantityString( + R.plurals.stream_compose_message_list_unread_separator, + unreadCount, + unreadCount, + ) + assertEquals(expectedText, MessageListPage.MessageList.unreadMessagesBadge.waitForText(expectedText)) + } else { + assertVisibility(MessageListPage.MessageList.unreadMessagesBadge, isDisplayed = false) + } + return this +} + +fun UserRobot.assertScrollToFirstUnreadButton(unreadCount: Int? = null, isDisplayed: Boolean = true): UserRobot { + assertVisibility(MessageListPage.MessageList.scrollToFirstUnreadButton, isDisplayed) + if (isDisplayed && unreadCount != null) { + val expectedText = appContext.resources.getQuantityString( + R.plurals.stream_compose_scroll_to_first_unread_count, + unreadCount, + unreadCount, + ) + assertTrue(By.text(expectedText).waitDisplayed()) + } + return this +} + +fun UserRobot.assertMessageCopied(text: String): UserRobot { + val clipboard = appContext.getSystemService(ClipboardManager::class.java) + assertEquals(text, clipboard.primaryClip?.getItemAt(0)?.text?.toString()) + return this +} + +/** + * Asserts the "Pinned by X" label above a message. [pinnedBy] is the name shown in the label; + * `null` expects the current user's label ("Pinned by You"). + */ +fun UserRobot.assertMessagePinnedLabel(pinnedBy: String? = null, isDisplayed: Boolean = true): UserRobot { + val pinnedByName = pinnedBy ?: appContext.getString(R.string.stream_compose_message_list_you) + val expectedLabel = appContext.getString(R.string.stream_compose_pinned_to_channel_by, pinnedByName) + if (isDisplayed) { + assertEquals(expectedLabel, Message.messageHeaderLabel.waitForText(expectedLabel)) + } else { + assertVisibility(Message.messageHeaderLabel.text(expectedLabel), isDisplayed = false) + } + return this +} + fun UserRobot.assertGiphyImage(isDisplayed: Boolean = true): UserRobot { if (isDisplayed) { assertTrue(Message.giphy.waitDisplayed()) @@ -354,6 +403,11 @@ fun UserRobot.assertReaction(type: ReactionType, isDisplayed: Boolean): UserRobo return this } +fun UserRobot.assertReactionAuthor(name: String): UserRobot { + assertTrue(Message.Reactions.reactionAuthor.hasDescendant(By.text(name)).waitDisplayed()) + return this +} + @SuppressLint("ResourceType") fun UserRobot.assertThreadReplyLabel(replies: Int, inThread: Boolean = false): UserRobot { if (inThread) { 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 new file mode 100644 index 00000000000..153f1ce6e81 --- /dev/null +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ChannelActionsTests.kt @@ -0,0 +1,74 @@ +/* + * 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.assertChannelActionsSheetForGroupChannel +import io.getstream.chat.android.compose.robots.assertGroupChannelInfoScreen +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 + +class ChannelActionsTests : StreamTestCase() { + + override fun initTestActivity() = InitTestActivity.UserLogin + + @AllureId("11456") + @Test + fun test_channelActionsSheetIsShown_whenUserLongPressesTheChannel() { + step("GIVEN user logs in") { + userRobot.login().waitForChannelListToLoad() + } + step("WHEN user long presses the channel") { + userRobot.openChannelMenu() + } + step("THEN the channel actions sheet is shown") { + userRobot.assertChannelActionsSheetForGroupChannel() + } + } + + @AllureId("11457") + @Test + fun test_channelActionsSheetIsShown_whenUserSwipesTheChannel() { + step("GIVEN user logs in") { + userRobot.login().waitForChannelListToLoad() + } + step("WHEN user swipes the channel and taps on the more action") { + userRobot.swipeChannel().tapOnMoreSwipeAction() + } + step("THEN the channel actions sheet is shown") { + userRobot.assertChannelActionsSheetForGroupChannel() + } + } + + @AllureId("11458") + @Test + fun test_userOpensChannelInfoFromTheChannelActionsSheet() { + step("GIVEN user logs in") { + userRobot.login().waitForChannelListToLoad() + } + step("AND user long presses the channel") { + userRobot.openChannelMenu() + } + step("WHEN user taps on the view info option") { + userRobot.tapOnViewChannelInfo() + } + step("THEN the group channel info screen is shown") { + userRobot.assertGroupChannelInfoScreen() + } + } +} diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/MessageActionsTests.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/MessageActionsTests.kt new file mode 100644 index 00000000000..29998c046e3 --- /dev/null +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/MessageActionsTests.kt @@ -0,0 +1,46 @@ +/* + * 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.assertMessageCopied +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 + +class MessageActionsTests : StreamTestCase() { + + override fun initTestActivity() = InitTestActivity.UserLogin + private val sampleText = "Test" + + @AllureId("11418") + @Test + fun test_userCopiesMessage() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN participant sends the message") { + participantRobot.sendMessage(sampleText) + } + step("AND user copies the message") { + userRobot.copyMessage() + } + step("THEN the message text is in the clipboard") { + userRobot.assertMessageCopied(sampleText) + } + } +} diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/PinnedMessagesTests.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/PinnedMessagesTests.kt new file mode 100644 index 00000000000..24b2b706ae0 --- /dev/null +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/PinnedMessagesTests.kt @@ -0,0 +1,115 @@ +/* + * 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.assertMessageDeliveryStatus +import io.getstream.chat.android.compose.robots.assertMessagePinnedLabel +import io.getstream.chat.android.compose.sample.ui.InitTestActivity +import io.getstream.chat.android.e2e.test.mockserver.MessageDeliveryStatus +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 + +class PinnedMessagesTests : StreamTestCase() { + + override fun initTestActivity() = InitTestActivity.UserLogin + private val sampleText = "Test" + + @AllureId("5953") + @Test + fun test_userPinsMessage() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN participant sends the message") { + participantRobot.sendMessage(sampleText) + } + step("AND user pins the message") { + userRobot.pinMessage() + } + step("THEN the message shows the pinned by you label") { + userRobot.assertMessagePinnedLabel() + } + } + + @AllureId("11383") + @Test + fun test_userUnpinsMessage() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND participant sends the message") { + participantRobot.sendMessage(sampleText) + } + step("AND user pins the message") { + userRobot.pinMessage() + } + step("WHEN user unpins the message") { + userRobot.unpinMessage() + } + step("THEN the message shows no pinned label") { + userRobot.assertMessagePinnedLabel(isDisplayed = false) + } + } + + @AllureId("11384") + @Test + fun test_participantPinsMessage() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user sends the message") { + userRobot.sendMessage(sampleText) + } + step("AND the message is delivered") { + userRobot.assertMessageDeliveryStatus(MessageDeliveryStatus.SENT) + } + step("AND participant pins the message") { + participantRobot.pinMessage() + } + step("THEN the message shows the pinned by participant label") { + userRobot.assertMessagePinnedLabel(pinnedBy = ParticipantRobot.name) + } + } + + @AllureId("11385") + @Test + fun test_participantUnpinsMessage() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND user sends the message") { + userRobot.sendMessage(sampleText) + } + step("AND the message is delivered") { + userRobot.assertMessageDeliveryStatus(MessageDeliveryStatus.SENT) + } + step("AND participant pins the message") { + participantRobot.pinMessage() + } + step("AND the message shows the pinned by participant label") { + userRobot.assertMessagePinnedLabel(pinnedBy = ParticipantRobot.name) + } + step("WHEN participant unpins the message") { + participantRobot.unpinMessage() + } + step("THEN the message shows no pinned label") { + userRobot.assertMessagePinnedLabel(pinnedBy = ParticipantRobot.name, isDisplayed = false) + } + } +} diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ReactionsTests.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ReactionsTests.kt index 139c9a41d47..bc3d69e0d75 100644 --- a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ReactionsTests.kt +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ReactionsTests.kt @@ -18,8 +18,10 @@ package io.getstream.chat.android.compose.tests import io.getstream.chat.android.compose.pages.MessageListPage.MessageList.Message import io.getstream.chat.android.compose.robots.assertReaction +import io.getstream.chat.android.compose.robots.assertReactionAuthor import io.getstream.chat.android.compose.sample.ui.InitTestActivity import io.getstream.chat.android.e2e.test.mockserver.ReactionType +import io.getstream.chat.android.e2e.test.robots.ParticipantRobot import io.getstream.chat.android.e2e.test.uiautomator.device import io.getstream.chat.android.e2e.test.uiautomator.disableInternetConnection import io.getstream.chat.android.e2e.test.uiautomator.enableInternetConnection @@ -71,6 +73,64 @@ class ReactionsTests : StreamTestCase() { } } + @AllureId("11451") + @Test + fun test_userAddsReactionUsingExtendedReactionsPicker() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN participant sends the message") { + participantRobot.sendMessage(sampleText) + } + step("AND user adds the reaction using the extended reactions picker") { + userRobot.toggleReactionUsingExtendedPicker(type = ReactionType.LOL) + } + step("THEN the reaction is added") { + userRobot.assertReaction(type = ReactionType.LOL, isDisplayed = true) + } + } + + @AllureId("11452") + @Test + fun test_userRemovesReactionUsingExtendedReactionsPicker() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND participant sends the message") { + participantRobot.sendMessage(sampleText) + } + step("AND user adds the reaction using the extended reactions picker") { + userRobot.toggleReactionUsingExtendedPicker(type = ReactionType.LOL) + } + step("WHEN user selects the same reaction in the extended reactions picker") { + userRobot.toggleReactionUsingExtendedPicker(type = ReactionType.LOL) + } + step("THEN the reaction is removed") { + userRobot.assertReaction(type = ReactionType.LOL, isDisplayed = false) + } + } + + @AllureId("11459") + @Test + fun test_reactionAuthorsSheetIsShown_whenUserTapsOnReaction() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND participant sends the message") { + participantRobot.sendMessage(sampleText) + } + step("AND participant adds the reaction") { + participantRobot.addReaction(type = ReactionType.LOVE) + } + step("WHEN user taps on the message reaction") { + userRobot.assertReaction(type = ReactionType.LOVE, isDisplayed = true) + userRobot.tapOnMessageReaction() + } + step("THEN the reaction authors sheet shows the participant") { + userRobot.assertReactionAuthor(ParticipantRobot.name) + } + } + @AllureId("5676") @Test fun test_reactionIsAdded_whenReactingToParticipantsMessage() { diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/UnreadMessagesTests.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/UnreadMessagesTests.kt new file mode 100644 index 00000000000..a9ae767775a --- /dev/null +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/UnreadMessagesTests.kt @@ -0,0 +1,106 @@ +/* + * 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.assertScrollToFirstUnreadButton +import io.getstream.chat.android.compose.robots.assertUnreadSeparator +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.Ignore +import org.junit.Test + +class UnreadMessagesTests : StreamTestCase() { + + override fun initTestActivity() = InitTestActivity.UserLogin + private val sampleText = "Test" + + @AllureId("11453") + @Ignore("https://linear.app/stream/issue/AND-1329") + @Test + fun test_unreadSeparatorIsShown_whenParticipantSendsMessagesWhileUserIsAway() { + val unreadCount = 2 + step("GIVEN user opens the channel and sends the message") { + userRobot.login().openChannel().sendMessage(sampleText) + } + step("AND user moves back to the channel list") { + userRobot.moveToChannelListFromMessageList() + } + step("WHEN participant sends new messages") { + participantRobot.sendMultipleMessages(text = "New", count = unreadCount) + } + step("AND user reopens the channel") { + userRobot.openChannel() + } + step("THEN the unread separator is shown with the unread count") { + userRobot.assertUnreadSeparator(unreadCount = unreadCount) + } + } + + @AllureId("11454") + @Ignore("https://linear.app/stream/issue/AND-1329") + @Test + fun test_userScrollsToFirstUnreadMessage() { + val unreadCount = 25 + step("GIVEN user opens the channel and sends the message") { + userRobot.login().openChannel().sendMessage(sampleText) + } + step("AND user moves back to the channel list") { + userRobot.moveToChannelListFromMessageList() + } + step("AND participant sends new messages") { + participantRobot.sendMultipleMessages(text = "New", count = unreadCount) + } + step("WHEN user reopens the channel") { + userRobot.openChannel() + } + step("THEN the scroll to first unread button is shown with the unread count") { + userRobot.assertScrollToFirstUnreadButton(unreadCount = unreadCount) + } + step("WHEN user taps on the scroll to first unread button") { + userRobot.tapOnScrollToFirstUnreadButton() + } + step("THEN the list scrolls to the unread separator") { + userRobot.assertUnreadSeparator(unreadCount = unreadCount) + } + } + + @AllureId("11455") + @Ignore("https://linear.app/stream/issue/AND-1329") + @Test + fun test_userDismissesTheUnreadIndicator() { + val unreadCount = 25 + step("GIVEN user opens the channel and sends the message") { + userRobot.login().openChannel().sendMessage(sampleText) + } + step("AND user moves back to the channel list") { + userRobot.moveToChannelListFromMessageList() + } + step("AND participant sends new messages") { + participantRobot.sendMultipleMessages(text = "New", count = unreadCount) + } + step("AND user reopens the channel") { + userRobot.openChannel() + } + step("WHEN user dismisses the unread indicator") { + userRobot.dismissUnreadIndicator() + } + step("THEN the scroll to first unread button is hidden") { + userRobot.assertScrollToFirstUnreadButton(isDisplayed = false) + } + } +} diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/component/MembersList.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/component/MembersList.kt deleted file mode 100644 index b70a02b4d6a..00000000000 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/component/MembersList.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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.sample.ui.component - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.chat.android.compose.sample.vm.MembersViewModel - -@Composable -internal fun MembersList( - viewModel: MembersViewModel, -) { - val memberNames by viewModel.memberNames.collectAsStateWithLifecycle() - LazyRow( - modifier = Modifier.height(32.dp) - .fillMaxWidth() - .background(color = Color.LightGray), - verticalAlignment = Alignment.CenterVertically, - ) { - itemsIndexed(memberNames) { index, (name, isCurrentUser) -> - if (index == 0) { - Spacer(modifier = Modifier.width(16.dp)) - } - val displayName = when (index == memberNames.lastIndex) { - true -> name - else -> "$name, " - } - Text( - modifier = Modifier.testTag("Stream_MembersList"), - fontWeight = if (isCurrentUser) FontWeight.Bold else FontWeight.Normal, - text = displayName, - ) - if (index == memberNames.lastIndex) { - Spacer(modifier = Modifier.width(16.dp)) - } else { - Spacer(modifier = Modifier.width(4.dp)) - } - } - } -} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/StreamModalBottomSheet.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/StreamModalBottomSheet.kt index 4c6584b4832..b82c97c1068 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/StreamModalBottomSheet.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/StreamModalBottomSheet.kt @@ -36,6 +36,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.ui.theme.ChatTheme @@ -68,7 +70,9 @@ internal fun StreamCardBottomSheet( ) { ModalBottomSheet( onDismissRequest = onDismissRequest, - modifier = modifier, + // The sheet renders in its own window, so the app-level `testTagsAsResourceId` + // does not reach it; re-enable it here for UiAutomator access to the content tags. + modifier = modifier.semantics { testTagsAsResourceId = true }, sheetState = sheetState, shape = StreamCardSheetShape, containerColor = ChatTheme.colors.backgroundCoreElevation1, @@ -135,7 +139,9 @@ internal fun StreamScreenBottomSheet( ) { ModalBottomSheet( onDismissRequest = onDismissRequest, - modifier = modifier, + // The sheet renders in its own window, so the app-level `testTagsAsResourceId` + // does not reach it; re-enable it here for UiAutomator access to the content tags. + modifier = modifier.semantics { testTagsAsResourceId = true }, sheetState = sheetState, sheetMaxWidth = Dp.Unspecified, shape = RectangleShape, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messageactions/UserReactionRow.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messageactions/UserReactionRow.kt index d8d27c6135f..dbffe685d9f 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messageactions/UserReactionRow.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messageactions/UserReactionRow.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -57,6 +58,7 @@ internal fun UserReactionRow( ) { Row( modifier = modifier + .testTag("Stream_ReactionAuthor") .fillMaxWidth() .ifNotNull(onClick) { clickable(onClick = it) } .padding(horizontal = StreamTokens.spacingSm, vertical = StreamTokens.spacingXs),