From d3a197c2c03e95170887adc1081e7487e1092e37 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Jan 2025 13:38:31 +0000 Subject: [PATCH] [CI] Add MessageList E2E tests (#5530) --- .github/workflows/e2e-test.yml | 1 + fastlane/Fastfile | 13 +- .../android/compose/pages/MessageListPage.kt | 5 + .../chat/android/compose/robots/UserRobot.kt | 51 +- .../robots/UserRobotChannelListAsserts.kt | 12 +- .../robots/UserRobotMessageListAsserts.kt | 228 +++++- .../android/compose/tests/MessageListTests.kt | 768 +++++++++++++++++- .../android/compose/tests/StreamTestCase.kt | 2 + .../messages/factory/MessageContentFactory.kt | 3 +- .../components/suggestions/SuggestionList.kt | 6 +- .../compose/ui/messages/list/MessageItem.kt | 3 +- .../api/stream-chat-android-e2e-test.api | 32 +- .../src/main/AndroidManifest.xml | 2 +- .../android/e2e/test/mockserver/DataTypes.kt | 6 + .../android/e2e/test/mockserver/MockServer.kt | 1 + .../android/e2e/test/robots/BackendRobot.kt | 33 + .../e2e/test/robots/ParticipantRobot.kt | 43 +- .../android/e2e/test/uiautomator/Actions.kt | 26 +- .../android/e2e/test/uiautomator/Element.kt | 10 +- .../chat/android/e2e/test/uiautomator/Math.kt | 5 + .../chat/android/e2e/test/uiautomator/Wait.kt | 13 + 21 files changed, 1207 insertions(+), 56 deletions(-) create mode 100644 stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/robots/BackendRobot.kt diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 9b508514c1b..fbda3a00c24 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -29,6 +29,7 @@ jobs: include: - batch: 0 - batch: 1 + fail-fast: false env: ANDROID_API_LEVEL: 34 steps: diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 1ee83114066..8c5b4acbe3e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -18,11 +18,16 @@ before_all do |lane| end end -lane :start_mock_server do - stop_mock_server if is_localhost +lane :start_mock_server do |options| mock_server_repo = 'stream-chat-test-mock-server' - sh("rm -rf #{mock_server_repo}") if File.directory?(mock_server_repo) - sh("git clone git@github.com:#{github_repo.split('/').first}/#{mock_server_repo}.git") + stop_mock_server if is_localhost + + if options[:local_server] + mock_server_repo = options[:local_server] + else + sh("rm -rf #{mock_server_repo}") if File.directory?(mock_server_repo) + sh("git clone git@github.com:#{github_repo.split('/').first}/#{mock_server_repo}.git") + end Dir.chdir(mock_server_repo) do FileUtils.mkdir_p('logs') 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 f3c1cea93aa..60dbc14afe3 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 @@ -73,6 +73,7 @@ open class MessageListPage { class MessageList { companion object { + val messageList = By.res("Stream_MessageList") val messages = By.res("Stream_MessageCell") val dateSeparator = By.res("Stream_MessageDateSeparator") val unreadMessagesBadge = By.res("Stream_UnreadMessagesBadge") @@ -90,6 +91,7 @@ open class MessageListPage { val readStatusIsRead = By.res("Stream_MessageReadStatus_isRead") val readStatusIsPending = By.res("Stream_MessageReadStatus_isPending") val readStatusIsSent = By.res("Stream_MessageReadStatus_isSent") + val failedIcon = By.res("Stream_MessageFailedIcon") val readCount = By.res("Stream_MessageReadCount") val timestamp = By.res("Stream_Timestamp") val reactions = By.res("Stream_MessageReaction") @@ -98,6 +100,7 @@ open class MessageListPage { val threadRepliesLabel = By.res("Stream_ThreadRepliesLabel") val threadParticipantAvatar = By.res("Stream_ThreadParticipantAvatar") val editedLabel = By.res("Stream_MessageEditedLabel") + val deletedMessage = By.res("Stream_MessageDeleted") val messageHeaderLabel = By.res("Stream_MessageHeaderLabel") // e.g.: Pinned by you val image = By.res("Stream_MediaContent") val video = By.res("Stream_PlayButton") @@ -145,6 +148,7 @@ open class MessageListPage { companion object { val reply = By.res("Stream_ContextMenu_Reply") + val resend = By.res("Stream_ContextMenu_Resend") val threadReply = By.res("Stream_ContextMenu_Thread reply") val markAsUnread = By.res("Stream_ContextMenu_Mark as Unread") val copy = By.res("Stream_ContextMenu_Copy Message") @@ -154,6 +158,7 @@ open class MessageListPage { val unpin = By.res("Stream_ContextMenu_Unpin from this Chat") val block = By.res("Stream_ContextMenu_Block user") val delete = By.res("Stream_ContextMenu_Delete Message") + 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 1206187c744..09fe743c94d 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 @@ -16,6 +16,7 @@ package io.getstream.chat.android.compose.robots +import androidx.test.uiautomator.By import androidx.test.uiautomator.Direction import io.getstream.chat.android.compose.pages.ChannelListPage import io.getstream.chat.android.compose.pages.LoginPage @@ -23,6 +24,7 @@ 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 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.compose.uiautomator.defaultTimeout import io.getstream.chat.android.compose.uiautomator.device @@ -31,11 +33,13 @@ import io.getstream.chat.android.compose.uiautomator.findObjects import io.getstream.chat.android.compose.uiautomator.longPress import io.getstream.chat.android.compose.uiautomator.swipeDown import io.getstream.chat.android.compose.uiautomator.swipeUp +import io.getstream.chat.android.compose.uiautomator.tapOnScreenCenter import io.getstream.chat.android.compose.uiautomator.typeText import io.getstream.chat.android.compose.uiautomator.wait import io.getstream.chat.android.compose.uiautomator.waitToAppear import io.getstream.chat.android.compose.uiautomator.waitToDisappear import io.getstream.chat.android.e2e.test.mockserver.ReactionType +import io.getstream.chat.android.e2e.test.robots.ParticipantRobot class UserRobot { @@ -87,17 +91,24 @@ class UserRobot { fun deleteMessage(messageCellIndex: Int = 0, hard: Boolean = false): UserRobot { openContextMenu(messageCellIndex) - Message.ContextMenu.delete.waitToAppear().click() + ContextMenu.delete.waitToAppear().click() + ContextMenu.ok.findObject().click() return this } fun editMessage(newText: String, messageCellIndex: Int = 0): UserRobot { openContextMenu(messageCellIndex) - Message.ContextMenu.edit.waitToAppear().click() + ContextMenu.edit.waitToAppear().click() sendMessage(newText) return this } + fun resendMessage(messageCellIndex: Int = 0): UserRobot { + openContextMenu(messageCellIndex) + ContextMenu.resend.waitToAppear().click() + return this + } + fun clearComposer(): UserRobot { Composer.inputField.waitToAppear().clear() return this @@ -121,7 +132,7 @@ class UserRobot { fun quoteMessage(text: String, messageCellIndex: Int = 0): UserRobot { openContextMenu(messageCellIndex) - Message.ContextMenu.reply.waitToAppear().click() + ContextMenu.reply.waitToAppear().click() sendMessage(text) return this } @@ -129,7 +140,7 @@ class UserRobot { fun openThread(messageCellIndex: Int = 0, usingContextMenu: Boolean = true): UserRobot { if (usingContextMenu) { openContextMenu(messageCellIndex) - Message.ContextMenu.threadReply.waitToAppear().click() + ContextMenu.threadReply.waitToAppear().click() } else { Message.threadRepliesLabel.waitToAppear().click() } @@ -151,6 +162,17 @@ class UserRobot { return this } + fun sendMessageInThread( + text: String, + alsoSendInChannel: Boolean = false, + ): UserRobot { + if (alsoSendInChannel) { + ThreadPage.ThreadList.alsoSendToChannelCheckbox.waitToAppear().click() + } + sendMessage(text) + return this + } + fun quoteMessageInThread( text: String, alsoSendInChannel: Boolean = false, @@ -176,22 +198,22 @@ class UserRobot { return this } - fun scrollChannelListDown(times: Int = 1): UserRobot { + fun scrollChannelListDown(times: Int = 3): UserRobot { device.swipeUp(times) return this } - fun scrollChannelListUp(times: Int = 1): UserRobot { + fun scrollChannelListUp(times: Int = 3): UserRobot { device.swipeDown(times) return this } - fun scrollMessageListDown(times: Int = 1): UserRobot { + fun scrollMessageListDown(times: Int = 3): UserRobot { scrollChannelListDown(times) // Reusing the channel list scroll return this } - fun scrollMessageListUp(times: Int = 1): UserRobot { + fun scrollMessageListUp(times: Int = 3): UserRobot { scrollChannelListUp(times) // Reusing the channel list scroll return this } @@ -206,6 +228,11 @@ class UserRobot { return this } + fun openAttachmentsMenu(): UserRobot { + Composer.attachmentsButton.waitToAppear().click() + return this + } + fun uploadGiphy(useComposerCommand: Boolean = false, send: Boolean = true): UserRobot { val giphyMessageText = "G" // any message text will result in sending a giphy if (useComposerCommand) { @@ -263,9 +290,9 @@ class UserRobot { fun mentionParticipant(useSuggestions: Boolean = true, send: Boolean = true): UserRobot { if (useSuggestions) { typeText("@") - Composer.participantMentionSuggestion.waitToAppear().click() + By.text(ParticipantRobot.name).waitToAppear().click() } else { - typeText("@Han Solo") + typeText("@${ParticipantRobot.name}") } if (send) { @@ -273,4 +300,8 @@ class UserRobot { } return this } + + fun tapOnMessageList() { + device.tapOnScreenCenter() + } } 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 4681eaf99b2..12109e2e365 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 @@ -17,7 +17,7 @@ package io.getstream.chat.android.compose.robots import io.getstream.chat.android.compose.pages.ChannelListPage.ChannelList.Channel -import io.getstream.chat.android.compose.uiautomator.exists +import io.getstream.chat.android.compose.uiautomator.isDisplayed import io.getstream.chat.android.compose.uiautomator.wait import io.getstream.chat.android.compose.uiautomator.waitToAppear import org.junit.Assert.assertEquals @@ -25,24 +25,24 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue fun UserRobot.assertChannelAvatar(): UserRobot { - assertTrue(Channel.avatar.exists()) + assertTrue(Channel.avatar.isDisplayed()) return this } fun UserRobot.assertMessageInChannelPreview(text: String, fromCurrentUser: Boolean): UserRobot { val expectedPreview = if (fromCurrentUser) "You: $text" else text assertEquals(expectedPreview, Channel.messagePreview.waitToAppear().text.trimEnd()) - assertTrue(Channel.timestamp.exists()) + assertTrue(Channel.timestamp.isDisplayed()) return this } fun UserRobot.assertMessageDeliveryStatus(shouldBeVisible: Boolean, shouldBeRead: Boolean = false): UserRobot { if (shouldBeVisible) { val readStatus = if (shouldBeRead) Channel.readStatusIsRead else Channel.readStatusIsSent - assertTrue(readStatus.wait().exists()) + assertTrue(readStatus.wait().isDisplayed()) } else { - assertFalse(Channel.readStatusIsRead.exists()) - assertFalse(Channel.readStatusIsSent.exists()) + assertFalse(Channel.readStatusIsRead.isDisplayed()) + assertFalse(Channel.readStatusIsSent.isDisplayed()) } return this } 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 09527e7b422..1f2b0df2d71 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 @@ -16,21 +16,237 @@ package io.getstream.chat.android.compose.robots +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.uiautomator.exists +import io.getstream.chat.android.compose.pages.ThreadPage +import io.getstream.chat.android.compose.uiautomator.appContext +import io.getstream.chat.android.compose.uiautomator.findObject +import io.getstream.chat.android.compose.uiautomator.findObjects +import io.getstream.chat.android.compose.uiautomator.height +import io.getstream.chat.android.compose.uiautomator.isDisplayed +import io.getstream.chat.android.compose.uiautomator.wait +import io.getstream.chat.android.compose.uiautomator.waitForText import io.getstream.chat.android.compose.uiautomator.waitToAppear +import io.getstream.chat.android.compose.uiautomator.waitToDisappear +import io.getstream.chat.android.e2e.test.mockserver.MessageReadStatus +import io.getstream.chat.android.e2e.test.robots.ParticipantRobot import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertNotEquals import org.junit.Assert.assertTrue -fun UserRobot.assertMessage(text: String): UserRobot { - assertEquals(text, Message.text.waitToAppear().text) - assertTrue(Message.timestamp.exists()) +fun UserRobot.assertMessage(text: String, isDisplayed: Boolean = true): UserRobot { + if (isDisplayed) { + assertEquals(text, Message.text.waitToAppear().waitForText(text).text) + assertTrue(Message.text.isDisplayed()) + assertTrue(Message.timestamp.isDisplayed()) + } else { + MessageListPage.MessageList.messages.findObjects().forEach { + assertTrue(it.text != text) + } + } return this } fun UserRobot.assertMessageAuthor(isCurrentUser: Boolean): UserRobot { - assertNotEquals(isCurrentUser, Message.authorName.exists()) - assertNotEquals(isCurrentUser, Message.avatar.exists()) + assertNotEquals(isCurrentUser, Message.authorName.isDisplayed()) + assertNotEquals(isCurrentUser, Message.avatar.isDisplayed()) + return this +} + +fun UserRobot.assertMessageTimestamps(count: Int): UserRobot { + assertEquals(count, Message.timestamp.findObjects().size) + return this +} + +fun UserRobot.assertMessageReadStatus(status: MessageReadStatus): UserRobot { + when (status) { + MessageReadStatus.READ -> assertTrue(Message.readStatusIsRead.wait().isDisplayed()) + MessageReadStatus.PENDING -> assertTrue(Message.readStatusIsPending.wait().isDisplayed()) + MessageReadStatus.SENT -> assertTrue(Message.readStatusIsSent.wait().isDisplayed()) + } + return this +} + +fun UserRobot.assertMessageFailedIcon(isDisplayed: Boolean): UserRobot { + if (isDisplayed) { + assertTrue(Message.failedIcon.wait().isDisplayed()) + } else { + assertFalse(Message.failedIcon.waitToDisappear().isDisplayed()) + } + return this +} + +fun UserRobot.assertEditedMessage(text: String): UserRobot { + assertMessage(text) + assertEquals( + appContext.getString(R.string.stream_compose_message_list_footnote_edited), + Message.editedLabel.waitToAppear().text, + ) + return this +} + +fun UserRobot.assertDeletedMessage(text: String, hard: Boolean = false): UserRobot { + if (hard) { + assertFalse(Message.deletedMessage.isDisplayed()) + } else { + Message.deletedMessage.waitToAppear() + assertTrue(Message.deletedMessage.isDisplayed()) + assertTrue(Message.timestamp.isDisplayed()) + } + assertMessage(text, isDisplayed = false) + return this +} + +fun UserRobot.assertMessageSizeChangesAfterEditing(linesCountShouldBeIncreased: Boolean): UserRobot { + val cellHeight = MessageListPage.MessageList.messages.waitToAppear(withIndex = 0).height + val messageText = Message.text.findObject().text + val newLine = "new line" + val newText = if (linesCountShouldBeIncreased) "ok\n${messageText}\n$newLine" else newLine + + editMessage(newText) + assertMessage(newText) + + val updatedCellHeight = MessageListPage.MessageList.messages.findObjects().first().height + if (linesCountShouldBeIncreased) { + assertTrue(cellHeight < updatedCellHeight) + } else { + assertTrue(cellHeight > updatedCellHeight) + } + return this +} + +fun UserRobot.assertComposerSize(isChangeable: Boolean): UserRobot { + val composer = Composer.inputField + val initialComposerHeight: Int + if (isChangeable) { + initialComposerHeight = composer.findObject().height + val text = "1\n2\n3" + typeText(text) + assertTrue(initialComposerHeight != composer.findObject().height) + } else { + val text = "1\n2\n3\n4\n5\n6" + typeText(text) + initialComposerHeight = composer.findObject().height + typeText("${text}\n7") + assertEquals(initialComposerHeight, composer.findObject().height) + } + return this +} + +fun UserRobot.assertTypingIndicator(isDisplayed: Boolean): UserRobot { + if (isDisplayed) { + assertEquals( + appContext.resources.getQuantityString( + R.plurals.stream_compose_message_list_header_typing_users, + 1, + ParticipantRobot.name, + ), + MessageListPage.MessageList.typingIndicator.waitToAppear().text, + ) + } else { + assertFalse(MessageListPage.MessageList.typingIndicator.waitToDisappear().isDisplayed()) + } + return this +} + +fun UserRobot.assertAttachmentsMenu(isDisplayed: Boolean): UserRobot { + if (isDisplayed) { + assertTrue(MessageListPage.AttachmentPicker.view.waitToAppear().isDisplayed()) + } else { + assertFalse(MessageListPage.AttachmentPicker.view.waitToDisappear().isDisplayed()) + } + return this +} + +fun UserRobot.assertComposerCommandsMenu(isDisplayed: Boolean): UserRobot { + if (isDisplayed) { + assertTrue(Composer.suggestionList.waitToAppear().isDisplayed()) + assertTrue(Composer.suggestionListTitle.isDisplayed()) + } else { + assertFalse(Composer.suggestionList.waitToDisappear().isDisplayed()) + assertFalse(Composer.suggestionListTitle.isDisplayed()) + } + return this +} + +fun UserRobot.assertComposerMentionsMenu(isDisplayed: Boolean): UserRobot { + if (isDisplayed) { + assertTrue(Composer.participantMentionSuggestion.waitToAppear().isDisplayed()) + } else { + assertFalse(Composer.participantMentionSuggestion.waitToDisappear().isDisplayed()) + } + return this +} + +fun UserRobot.assertMentionWasApplied(): UserRobot { + val additionalSpace = " " + val userName = ParticipantRobot.name + val expectedText = "@${userName}$additionalSpace" + val actualText = Composer.inputField.findObject().waitForText(expectedText).text + assertEquals(expectedText, actualText) + return this +} + +fun UserRobot.assertScrollToBottomButton(isDisplayed: Boolean): UserRobot { + if (isDisplayed) { + assertTrue(MessageListPage.MessageList.scrollToBottomButton.waitToAppear().isDisplayed()) + } else { + assertFalse(MessageListPage.MessageList.scrollToBottomButton.waitToDisappear().isDisplayed()) + } + return this +} + +fun UserRobot.assertLinkPreview(): UserRobot { + assertTrue(Message.linkAttachmentPreview.waitToAppear().isClickable) + assertTrue(Message.linkAttachmentTitle.findObject().text.isNotEmpty()) + assertTrue(Message.linkAttachmentDescription.findObject().text.isNotEmpty()) + return this +} + +fun UserRobot.assertThreadIsOpen(): UserRobot { + assertTrue(ThreadPage.ThreadList.alsoSendToChannelCheckbox.waitToAppear().isDisplayed()) + return this +} + +fun UserRobot.assertThreadMessage(text: String): UserRobot { + assertThreadIsOpen() + assertMessage(text) + return this +} + +fun UserRobot.assertThreadReplyLabelOnParentMessage(): UserRobot { + assertEquals( + Message.threadRepliesLabel.waitToAppear().text, + appContext.getString(R.string.stream_compose_message_list_thread_footnote_thread_reply), + ) + assertTrue(Message.threadParticipantAvatar.isDisplayed()) + return this +} + +fun UserRobot.assertThreadReplyLabelOnThreadMessage(): UserRobot { + assertEquals( + Message.threadRepliesLabel.waitToAppear().text, + appContext.getString(R.string.stream_compose_thread_reply), + ) + assertTrue(Message.threadParticipantAvatar.isDisplayed()) + return this +} + +fun UserRobot.assertAlsoInTheChannelLabelInChannel(): UserRobot { + assertEquals( + Message.messageHeaderLabel.waitToAppear().text, + appContext.getString(R.string.stream_compose_replied_to_thread), + ) + return this +} + +fun UserRobot.assertAlsoInTheChannelLabelInThread(): UserRobot { + assertEquals( + Message.messageHeaderLabel.waitToAppear().text, + appContext.getString(R.string.stream_compose_also_sent_to_channel), + ) return this } diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/MessageListTests.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/MessageListTests.kt index 7d53bc1b60b..f94e28a0db1 100644 --- a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/MessageListTests.kt +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/MessageListTests.kt @@ -16,16 +16,43 @@ package io.getstream.chat.android.compose.tests +import io.getstream.chat.android.compose.robots.assertAlsoInTheChannelLabelInChannel +import io.getstream.chat.android.compose.robots.assertAlsoInTheChannelLabelInThread +import io.getstream.chat.android.compose.robots.assertComposerCommandsMenu +import io.getstream.chat.android.compose.robots.assertComposerMentionsMenu +import io.getstream.chat.android.compose.robots.assertComposerSize +import io.getstream.chat.android.compose.robots.assertDeletedMessage +import io.getstream.chat.android.compose.robots.assertEditedMessage +import io.getstream.chat.android.compose.robots.assertMentionWasApplied import io.getstream.chat.android.compose.robots.assertMessage import io.getstream.chat.android.compose.robots.assertMessageAuthor +import io.getstream.chat.android.compose.robots.assertMessageFailedIcon +import io.getstream.chat.android.compose.robots.assertMessageReadStatus +import io.getstream.chat.android.compose.robots.assertMessageSizeChangesAfterEditing +import io.getstream.chat.android.compose.robots.assertMessageTimestamps +import io.getstream.chat.android.compose.robots.assertScrollToBottomButton +import io.getstream.chat.android.compose.robots.assertThreadMessage +import io.getstream.chat.android.compose.robots.assertThreadReplyLabelOnParentMessage +import io.getstream.chat.android.compose.robots.assertThreadReplyLabelOnThreadMessage +import io.getstream.chat.android.compose.robots.assertTypingIndicator +import io.getstream.chat.android.compose.uiautomator.device +import io.getstream.chat.android.compose.uiautomator.disableInternetConnection +import io.getstream.chat.android.compose.uiautomator.enableInternetConnection +import io.getstream.chat.android.compose.uiautomator.goToBackground +import io.getstream.chat.android.compose.uiautomator.goToForeground +import io.getstream.chat.android.e2e.test.mockserver.MessageReadStatus +import io.getstream.chat.android.e2e.test.mockserver.forbiddenWord import io.qameta.allure.kotlin.Allure.step import io.qameta.allure.kotlin.AllureId +import org.junit.Ignore import org.junit.Test class MessageListTests : StreamTestCase() { private val sampleText = "Test" + // MARK: Message sending + @AllureId("5661") @Test fun test_messageListUpdates_whenParticipantSendsMessage() { @@ -53,12 +80,751 @@ class MessageListTests : StreamTestCase() { .openChannel() } step("WHEN user sends a message") { - userRobot.sendMessage("Test") + userRobot.sendMessage(sampleText) } step("THEN message list updates") { userRobot .assertMessage(sampleText) .assertMessageAuthor(isCurrentUser = true) + .assertMessageReadStatus(MessageReadStatus.SENT) + } + step("WHEN participant reads the message") { + participantRobot.readMessage() + } + step("THEN the message is read") { + userRobot.assertMessageReadStatus(MessageReadStatus.READ) + } + } + + @AllureId("5695") + @Test + fun test_userSendsMessageWithOneEmoji() { + val message = "🤖" + + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user sends the emoji: $message") { + userRobot.sendMessage(message) + } + step("THEN the message is delivered") { + userRobot + .assertMessage(message) + .assertMessageReadStatus(MessageReadStatus.SENT) + } + } + + @AllureId("5697") + @Test + fun test_userSendsMessageWithMultipleEmojis() { + val message = "🤖🔥✅" + + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user sends a message with multiple emojis: $message") { + userRobot.sendMessage(message) + } + step("THEN the message is delivered") { + userRobot + .assertMessage(message) + .assertMessageReadStatus(MessageReadStatus.SENT) + } + } + + // MARK: Message editing + + @AllureId("5673") + @Test + fun test_userEditsMessage() { + val editedMessage = "hello" + + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user sends the message") { + userRobot.sendMessage(sampleText) + } + step("AND user edits the message") { + userRobot.editMessage(editedMessage) + } + step("THEN the message is edited") { + userRobot + .assertEditedMessage(editedMessage) + .assertMessageReadStatus(MessageReadStatus.SENT) + } + } + + @AllureId("5674") + @Test + fun test_participantEditsMessage() { + val editedMessage = "hello" + + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN participant sends the message") { + participantRobot.sendMessage(sampleText) + userRobot.assertMessage(sampleText) + } + step("AND participant edits the message") { + participantRobot.editMessage(editedMessage) + } + step("THEN the message is edited") { + userRobot.assertEditedMessage(editedMessage) + } + } + + // MARK: Message size + + @AllureId("5718") + @Test + fun test_messageIncreases_whenUserEditsMessageWithOneLineText() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND user sends a one line message: $sampleText") { + userRobot.sendMessage(sampleText) + } + step("THEN user verifies that message cell increases after editing") { + userRobot.assertMessageSizeChangesAfterEditing(linesCountShouldBeIncreased = true) + } + } + + @AllureId("5719") + @Test + fun test_messageDecreases_whenUserEditsMessage() { + val message = "test\nmessage" + + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND user sends a two line message: $message") { + userRobot.sendMessage(message) + } + step("THEN user verifies that message cell decreases after editing") { + userRobot.assertMessageSizeChangesAfterEditing(linesCountShouldBeIncreased = false) + } + } + + // MARK: Composer + + @AllureId("5701") + @Test + fun test_composerSizeChange() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("THEN user verifies that composer does not grow more than 5 lines") { + userRobot.assertComposerSize(isChangeable = true) + } + } + + @AllureId("5871") + @Test + fun test_composerSizeDoesNotChange() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("THEN user verifies that composer size changes") { + userRobot.assertComposerSize(isChangeable = false) + } + } + + @Ignore("https://linear.app/stream/issue/AND-181") + @AllureId("5717") + @Test + fun test_commandsMenuCloses_whenUserTapsOnMessageList() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND user opens attachments menu") { + userRobot + .openComposerCommands() + .assertComposerCommandsMenu(isDisplayed = true) + } + step("WHEN user taps on message list") { + userRobot.tapOnMessageList() + } + step("THEN command suggestions disappear") { + userRobot.assertComposerCommandsMenu(isDisplayed = false) + } + } + + // MARK: Typing indicator + + @AllureId("5702") + @Test + fun test_typingIndicator() { + step("GIVEN user opens the channel") { + userRobot + .login() + .openChannel() + .sendMessage(sampleText) + } + step("WHEN participant starts typing") { + participantRobot.startTyping() + } + step("THEN user observes typing indicator is shown") { + userRobot.assertTypingIndicator(isDisplayed = true) + } + step("WHEN participant stops typing") { + participantRobot.stopTyping() + } + step("THEN user observes typing indicator has disappeared") { + userRobot.assertTypingIndicator(isDisplayed = false) + } + } + + @AllureId("5819") + @Test + fun test_threadTypingIndicatorHidden_whenParticipantStopsTyping() { + step("GIVEN user opens the channel") { + backendRobot.generateChannels(channelsCount = 1, messagesCount = 1) + userRobot.login().openChannel() + } + step("AND user opens the thread") { + userRobot.openThread() + } + step("WHEN participant starts typing in thread") { + participantRobot.startTypingInThread() + } + step("THEN user observes typing indicator is shown") { + userRobot.assertTypingIndicator(isDisplayed = true) + } + step("WHEN participant stops typing in thread") { + participantRobot.stopTypingInThread() + } + step("THEN user observes typing indicator has disappeared") { + userRobot.assertTypingIndicator(isDisplayed = false) + } + } + + // MARK: Offline mode + + @AllureId("6609") + @Test + fun test_offlineMessageInTheMessageList() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND user goes to background") { + device.goToBackground() + } + step("WHEN participant sends a new message") { + participantRobot.sendMessage(sampleText) + } + step("AND user becomes offline") { + device.disableInternetConnection() + } + step("AND user comes back to foreground") { + device.goToForeground() + } + step("THEN user does not observe a new message from participant") { + userRobot.assertMessage(sampleText, isDisplayed = false) + } + step("AND user becomes online") { + device.enableInternetConnection() + } + step("THEN user observes a new message from participant") { + userRobot.assertMessage(sampleText) + } + } + + @AllureId("5670") + @Test + fun test_userAddsMessageWhileOffline() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND user becomes offline") { + device.disableInternetConnection() + } + step("WHEN user sends a new message") { + userRobot.sendMessage(sampleText) + } + step("THEN error indicator is shown for the message") { + userRobot.assertMessageFailedIcon(isDisplayed = true) + } + step("WHEN user becomes online") { + device.enableInternetConnection() + } + step("AND user resends the message") { + userRobot.resendMessage() + } + step("THEN new message is delivered") { + userRobot + .assertMessageReadStatus(MessageReadStatus.SENT) + .assertMessageFailedIcon(isDisplayed = false) + } + } + + @AllureId("6610") + @Test + fun test_offlineRecoveryWithinSession() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND user goes to the background") { + device.goToBackground() + } + step("WHEN participant sends a new message") { + participantRobot.sendMessage(sampleText) + } + step("AND user comes back to the foreground") { + device.goToForeground() + } + step("THEN new message is delivered") { + userRobot + .assertMessage(sampleText) + .assertMessageAuthor(isCurrentUser = false) + } + } + + // MARK: Scrolling + + @Ignore("https://linear.app/stream/issue/AND-76") + @AllureId("5792") + @Test + fun test_messageListScrollsDown_whenMessageListIsScrolledUp_andUserSendsNewMessage() { + step("GIVEN user opens the channel") { + backendRobot.generateChannels(channelsCount = 1, messagesCount = 30) + userRobot.login().openChannel() + } + step("WHEN user scrolls up") { + userRobot.scrollMessageListUp() + } + step("AND user sends a new message") { + userRobot.sendMessage(sampleText) + } + step("THEN message list is scrolled down") { + userRobot + .assertScrollToBottomButton(isDisplayed = false) + .assertMessage(sampleText) + } + } + + @AllureId("5703") + @Test + fun test_messageListScrollsDown_whenMessageListIsScrolledDown_andUserReceivesNewMessage() { + step("GIVEN user opens the channel") { + backendRobot.generateChannels(channelsCount = 1, messagesCount = 30) + userRobot.login().openChannel() + } + step("WHEN participant sends a message") { + participantRobot.sendMessage(sampleText) + } + step("THEN message list is scrolled down") { + userRobot + .assertScrollToBottomButton(isDisplayed = false) + .assertMessage(sampleText) + } + } + + @AllureId("5793") + @Test + fun test_messageListDoesNotScrollDown_whenMessageListIsScrolledUp_andUserReceivesNewMessage() { + step("GIVEN user opens the channel") { + backendRobot.generateChannels(channelsCount = 1, messagesCount = 30) + userRobot.login().openChannel() + } + step("WHEN user scrolls up") { + userRobot.scrollMessageListUp() + } + step("AND participant sends a message") { + participantRobot.sendMessage(sampleText) + } + step("THEN message list is not scrolled down") { + userRobot + .assertMessage(sampleText, isDisplayed = false) + .assertScrollToBottomButton(isDisplayed = true) + } + step("WHEN user taps on scroll to botton button") { + userRobot.tapOnScrollToBottomButton() + } + step("THEN message list is scrolled down") { + userRobot + .assertMessage(sampleText) + .assertScrollToBottomButton(isDisplayed = false) + } + } + + // MARK: Mentions + + @AllureId("5693") + @Test + fun test_mentionsView() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user types '@'") { + userRobot.typeText("@") + } + step("THEN composer mention view appears") { + userRobot.assertComposerMentionsMenu(isDisplayed = true) + } + step("WHEN user removes '@'") { + userRobot.clearComposer() + } + step("THEN composer mention view disappears") { + userRobot.assertComposerMentionsMenu(isDisplayed = false) + } + } + + @AllureId("5694") + @Test + fun test_userFillsTheComposerMentioningParticipantThroughMentionsView() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user taps on participant's name") { + userRobot.mentionParticipant(send = false) + } + step("THEN composer fills in participant's name") { + userRobot.assertMentionWasApplied() + } + } + + // MARK: - Thread replies + + @AllureId("5683") + @Test + fun test_threadReplyAppearsInThread_whenParticipantAddsThreadReply() { + step("GIVEN user opens the channel") { + backendRobot.generateChannels(channelsCount = 1, messagesCount = 1) + userRobot.login().openChannel() + } + step("WHEN participant adds a thread reply") { + participantRobot.sendMessageInThread(sampleText, alsoSendInChannel = false) + } + step("AND user enters thread") { + userRobot + .assertThreadReplyLabelOnParentMessage() + .openThread(usingContextMenu = false) + } + step("THEN user observes the thread reply in thread") { + userRobot.assertThreadMessage(sampleText) + } + } + + @AllureId("5724") + @Test + fun test_threadReplyAppearsInChannelAndThread_whenParticipantAddsThreadReplySentAlsoToChannel() { + step("GIVEN user opens the channel") { + backendRobot.generateChannels(channelsCount = 1, messagesCount = 1) + userRobot.login().openChannel() + } + step("WHEN participant adds a thread reply and also sends it to the channel") { + participantRobot.sendMessageInThread(sampleText, alsoSendInChannel = true) + } + step("THEN user observes the thread reply in channel") { + userRobot + .assertMessage(sampleText) + .assertAlsoInTheChannelLabelInChannel() + .assertThreadReplyLabelOnThreadMessage() + } + step("WHEN user enters thread") { + userRobot.openThread(usingContextMenu = false) + } + step("THEN user observes the thread reply in thread") { + userRobot + .assertThreadMessage(sampleText) + .assertAlsoInTheChannelLabelInThread() + } + } + + @AllureId("5725") + @Test + fun test_threadReplyAppearsInChannelAndThread_whenUserAddsThreadReplySentAlsoToChannel() { + step("GIVEN user opens the channel") { + backendRobot.generateChannels(channelsCount = 1, messagesCount = 1) + userRobot.login().openChannel() + } + step("WHEN user adds a thread reply and sends it also to the main channel") { + userRobot.openThread().sendMessageInThread(sampleText, alsoSendInChannel = true) + } + step("THEN user observes the thread reply in thread") { + userRobot + .assertThreadMessage(sampleText) + .assertAlsoInTheChannelLabelInThread() + } + step("AND user observes the thread reply in channel") { + userRobot + .tapOnBackButton() + .assertMessage(sampleText) + .assertAlsoInTheChannelLabelInChannel() + .assertThreadReplyLabelOnThreadMessage() + } + } + + // MARK: Message deleting + + @AllureId("5671") + @Test + fun test_userDeletesMessage() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user sends the message") { + userRobot.sendMessage(sampleText) + } + step("AND user deletes the message") { + userRobot.deleteMessage() + } + step("THEN the message is deleted") { + userRobot + .assertDeletedMessage(sampleText) + .assertMessageAuthor(isCurrentUser = true) + } + } + + @AllureId("5672") + @Test + fun test_participantDeletesMessage() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN participant sends the message") { + participantRobot.sendMessage(sampleText) + } + step("AND participant deletes the message") { + participantRobot.deleteMessage() + } + step("THEN the message is deleted") { + userRobot + .assertDeletedMessage(sampleText) + .assertMessageAuthor(isCurrentUser = false) + } + } + + @AllureId("5813") + @Ignore("https://linear.app/stream/issue/AND-211") + @Test + fun test_userHardDeletesMessage() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user sends the message") { + userRobot.sendMessage(sampleText) + } + step("AND user hard-deletes the message") { + userRobot.deleteMessage(hard = true) + } + step("THEN the message is hard-deleted") { + userRobot.assertDeletedMessage(sampleText, hard = true) + } + } + + @AllureId("5814") + @Test + fun test_participantHardDeletesMessage() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN participant sends the message") { + participantRobot.sendMessage(sampleText) + userRobot.assertMessage(sampleText) + } + step("AND participant hard-deletes the message") { + participantRobot.deleteMessage(hard = true) + } + step("THEN the message is hard-deleted") { + userRobot.assertDeletedMessage(sampleText, hard = true) + } + } + + @AllureId("5726") + @Test + fun test_threadReplyIsRemovedEverywhere_whenParticipantRemovesItFromChannel() { + step("GIVEN user opens the channel") { + backendRobot.generateChannels(channelsCount = 1, messagesCount = 1) + userRobot.login().openChannel() + } + step("AND participant adds a thread reply and sends it also to main channel") { + participantRobot.sendMessageInThread(sampleText, alsoSendInChannel = true) + } + step("WHEN participant removes the thread reply") { + participantRobot.deleteMessage() + } + step("THEN user observes the thread reply removed in channel") { + userRobot + .assertDeletedMessage(sampleText) + .assertAlsoInTheChannelLabelInChannel() + } + step("AND user observes the thread reply removed in thread") { + userRobot + .openThread(messageCellIndex = 1) + .assertDeletedMessage(sampleText) + .assertAlsoInTheChannelLabelInThread() + } + } + + @AllureId("5728") + @Test + fun test_threadReplyIsRemovedEverywhere_whenUserRemovesItFromChannel() { + step("GIVEN user opens the channel") { + backendRobot.generateChannels(channelsCount = 1, messagesCount = 1) + userRobot.login().openChannel() + } + step("AND user adds a thread reply and sends it also to main channel") { + userRobot + .openThread() + .sendMessageInThread(sampleText, alsoSendInChannel = true) + } + step("WHEN user removes thread reply from channel") { + userRobot + .tapOnBackButton() + .deleteMessage() + } + step("THEN user observes the thread reply removed in channel") { + userRobot + .assertDeletedMessage(sampleText) + .assertAlsoInTheChannelLabelInChannel() + } + step("AND user observes the thread reply removed in thread") { + userRobot + .openThread(messageCellIndex = 1) + .assertDeletedMessage(sampleText) + .assertAlsoInTheChannelLabelInThread() + } + } + + @AllureId("5686") + @Test + fun test_participantRemovesThreadReply() { + step("GIVEN user opens the channel") { + backendRobot.generateChannels(channelsCount = 1, messagesCount = 1) + userRobot.login().openChannel() + } + step("AND participant adds a thread reply") { + participantRobot.sendMessageInThread(sampleText, alsoSendInChannel = false) + } + step("WHEN participant removes the thread reply") { + participantRobot.deleteMessage() + } + step("THEN user observes the thread reply removed in thread") { + userRobot + .assertThreadReplyLabelOnParentMessage() + .openThread() + .assertDeletedMessage(sampleText) + } + } + + @AllureId("5729") + @Test + fun test_threadReplyIsRemovedEverywhere_whenUserRemovesItFromThread() { + step("GIVEN user opens the channel") { + backendRobot.generateChannels(channelsCount = 1, messagesCount = 1) + userRobot.login().openChannel() + } + step("AND user adds a thread reply and sends it also to main channel") { + userRobot + .openThread() + .sendMessageInThread(sampleText, alsoSendInChannel = true) + } + step("WHEN user removes thread reply from thread") { + userRobot.deleteMessage() + } + step("THEN user observes the thread reply removed in thread") { + userRobot + .assertDeletedMessage(sampleText) + .assertAlsoInTheChannelLabelInThread() + } + step("AND user observes the thread reply removed in channel") { + userRobot + .tapOnBackButton() + .assertDeletedMessage(sampleText) + .assertAlsoInTheChannelLabelInChannel() + } + } + + @AllureId("5686") + @Test + fun test_userRemovesThreadReply() { + step("GIVEN user opens the channel") { + backendRobot.generateChannels(channelsCount = 1, messagesCount = 1) + userRobot.login().openChannel() + } + step("AND user adds a thread reply") { + userRobot + .openThread() + .sendMessageInThread(sampleText, alsoSendInChannel = false) + } + step("WHEN user removes the thread reply") { + userRobot.deleteMessage() + } + step("THEN user observes the thread reply removed in thread") { + userRobot.assertDeletedMessage(sampleText) + } + step("AND user observes a thread reply count button in channel") { + userRobot + .tapOnBackButton() + .assertThreadReplyLabelOnParentMessage() + } + } + + // MARK: - Message grouping + + @AllureId("5803") + @Test + fun test_messageEndsGroup_whenFollowedByErrorMessage() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND user sends the 1st message") { + userRobot.sendMessage(sampleText) + } + step("AND the timestamp is shown under the 1st message") { + userRobot.assertMessageTimestamps(1) + } + step("WHEN user sends a message that does not pass moderation") { + userRobot.sendMessage(forbiddenWord) + } + step("THEN messages are not grouped, 1st message shows the timestamp") { + userRobot.assertMessageTimestamps(1) + } + } + + @AllureId("5830") + @Test + fun test_messageRendersTimestampAgain_whenMessageLastInGroupIsHardDeleted() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND participant inserts 3 group messages") { + participantRobot + .sendMessage("1") + .sendMessage("2") + .sendMessage("3") + userRobot.assertMessageTimestamps(1) + } + step("WHEN participant hard deletes last message") { + participantRobot.deleteMessage(hard = true) + } + step("THEN previous message should re-render timestamp") { + userRobot.assertMessageTimestamps(1) + } + } + + @AllureId("6608") + @Ignore("https://linear.app/stream/issue/AND-212") + @Test + fun test_messageRendersTimestampAgain_whenMessageLastInGroupIsSoftDeleted() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND participant inserts 3 group messages") { + participantRobot + .sendMessage("1") + .sendMessage("2") + .sendMessage("3") + userRobot.assertMessageTimestamps(1) + } + step("WHEN participant hard deletes last message") { + participantRobot.deleteMessage(hard = false) + } + step("THEN previous message should re-render timestamp") { + userRobot.assertMessageTimestamps(1) } } } 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 a9a9b12486a..0d96e4b66d6 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 @@ -25,6 +25,7 @@ import io.getstream.chat.android.compose.uiautomator.device import io.getstream.chat.android.compose.uiautomator.grantPermission import io.getstream.chat.android.compose.uiautomator.mockServer import io.getstream.chat.android.compose.uiautomator.startApp +import io.getstream.chat.android.e2e.test.robots.BackendRobot import io.getstream.chat.android.e2e.test.robots.ParticipantRobot import io.getstream.chat.android.e2e.test.rules.RetryRule import io.qameta.allure.android.rules.LogcatRule @@ -38,6 +39,7 @@ import org.junit.rules.TestName open class StreamTestCase { val userRobot = UserRobot() + val backendRobot = BackendRobot() val participantRobot = ParticipantRobot() @get:Rule diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/factory/MessageContentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/factory/MessageContentFactory.kt index 3965ea9203b..037d2c8893f 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/factory/MessageContentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/factory/MessageContentFactory.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.ui.components.messages.DefaultMessageDeletedContent import io.getstream.chat.android.compose.ui.components.messages.DefaultMessageGiphyContent @@ -59,7 +60,7 @@ public open class MessageContentFactory { public open fun MessageDeletedContent( modifier: Modifier, ) { - DefaultMessageDeletedContent(modifier = modifier) + DefaultMessageDeletedContent(modifier = modifier.testTag("Stream_MessageDeleted")) } /** diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/suggestions/SuggestionList.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/suggestions/SuggestionList.kt index 2c08a0e886e..ed04b0a8019 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/suggestions/SuggestionList.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/suggestions/SuggestionList.kt @@ -22,8 +22,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.window.Popup import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.util.AboveAnchorPopupPositionProvider @@ -37,6 +40,7 @@ import io.getstream.chat.android.compose.ui.util.AboveAnchorPopupPositionProvide * @param headerContent The content shown at the top of a suggestion list popup. * @param centerContent The content shown inside the suggestion list popup. */ +@OptIn(ExperimentalComposeUiApi::class) @Composable public fun SuggestionList( modifier: Modifier = Modifier, @@ -47,7 +51,7 @@ public fun SuggestionList( ) { Popup(popupPositionProvider = AboveAnchorPopupPositionProvider()) { Card( - modifier = modifier, + modifier = modifier.semantics { testTagsAsResourceId = true }, elevation = CardDefaults.cardElevation(defaultElevation = ChatTheme.dimens.suggestionListElevation), shape = shape, colors = CardDefaults.cardColors(containerColor = ChatTheme.colors.barsBackground), diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt index 9457b933fb4..bb99807ba0c 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt @@ -632,7 +632,8 @@ public fun RegularMessageContent( Icon( modifier = Modifier .size(24.dp) - .align(BottomEnd), + .align(BottomEnd) + .testTag("Stream_MessageFailedIcon"), painter = painterResource(id = R.drawable.stream_compose_ic_error), contentDescription = null, tint = ChatTheme.colors.errorAccent, diff --git a/stream-chat-android-e2e-test/api/stream-chat-android-e2e-test.api b/stream-chat-android-e2e-test/api/stream-chat-android-e2e-test.api index d4dbb5875d3..aed782a0bfd 100644 --- a/stream-chat-android-e2e-test/api/stream-chat-android-e2e-test.api +++ b/stream-chat-android-e2e-test/api/stream-chat-android-e2e-test.api @@ -1,4 +1,8 @@ public final class io/getstream/chat/android/compose/uiautomator/ActionsKt { + public static final fun disableInternetConnection (Landroidx/test/uiautomator/UiDevice;)V + public static final fun enableInternetConnection (Landroidx/test/uiautomator/UiDevice;)V + public static final fun goToBackground (Landroidx/test/uiautomator/UiDevice;)V + public static final fun goToForeground (Landroidx/test/uiautomator/UiDevice;)V public static final fun longPress (Landroidx/test/uiautomator/UiObject2;I)V public static synthetic fun longPress$default (Landroidx/test/uiautomator/UiObject2;IILjava/lang/Object;)V public static final fun startApp (Landroidx/test/uiautomator/UiDevice;)V @@ -7,6 +11,7 @@ public final class io/getstream/chat/android/compose/uiautomator/ActionsKt { public static synthetic fun swipeDown$default (Landroidx/test/uiautomator/UiDevice;IIILjava/lang/Object;)V public static final fun swipeUp (Landroidx/test/uiautomator/UiDevice;II)V public static synthetic fun swipeUp$default (Landroidx/test/uiautomator/UiDevice;IIILjava/lang/Object;)V + public static final fun tapOnScreenCenter (Landroidx/test/uiautomator/UiDevice;)V public static final fun typeText (Landroidx/test/uiautomator/UiObject2;Ljava/lang/String;)Landroidx/test/uiautomator/UiObject2; } @@ -25,9 +30,9 @@ public final class io/getstream/chat/android/compose/uiautomator/BaseKt { } public final class io/getstream/chat/android/compose/uiautomator/ElementKt { - public static final fun exists (Landroidx/test/uiautomator/BySelector;)Z public static final fun isChecked (Landroidx/test/uiautomator/BySelector;)Z public static final fun isDisplayed (Landroidx/test/uiautomator/BySelector;)Z + public static final fun isDisplayed (Landroidx/test/uiautomator/UiObject2;)Z public static final fun isEnabled (Landroidx/test/uiautomator/BySelector;)Z public static final fun scrollDownUntilDisplayed (Landroidx/test/uiautomator/BySelector;I)Landroidx/test/uiautomator/UiObject2; public static synthetic fun scrollDownUntilDisplayed$default (Landroidx/test/uiautomator/BySelector;IILjava/lang/Object;)Landroidx/test/uiautomator/UiObject2; @@ -45,7 +50,9 @@ public final class io/getstream/chat/android/compose/uiautomator/FindByKt { public final class io/getstream/chat/android/compose/uiautomator/MathKt { public static final fun bottomPoint (Landroid/graphics/Rect;)Landroid/graphics/Point; + public static final fun getHeight (Landroidx/test/uiautomator/UiObject2;)I public static final fun getSeconds (I)J + public static final fun getWidth (Landroidx/test/uiautomator/UiObject2;)I public static final fun leftPoint (Landroid/graphics/Rect;)Landroid/graphics/Point; public static final fun toSeconds (J)I } @@ -60,6 +67,8 @@ public final class io/getstream/chat/android/compose/uiautomator/WaitKt { public static synthetic fun sleep$default (JILjava/lang/Object;)V public static final fun wait (Landroidx/test/uiautomator/BySelector;J)Landroidx/test/uiautomator/BySelector; public static synthetic fun wait$default (Landroidx/test/uiautomator/BySelector;JILjava/lang/Object;)Landroidx/test/uiautomator/BySelector; + public static final fun waitForText (Landroidx/test/uiautomator/UiObject2;Ljava/lang/String;ZJ)Landroidx/test/uiautomator/UiObject2; + public static synthetic fun waitForText$default (Landroidx/test/uiautomator/UiObject2;Ljava/lang/String;ZJILjava/lang/Object;)Landroidx/test/uiautomator/UiObject2; public static final fun waitToAppear (Landroidx/test/uiautomator/BySelector;IJ)Landroidx/test/uiautomator/UiObject2; public static final fun waitToAppear (Landroidx/test/uiautomator/BySelector;J)Landroidx/test/uiautomator/UiObject2; public static synthetic fun waitToAppear$default (Landroidx/test/uiautomator/BySelector;IJILjava/lang/Object;)Landroidx/test/uiautomator/UiObject2; @@ -83,6 +92,15 @@ public final class io/getstream/chat/android/e2e/test/mockserver/AttachmentType public static fun values ()[Lio/getstream/chat/android/e2e/test/mockserver/AttachmentType; } +public final class io/getstream/chat/android/e2e/test/mockserver/MessageReadStatus : java/lang/Enum { + public static final field PENDING Lio/getstream/chat/android/e2e/test/mockserver/MessageReadStatus; + public static final field READ Lio/getstream/chat/android/e2e/test/mockserver/MessageReadStatus; + public static final field SENT Lio/getstream/chat/android/e2e/test/mockserver/MessageReadStatus; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/getstream/chat/android/e2e/test/mockserver/MessageReadStatus; + public static fun values ()[Lio/getstream/chat/android/e2e/test/mockserver/MessageReadStatus; +} + public final class io/getstream/chat/android/e2e/test/mockserver/MockServer { public fun ()V public final fun getRequest (Ljava/lang/String;)Lokhttp3/ResponseBody; @@ -93,6 +111,7 @@ public final class io/getstream/chat/android/e2e/test/mockserver/MockServer { } public final class io/getstream/chat/android/e2e/test/mockserver/MockServerKt { + public static final field forbiddenWord Ljava/lang/String; public static final fun getMockServerUrl ()Ljava/lang/String; public static final fun setMockServerUrl (Ljava/lang/String;)V } @@ -109,7 +128,15 @@ public final class io/getstream/chat/android/e2e/test/mockserver/ReactionType : public static fun values ()[Lio/getstream/chat/android/e2e/test/mockserver/ReactionType; } +public final class io/getstream/chat/android/e2e/test/robots/BackendRobot { + public fun ()V + public final fun generateChannels (III)Lio/getstream/chat/android/e2e/test/robots/BackendRobot; + public static synthetic fun generateChannels$default (Lio/getstream/chat/android/e2e/test/robots/BackendRobot;IIIILjava/lang/Object;)Lio/getstream/chat/android/e2e/test/robots/BackendRobot; +} + public final class io/getstream/chat/android/e2e/test/robots/ParticipantRobot { + public static final field Companion Lio/getstream/chat/android/e2e/test/robots/ParticipantRobot$Companion; + public static final field name Ljava/lang/String; public fun ()V public final fun addReaction (Lio/getstream/chat/android/e2e/test/mockserver/ReactionType;)Lio/getstream/chat/android/e2e/test/robots/ParticipantRobot; public final fun deleteMessage (Z)Lio/getstream/chat/android/e2e/test/robots/ParticipantRobot; @@ -147,6 +174,9 @@ public final class io/getstream/chat/android/e2e/test/robots/ParticipantRobot { public static synthetic fun uploadAttachmentInThread$default (Lio/getstream/chat/android/e2e/test/robots/ParticipantRobot;Lio/getstream/chat/android/e2e/test/mockserver/AttachmentType;IZILjava/lang/Object;)Lio/getstream/chat/android/e2e/test/robots/ParticipantRobot; } +public final class io/getstream/chat/android/e2e/test/robots/ParticipantRobot$Companion { +} + public abstract interface annotation class io/getstream/chat/android/e2e/test/rules/Retry : java/lang/annotation/Annotation { public abstract fun times ()I } diff --git a/stream-chat-android-e2e-test/src/main/AndroidManifest.xml b/stream-chat-android-e2e-test/src/main/AndroidManifest.xml index 63aa001bcdd..21d822b2a40 100644 --- a/stream-chat-android-e2e-test/src/main/AndroidManifest.xml +++ b/stream-chat-android-e2e-test/src/main/AndroidManifest.xml @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - diff --git a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/mockserver/DataTypes.kt b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/mockserver/DataTypes.kt index 3b352b86dd9..64b789c3e23 100644 --- a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/mockserver/DataTypes.kt +++ b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/mockserver/DataTypes.kt @@ -29,3 +29,9 @@ public enum class ReactionType(public val reaction: String) { SAD("sad"), LIKE("like"), } + +public enum class MessageReadStatus { + READ, + PENDING, + SENT, +} diff --git a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/mockserver/MockServer.kt b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/mockserver/MockServer.kt index 6d23f7c780b..d41ea82b5ca 100644 --- a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/mockserver/MockServer.kt +++ b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/mockserver/MockServer.kt @@ -26,6 +26,7 @@ import okhttp3.ResponseBody public var mockServerUrl: String? = null private const val driverUrl: String = "http://10.0.2.2:4567" private val okHttp: OkHttpClient = OkHttpClient() +public const val forbiddenWord: String = "wth" public class MockServer { 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 new file mode 100644 index 00000000000..6af82aae21e --- /dev/null +++ b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/robots/BackendRobot.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2014-2025 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.e2e.test.robots + +import io.getstream.chat.android.compose.uiautomator.mockServer +import io.getstream.chat.android.compose.uiautomator.sleep + +public class BackendRobot { + + public fun generateChannels( + channelsCount: Int, + messagesCount: Int = 0, + repliesCount: Int = 0, + ): BackendRobot { + sleep(2000) + mockServer.postRequest("mock?channels=$channelsCount&messages=$messagesCount&replies=$repliesCount") + return this + } +} diff --git a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/robots/ParticipantRobot.kt b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/robots/ParticipantRobot.kt index 7b965229662..b82176bfc7a 100644 --- a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/robots/ParticipantRobot.kt +++ b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/robots/ParticipantRobot.kt @@ -25,6 +25,10 @@ import okhttp3.RequestBody.Companion.toRequestBody public class ParticipantRobot { + public companion object { + public const val name: String = "Han Solo" + } + public fun startTyping(): ParticipantRobot { mockServer.postRequest("participant/typing/start") return this @@ -56,80 +60,83 @@ public class ParticipantRobot { } public fun sendMessage(text: String): ParticipantRobot { - mockServer.postRequest("/participant/message", text.toRequestBody("text".toMediaTypeOrNull())) + mockServer.postRequest("participant/message", text.toRequestBody("text".toMediaTypeOrNull())) return this } public fun sendMessageInThread(text: String, alsoSendInChannel: Boolean = false): ParticipantRobot { mockServer.postRequest( - "/participant/message?thread=true&thread_and_channel=$alsoSendInChannel", + "participant/message?thread=true&thread_and_channel=$alsoSendInChannel", text.toRequestBody("text".toMediaTypeOrNull()), ) return this } public fun editMessage(text: String): ParticipantRobot { - mockServer.postRequest("/participant/message?action=edit") + mockServer.postRequest( + "participant/message?action=edit", + text.toRequestBody("text".toMediaTypeOrNull()), + ) return this } public fun deleteMessage(hard: Boolean = false): ParticipantRobot { - mockServer.postRequest("/participant/message?action=delete&hard_delete=$hard") + mockServer.postRequest("participant/message?action=delete&hard_delete=$hard") return this } public fun quoteMessage(text: String): ParticipantRobot { - val endpoint = "/participant/message?quote=true" + val endpoint = "participant/message?quote=true" mockServer.postRequest(endpoint, text.toRequestBody("text".toMediaTypeOrNull())) return this } public fun quoteMessageInThread(text: String, alsoSendInChannel: Boolean = false): ParticipantRobot { mockServer.postRequest( - "/participant/message?quote=true&thread=true&thread_and_channel=$alsoSendInChannel", + "participant/message?quote=true&thread=true&thread_and_channel=$alsoSendInChannel", text.toRequestBody("text".toMediaTypeOrNull()), ) return this } public fun sendGiphy(): ParticipantRobot { - mockServer.postRequest("/participant/message?giphy=true") + mockServer.postRequest("participant/message?giphy=true") return this } public fun sendGiphyInThread(): ParticipantRobot { - mockServer.postRequest("/participant/message?giphy=true&thread=true") + mockServer.postRequest("participant/message?giphy=true&thread=true") return this } public fun quoteMessageWithGiphy(): ParticipantRobot { - mockServer.postRequest("/participant/message?giphy=true"e=true") + mockServer.postRequest("participant/message?giphy=true"e=true") return this } public fun quoteMessageWithGiphyInThread(alsoSendInChannel: Boolean = false): ParticipantRobot { - val endpoint = "/participant/message?giphy=true"e=true&thread=true&thread_and_channel=$alsoSendInChannel" + val endpoint = "participant/message?giphy=true"e=true&thread=true&thread_and_channel=$alsoSendInChannel" mockServer.postRequest(endpoint) return this } public fun pinMesage(): ParticipantRobot { - mockServer.postRequest("/participant/message?action=pin") + mockServer.postRequest("participant/message?action=pin") return this } public fun unpinMesage(): ParticipantRobot { - mockServer.postRequest("/participant/message?action=unpin") + mockServer.postRequest("participant/message?action=unpin") return this } public fun uploadAttachment(type: AttachmentType, count: Int = 1): ParticipantRobot { - mockServer.postRequest("/participant/message?$type=$count") + mockServer.postRequest("participant/message?$type=$count") return this } public fun quoteMessageWithAttachment(type: AttachmentType, count: Int = 1): ParticipantRobot { - mockServer.postRequest("/participant/message?quote=true&$type=$count") + mockServer.postRequest("participant/message?quote=true&$type=$count") return this } @@ -138,7 +145,7 @@ public class ParticipantRobot { count: Int = 1, alsoSendInChannel: Boolean = false, ): ParticipantRobot { - val endpoint = "/participant/message?$type=$count&thread=true&thread_and_channel=$alsoSendInChannel" + val endpoint = "participant/message?$type=$count&thread=true&thread_and_channel=$alsoSendInChannel" mockServer.postRequest(endpoint) return this } @@ -148,18 +155,18 @@ public class ParticipantRobot { count: Int = 1, alsoSendInChannel: Boolean = false, ): ParticipantRobot { - val endpoint = "/participant/message?quote=true&$type=$count&thread=true&thread_and_channel=$alsoSendInChannel" + val endpoint = "participant/message?quote=true&$type=$count&thread=true&thread_and_channel=$alsoSendInChannel" mockServer.postRequest(endpoint) return this } public fun addReaction(type: ReactionType): ParticipantRobot { - mockServer.postRequest("/participant/reaction?type=${type.reaction}") + mockServer.postRequest("participant/reaction?type=${type.reaction}") return this } public fun deleteReaction(type: String): ParticipantRobot { - mockServer.postRequest("/participant/reaction?type=$type&delete=true") + mockServer.postRequest("participant/reaction?type=$type&delete=true") return this } } diff --git a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Actions.kt b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Actions.kt index abf4ed92682..4aed111f13b 100644 --- a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Actions.kt +++ b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Actions.kt @@ -25,7 +25,6 @@ public fun UiDevice.startApp() { val intent = testContext.packageManager.getLaunchIntentForPackage(packageName) intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) intent?.putExtra("BASE_URL", mockServerUrl) - val a = mockServerUrl testContext.startActivity(intent) } @@ -71,3 +70,28 @@ public fun UiDevice.swipeUp(steps: Int = 10, times: Int = 1) { ) } } + +public fun UiDevice.tapOnScreenCenter() { + device.click(device.displayWidth / 2, device.displayHeight / 2) +} + +public fun UiDevice.goToBackground() { + device.pressHome() + sleep(1000) +} + +public fun UiDevice.goToForeground() { + device.pressRecentApps() + sleep(500) + device.tapOnScreenCenter() +} + +public fun UiDevice.enableInternetConnection() { + executeShellCommand("svc data enable") + executeShellCommand("svc wifi enable") +} + +public fun UiDevice.disableInternetConnection() { + executeShellCommand("svc data disable") + executeShellCommand("svc wifi disable") +} diff --git a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Element.kt b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Element.kt index 0e8ef4f8c0e..5303ebfa581 100644 --- a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Element.kt +++ b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Element.kt @@ -19,7 +19,11 @@ package io.getstream.chat.android.compose.uiautomator import androidx.test.uiautomator.BySelector import androidx.test.uiautomator.UiObject2 -public fun BySelector.exists(): Boolean { +public fun UiObject2.isDisplayed(): Boolean { + return this.isFocusable +} + +public fun BySelector.isDisplayed(): Boolean { return this.findObjects().isNotEmpty() } @@ -31,10 +35,6 @@ public fun BySelector.isChecked(): Boolean { return this.findObject().isChecked } -public fun BySelector.isDisplayed(): Boolean { - return this.findObjects().isNotEmpty() -} - public fun BySelector.scrollUpUntilDisplayed(scrolls: Int = 5): UiObject2 { var counter = scrolls while (!this.isDisplayed() && counter > 0) { diff --git a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Math.kt b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Math.kt index fb066bb9f59..25697769373 100644 --- a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Math.kt +++ b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Math.kt @@ -18,6 +18,7 @@ package io.getstream.chat.android.compose.uiautomator import android.graphics.Point import android.graphics.Rect +import androidx.test.uiautomator.UiObject2 public fun Rect.bottomPoint(): Point { val x = right - ((right - left) / 2) @@ -34,3 +35,7 @@ public fun Rect.leftPoint(): Point { public fun Long.toSeconds(): Int = (this / 1000).toInt() public val Int.seconds: Long get() = (this * 1000).toLong() + +public val UiObject2.height: Int get() = visibleBounds.height() + +public val UiObject2.width: Int get() = visibleBounds.width() diff --git a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Wait.kt b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Wait.kt index 260a3ccc7c4..61d7a66f4bc 100644 --- a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Wait.kt +++ b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Wait.kt @@ -43,3 +43,16 @@ public fun BySelector.waitToDisappear(timeOutMillis: Long = defaultTimeout): ByS device.wait(Until.gone(this), timeOutMillis) return this } + +public fun UiObject2.waitForText( + expectedText: String, + mustBeEqual: Boolean = true, + timeOutMillis: Long = defaultTimeout, +): UiObject2 { + val endTime = System.currentTimeMillis() + timeOutMillis + var textPresent = false + while (!textPresent && System.currentTimeMillis() < endTime) { + textPresent = if (mustBeEqual) text == expectedText else text.contains(expectedText) + } + return this +}