diff --git a/fastlane/Fastfile b/fastlane/Fastfile index bc3cad6c907..35c21fb144f 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..fed10fd5734 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") @@ -145,6 +147,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") 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..5b7e4a2b626 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 @@ -23,14 +23,17 @@ 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 import io.getstream.chat.android.compose.uiautomator.findObject import io.getstream.chat.android.compose.uiautomator.findObjects +import io.getstream.chat.android.compose.uiautomator.isDisplayed 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 @@ -87,17 +90,23 @@ class UserRobot { fun deleteMessage(messageCellIndex: Int = 0, hard: Boolean = false): UserRobot { openContextMenu(messageCellIndex) - Message.ContextMenu.delete.waitToAppear().click() + ContextMenu.delete.waitToAppear().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 +130,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 +138,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() } @@ -176,22 +185,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 +215,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) { @@ -273,4 +287,12 @@ class UserRobot { } return this } + + fun tapOnMessageList() { + if (MessageList.messageList.isDisplayed()) { + MessageList.messageList.waitToAppear().click() + } else { + 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..214f8df9937 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,159 @@ 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.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.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.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 composerHeight: Int + if (isChangeable) { + composerHeight = composer.findObject().height + val text = "1\n2\n3" + typeText(text) + } else { + val text = "1\n2\n3\n4\n5" + typeText(text) + composerHeight = composer.findObject().height + typeText("${text}\n6") + } + val updatedComposerHeight = composer.findObject().height + assertEquals(composerHeight, updatedComposerHeight) + 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.assertScrollToBottomButton(isDisplayed: Boolean): UserRobot { + if (isDisplayed) { + assertTrue(MessageListPage.MessageList.scrollToBottomButton.waitToAppear().isDisplayed()) + } else { + assertFalse(MessageListPage.MessageList.scrollToBottomButton.waitToDisappear().isDisplayed()) + } + 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..79a034b6708 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,10 +16,27 @@ package io.getstream.chat.android.compose.tests +import io.getstream.chat.android.compose.robots.assertAttachmentsMenu +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.assertEditedMessage 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.assertScrollToBottomButton +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.qameta.allure.kotlin.Allure.step import io.qameta.allure.kotlin.AllureId +import org.junit.Ignore import org.junit.Test class MessageListTests : StreamTestCase() { @@ -53,12 +70,888 @@ 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("") + @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("") + @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) + } + } + + @AllureId("") + @Test + fun test_userEditsMessage() { + val message = "test message" + val editedMessage = "hello" + + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user sends the message: $message") { + userRobot.sendMessage(message) + } + step("AND user edits the message: $editedMessage") { + userRobot.editMessage(editedMessage) + } + step("THEN the message is edited") { + userRobot + .assertEditedMessage(editedMessage) + .assertMessageReadStatus(MessageReadStatus.SENT) + } + } + + @AllureId("") + @Test + fun test_participantEditsMessage() { + val message = "test message" + val editedMessage = "hello" + + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN participant sends the message: $message") { + participantRobot.sendMessage(message) + userRobot.assertMessage(message) + } + step("AND participant edits the message: $editedMessage") { + participantRobot.editMessage(editedMessage) + } + step("THEN the message is edited") { + userRobot.assertEditedMessage(editedMessage) + } + } + + @AllureId("") + @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("") + @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) + } + } + + @AllureId("") + @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("") + @Test + fun test_composerSizeLimit() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("THEN user verifies that composer size changes") { + userRobot.assertComposerSize(isChangeable = false) + } + } + + @AllureId("") + @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("") + @Test + fun test_attachmentsMenuCloses_whenUserTapsOnMessageList() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("AND user opens attachments menu") { + userRobot + // .openAttachmentsMenu() + .assertAttachmentsMenu(isDisplayed = true) + } + step("WHEN user taps on message list") { + userRobot.tapOnMessageList() + } + step("THEN command suggestions disappear") { + userRobot.assertAttachmentsMenu(isDisplayed = false) + } + } + + @Ignore("https://linear.app/stream/issue/AND-181") + @AllureId("") + @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) } } + + @AllureId("") + @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("") + @Test + fun test_addMessageWhileOffline() { + 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("") + @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) + } + } + + @Ignore("https://linear.app/stream/issue/AND-76") + @AllureId("") + @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("") + @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("") + @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) + } + } + @AllureId("") + @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) + } + } + +// +// func test_userFillsTheComposerMentioningParticipantThroughMentionsView() { +// linkToScenario(withId: 62) +// +// GIVEN("user opens the channel") { +// userRobot.login().openChannel() +// } +// WHEN("user taps on participants name") { +// userRobot.mentionParticipant() +// } +// THEN("composer fills in participants name") { +// userRobot.assertMentionWasApplied() +// } +// } +// } +// +// // MARK: Links preview +// +// extension MessageList_Tests { +// +// func test_addMessageWithLinkToUnsplash() { +// linkToScenario(withId: 59) +// +// let message = "https://unsplash.com/photos/1_2d3MRbI9c" +// +// GIVEN("user opens the channel") { +// userRobot.login().openChannel() +// } +// WHEN("user sends a message with YouTube link") { +// userRobot +// .sendMessage(message) +// .scrollMessageListDown() // to hide the keyboard +// } +// THEN("user observes a preview of the image with description") { +// userRobot.assertLinkPreview() +// } +// } +// +// func test_addMessageWithLinkToYoutube() { +// linkToScenario(withId: 60) +// +// let message = "https://youtube.com/watch?v=xOX7MsrbaPY" +// +// GIVEN("user opens the channel") { +// userRobot.login().openChannel() +// } +// WHEN("user sends a message with YouTube link") { +// userRobot +// .sendMessage(message) +// .scrollMessageListDown() // to hide the keyboard +// } +// THEN("user observes a preview of the video with description") { +// userRobot.assertLinkPreview() +// } +// } +// +// func test_participantAddsMessageWithLinkToUnsplash() { +// linkToScenario(withId: 280) +// +// let message = "https://unsplash.com/photos/1_2d3MRbI9c" +// +// GIVEN("user opens the channel") { +// userRobot.login().openChannel() +// } +// WHEN("participant sends a message with Unsplash link") { +// participantRobot.sendMessage(message) +// userRobot.scrollMessageListDown() // to hide the keyboard +// } +// THEN("user observes a preview of the image with description") { +// userRobot.assertLinkPreview() +// } +// } +// +// func test_participantAddsMessageWithLinkToYoutube() { +// linkToScenario(withId: 281) +// +// let message = "https://youtube.com/watch?v=xOX7MsrbaPY" +// +// GIVEN("user opens the channel") { +// userRobot.login().openChannel() +// } +// WHEN("participant sends a message with YouTube link") { +// participantRobot.sendMessage(message) +// userRobot.scrollMessageListDown() // to hide the keyboard +// } +// THEN("user observes a preview of the video with description") { +// userRobot.assertLinkPreview() +// } +// } +// +// func test_messageWithLinkOpensSafari() { +// linkToScenario(withId: 3119) +// +// let message = "Some link: https://youtube.com" +// +// GIVEN("user opens the channel") { +// userRobot.login().openChannel() +// } +// WHEN("user sends a message with YouTube link") { +// userRobot +// .sendMessage(message) +// .scrollMessageListDown() // to hide the keyboard +// } +// THEN("user observes safari opening") { +// userRobot.assertLinkOpensSafari() +// } +// } +// +// func test_messageWithLinkOpensSafari_whenNoHttpScheme() { +// linkToScenario(withId: 3120) +// +// let message = "Some link: youtube.com" +// +// GIVEN("user opens the channel") { +// userRobot.login().openChannel() +// } +// WHEN("user sends a message with YouTube link without https://") { +// userRobot +// .sendMessage(message) +// .scrollMessageListDown() // to hide the keyboard +// } +// THEN("user observes safari opening") { +// userRobot.assertLinkOpensSafari() +// } +// } +// } +// +// // MARK: - Thread replies +// extension MessageList_Tests { +// func test_threadReplyAppearsInThread_whenParticipantAddsThreadReply() { +// linkToScenario(withId: 50) +// +// let threadReply = "thread reply" +// +// GIVEN("user opens the channel") { +// backendRobot.generateChannels(count: 1, messagesCount: 1) +// userRobot.login().openChannel() +// } +// WHEN("participant adds a thread reply") { +// participantRobot.replyToMessageInThread(threadReply) +// } +// AND("user enters thread") { +// userRobot.openThread() +// } +// THEN("user observes the thread reply in thread") { +// userRobot.assertThreadReply(threadReply) +// } +// } +// +// func test_threadReplyAppearsInChannelAndThread_whenParticipantAddsThreadReplySentAlsoToChannel() { +// linkToScenario(withId: 110) +// +// let threadReply = "thread reply" +// +// GIVEN("user opens the channel") { +// backendRobot.generateChannels(count: 1, messagesCount: 1) +// userRobot.login().openChannel() +// } +// WHEN("participant adds a thread reply") { +// participantRobot.replyToMessageInThread(threadReply, alsoSendInChannel: true) +// } +// THEN("user observes the thread reply in channel") { +// userRobot.assertMessage(threadReply) +// } +// WHEN("user enters thread") { +// userRobot.openThread() +// } +// THEN("user observes the thread reply in thread") { +// userRobot.assertThreadReply(threadReply) +// } +// } +// +// func test_threadReplyAppearsInChannelAndThread_whenUserAddsThreadReplySentAlsoToChannel() { +// linkToScenario(withId: 111) +// +// let threadReply = "thread reply" +// +// GIVEN("user opens the channel") { +// backendRobot.generateChannels(count: 1, messagesCount: 1) +// userRobot.login().openChannel() +// } +// WHEN("user adds a thread reply and sends it also to main channel") { +// userRobot.replyToMessageInThread(threadReply, alsoSendInChannel: true) +// } +// THEN("user observes the thread reply in thread") { +// userRobot.assertThreadReply(threadReply) +// } +// AND("user observes the thread reply in channel") { +// userRobot +// .tapOnBackButton() +// .assertMessage(threadReply) +// } +// } +// +// func test_threadTypingIndicatorHidden_whenParticipantStopsTyping() { +// linkToScenario(withId: 243) +// +// GIVEN("user opens the channel") { +// backendRobot.generateChannels(count: 1, messagesCount: 1) +// userRobot.login().openChannel() +// } +// AND("user opens the thread") { +// userRobot.openThread() +// } +// WHEN("participant starts typing in thread") { +// participantRobot.wait(2).startTypingInThread() +// } +// THEN("user observes typing indicator is shown") { +// let typingUserName = UserDetails.userName(for: participantRobot.currentUserId) +// userRobot.assertTypingIndicatorShown(typingUserName: typingUserName) +// } +// WHEN("participant stops typing in thread") { +// participantRobot.wait(2).stopTypingInThread() +// } +// THEN("user observes typing indicator has disappeared") { +// userRobot.assertTypingIndicatorHidden() +// } +// } +// } +// +// // MARK: - Message grouping +// +// extension MessageList_Tests { +// func test_messageEndsGroup_whenFollowedByErrorMessage() { +// linkToScenario(withId: 218) +// +// let message = "Hey there" +// let messageWithForbiddenContent = server.forbiddenWords.first ?? "" +// +// GIVEN("user opens the channel") { +// userRobot +// .login() +// .openChannel() +// } +// AND("user sends the 1st message") { +// userRobot.sendMessage(message) +// } +// AND("the timestamp is shown under the 1st message") { +// userRobot.assertMessageHasTimestamp() +// } +// WHEN("user sends a message that does not pass moderation") { +// userRobot.sendMessage(messageWithForbiddenContent, waitForAppearance: false) +// } +// THEN("messages are not grouped, 1st message shows the timestamp") { +// userRobot.assertMessageHasTimestamp(at: 1) +// } +// } +// +// func test_messageEndsGroup_whenFollowedByEphemeralMessage() { +// linkToScenario(withId: 221) +// +// let message = "Hey there" +// +// GIVEN("user opens the channel") { +// userRobot +// .login() +// .openChannel() +// } +// AND("user sends the 1st message") { +// userRobot.sendMessage(message) +// } +// AND("the timestamp is shown under the 1st message") { +// userRobot.assertMessageHasTimestamp() +// } +// WHEN("user sends an ephemeral message") { +// userRobot +// .sendGiphy(send: false) +// .scrollMessageListDown() // to hide the keyboard +// } +// THEN("messages are not grouped, 1st message shows the timestamp") { +// userRobot +// .assertMessageCount(2) +// .assertMessageHasTimestamp(at: 1) +// } +// } +// +// func test_messageRendersTimestampAgain_whenMessageLastInGroupIsHardDeleted() { +// linkToScenario(withId: 288) +// +// GIVEN("user opens the channel") { +// backendRobot +// .generateChannels(count: 1, messagesCount: 1) +// userRobot +// .login() +// .openChannel() +// } +// AND("user inserts 3 group messages") { +// userRobot.sendMessage("Hey") +// userRobot.sendMessage("Hey2") +// userRobot.sendMessage("Hey3") +// userRobot.assertMessageHasTimestamp() +// } +// WHEN("user deletes last message") { +// userRobot.deleteMessage(hard: true) +// } +// THEN("previous message should re-render timestamp") { +// userRobot.assertMessageHasTimestamp(at: 0) +// } +// } +// } +// +// // MARK: Deleted messages +// +// extension MessageList_Tests { +// func test_deletesMessage() throws { +// linkToScenario(withId: 37) +// +// let message = "test message" +// +// GIVEN("user opens the channel") { +// userRobot.login().openChannel() +// } +// WHEN("user sends the message: '\(message)'") { +// userRobot.sendMessage(message) +// } +// AND("user deletes the message: '\(message)'") { +// userRobot.deleteMessage() +// } +// THEN("the message is deleted") { +// userRobot.assertDeletedMessage() +// } +// } +// +// func test_messageDeleted_whenParticipantDeletesMessage() throws { +// linkToScenario(withId: 38) +// +// let message = "test message" +// +// GIVEN("user opens the channel") { +// userRobot.login().openChannel() +// } +// WHEN("participant sends the message: '\(message)'") { +// participantRobot.sendMessage(message) +// } +// AND("participant deletes the message: '\(message)'") { +// participantRobot.deleteMessage() +// } +// THEN("the message is deleted") { +// userRobot.assertDeletedMessage() +// } +// } +// +// func test_threadReplyIsRemovedEverywhere_whenParticipantRemovesItFromChannel() { +// linkToScenario(withId: 112) +// +// let threadReply = "thread reply" +// +// GIVEN("user opens the channel") { +// backendRobot.generateChannels(count: 1, messagesCount: 1) +// userRobot.login().openChannel() +// } +// AND("participant adds a thread reply and sends it also to main channel") { +// participantRobot.replyToMessageInThread(threadReply, alsoSendInChannel: true) +// } +// WHEN("participant removes the thread reply from channel") { +// participantRobot.deleteMessage() +// } +// THEN("user observes the thread reply removed in channel") { +// userRobot.assertDeletedMessage() +// } +// AND("user observes the thread reply removed in thread") { +// userRobot +// .openThread(messageCellIndex: 1) +// .assertDeletedMessage() +// } +// } +// +// func test_threadReplyIsRemovedEverywhere_whenUserRemovesItFromChannel() throws { +// linkToScenario(withId: 114) +// +// let threadReply = "thread reply" +// +// GIVEN("user opens the channel") { +// backendRobot.generateChannels(count: 1, messagesCount: 1) +// userRobot.login().openChannel() +// } +// AND("user adds a thread reply and sends it also to main channel") { +// userRobot.replyToMessageInThread(threadReply, alsoSendInChannel: true) +// } +// WHEN("user removes thread reply from thread") { +// userRobot.deleteMessage() +// } +// THEN("user observes the thread reply removed in thread") { +// userRobot.assertDeletedMessage() +// } +// AND("user observes the thread reply removed in channel") { +// userRobot +// .tapOnBackButton() +// .assertDeletedMessage() +// } +// } +// +// func test_participantRemovesThreadReply() { +// linkToScenario(withId: 54) +// +// let threadReply = "thread reply" +// +// GIVEN("user opens the channel") { +// backendRobot.generateChannels(count: 1, messagesCount: 1) +// userRobot.login().openChannel() +// } +// AND("participant adds a thread reply") { +// participantRobot.replyToMessageInThread(threadReply, alsoSendInChannel: false) +// } +// WHEN("participant removes the thread reply") { +// participantRobot.deleteMessage() +// } +// THEN("user observes a thread reply count button in channel") { +// userRobot.assertThreadReplyCountButton() +// } +// THEN("user observes the thread reply removed in thread") { +// userRobot.openThread().assertDeletedMessage() +// } +// } +// +// func test_threadReplyIsRemovedEverywhere_whenUserRemovesItFromThread() { +// linkToScenario(withId: 115) +// +// let threadReply = "thread reply" +// +// GIVEN("user opens the channel") { +// backendRobot.generateChannels(count: 1, messagesCount: 1) +// userRobot.login().openChannel() +// } +// AND("user adds a thread reply and sends it also to main channel") { +// userRobot.replyToMessageInThread(threadReply, alsoSendInChannel: true) +// } +// WHEN("user goes back to channel and removes thread reply") { +// userRobot +// .tapOnBackButton() +// .deleteMessage() +// } +// THEN("user observes the thread reply removed in channel") { +// userRobot.assertDeletedMessage() +// } +// AND("user observes the thread reply removed in thread") { +// userRobot +// .openThread(messageCellIndex: 1) +// .assertDeletedMessage() +// } +// } +// +// func test_userRemovesThreadReply() throws { +// linkToScenario(withId: 53) +// +// let threadReply = "thread reply" +// +// GIVEN("user opens the channel") { +// backendRobot.generateChannels(count: 1, messagesCount: 1) +// userRobot.login().openChannel() +// } +// AND("user adds a thread reply") { +// userRobot.replyToMessageInThread(threadReply, alsoSendInChannel: false) +// } +// WHEN("user removes the thread reply") { +// userRobot.deleteMessage() +// } +// THEN("user observes the thread reply removed in thread") { +// userRobot.assertDeletedMessage() +// } +// AND("user observes a thread reply count button in channel") { +// userRobot +// .tapOnBackButton() +// .assertThreadReplyCountButton() +// } +// } +// +// func test_hardDeletesMessage() throws { +// linkToScenario(withId: 234) +// +// let message = "test message" +// +// GIVEN("user opens the channel") { +// backendRobot.generateChannels(count: 1, messagesCount: 1) +// userRobot.login().openChannel() +// } +// WHEN("user sends the message: '\(message)'") { +// userRobot.sendMessage(message) +// } +// AND("user hard-deletes the message: '\(message)'") { +// userRobot.deleteMessage(hard: true) +// } +// THEN("the message is hard-deleted") { +// userRobot.assertHardDeletedMessage(withText: message) +// } +// } +// +// func test_messageDeleted_whenParticipantHardDeletesMessage() throws { +// linkToScenario(withId: 235) +// +// let message = "test message" +// +// GIVEN("user opens the channel") { +// backendRobot.generateChannels(count: 1, messagesCount: 1) +// userRobot.login().openChannel() +// } +// WHEN("participant sends the message: '\(message)'") { +// participantRobot.sendMessage(message) +// } +// AND("the message is delivered") { +// userRobot.assertMessage(message) +// } +// AND("participant hard-deletes the message: '\(message)'") { +// participantRobot.wait(2).deleteMessage(hard: true) +// } +// THEN("the message is hard-deleted") { +// userRobot.assertHardDeletedMessage(withText: message) +// } +// } + } 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/messages/list/MessageItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt index 529977b0ec7..ff48ff7a1e1 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/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..eb678416f78 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 +} \ No newline at end of file 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..161bb22c245 --- /dev/null +++ b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/robots/BackendRobot.kt @@ -0,0 +1,21 @@ +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 + } +} \ No newline at end of file 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..0ace6b558b2 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 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..9931b98350b 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 @@ -17,15 +17,17 @@ package io.getstream.chat.android.compose.uiautomator import android.content.Intent +import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.UiSelector +import androidx.test.uiautomator.Until import io.getstream.chat.android.e2e.test.mockserver.mockServerUrl 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 +73,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..6ea1d75b798 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 +} \ No newline at end of file