diff --git a/.github/workflows/e2e-test-cron.yml b/.github/workflows/e2e-test-cron.yml index c7cd88e7409..c1d5731d626 100644 --- a/.github/workflows/e2e-test-cron.yml +++ b/.github/workflows/e2e-test-cron.yml @@ -82,6 +82,4 @@ jobs: if: failure() with: name: test_report - path: | - ./**/build/reports/androidTests/* - fastlane/stream-chat-test-mock-server/logs/* + path: fastlane/stream-chat-test-mock-server/logs/* diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 1800548590b..33a21506ef4 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -34,6 +34,7 @@ jobs: include: - batch: 0 - batch: 1 + - batch: 2 fail-fast: false env: ANDROID_API_LEVEL: 34 @@ -77,6 +78,4 @@ jobs: if: failure() with: name: test_report - path: | - ./**/build/reports/androidTests/* - fastlane/stream-chat-test-mock-server/logs/* + path: fastlane/stream-chat-test-mock-server/logs/* diff --git a/README.md b/README.md index 826f3542030..eba2d4b75d0 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ![stream-chat-android-client](https://img.shields.io/badge/stream--chat--android--client-3.16%20MB-lightgreen) ![stream-chat-android-offline](https://img.shields.io/badge/stream--chat--android--offline-3.37%20MB-lightgreen) ![stream-chat-android-ui-components](https://img.shields.io/badge/stream--chat--android--ui--components-7.86%20MB-lightgreen) -![stream-chat-android-compose](https://img.shields.io/badge/stream--chat--android--compose-8.74%20MB-lightgreen) +![stream-chat-android-compose](https://img.shields.io/badge/stream--chat--android--compose-8.75%20MB-lightgreen) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d62f1a5d424..bb748e9afde 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -60,7 +60,7 @@ end lane :build_and_run_e2e_test do |options| build_e2e_test - run_e2e_test(batch: options[:batch], batch_count: options[:batch_count]) + run_e2e_test(batch: options[:batch], batch_count: options[:batch_count], local_server: options[:local_server]) end lane :build_e2e_test do @@ -80,8 +80,9 @@ lane :run_e2e_test do |options| sh("rm -rf #{allure_results_path}") sh("adb shell rm -rf #{adb_test_results_path}/#{allure_results_path}") - start_mock_server + start_mock_server(local_server: options[:local_server]) install_test_services + upload_attachments stream_apk_folder_path = is_ci ? '..' : "../#{test_flavor}/build/outputs/apk" stream_app_path = "#{stream_apk_folder_path}/e2e/debug/stream-chat-android-compose-sample-e2e-debug.apk" @@ -113,6 +114,12 @@ lane :run_e2e_test do |options| UI.user_error!('Tests have failed!') if result.include?('Failures') end +lane :upload_attachments do + ['png', 'pdf'].each do |ext| + [1, 2].each { |i| sh("adb push attachments/file.#{ext} /sdcard/Download/file_#{i}.#{ext}") } + end +end + private_lane :batch_tests do |options| if options[:batch] && options[:batch_count] install(tool: :test_parser) diff --git a/fastlane/attachments/file.pdf b/fastlane/attachments/file.pdf new file mode 100644 index 00000000000..9eb1cfabc37 Binary files /dev/null and b/fastlane/attachments/file.pdf differ diff --git a/fastlane/attachments/file.png b/fastlane/attachments/file.png new file mode 100644 index 00000000000..74103e3f838 Binary files /dev/null and b/fastlane/attachments/file.png differ diff --git a/metrics/size.json b/metrics/size.json index b2fa9a45c73..4afb4822ca5 100644 --- a/metrics/size.json +++ b/metrics/size.json @@ -9,6 +9,6 @@ "stream-chat-android-client": 3244, "stream-chat-android-offline": 3456, "stream-chat-android-ui-components": 8052, - "stream-chat-android-compose": 8956 + "stream-chat-android-compose": 8960 } } 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 2306fb50ebc..eb2ff3c32d5 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 @@ -44,11 +44,16 @@ open class MessageListPage { companion object { val view = By.res("Stream_AttachmentsPicker") val sendButton = By.res("Stream_AttachmentPickerSendButton") - val imagesTab = By.res("Stream_AttachmentPickerImagesTab") val filesTab = By.res("Stream_AttachmentPickerFilesTab") val mediaCaptureTab = By.res("Stream_AttachmentPickerMediaCaptureTab") val pollsTab = By.res("Stream_AttachmentPickerPollsTab") - val sampleImage = By.res("Stream_AttachmentPickerSampleImage") + val findFilesButton = By.res("Stream_FindFilesButton") + val rootsButton = By.descContains("Show roots") + val downloadsView = By.text("Downloads") + val image1 = By.text("file_1.png") + val image2 = By.text("file_2.png") + val pdf1 = By.text("file_1.pdf") + val pdf2 = By.text("file_2.pdf") } } @@ -67,6 +72,16 @@ open class MessageListPage { val quotedMessage = By.res("Stream_QuotedMessage") val quotedMessageAvatar = By.res("Stream_QuotedMessageAuthorAvatar") val cancelButton = By.res("Stream_ComposerCancelButton") + val attachmentCancelIcon = By.res("Stream_AttachmentCancelIcon") + val columnWithMultipleFileAttachments = By.res("Stream_FileAttachmentPreviewContent") + val columnWithMultipleMediaAttachments = By.res("Stream_MediaAttachmentPreviewContent") + val mediaAttachment = By.res("Stream_MediaAttachmentPreviewItem") + val fileSize = By.res("Stream_FileSizeInPreview") + val fileName = By.res("Stream_FileNameInPreview") + val fileImage = MessageList.Message.fileImage + val linkPreviewImage = By.res("Stream_LinkPreviewImage") + val linkPreviewTitle = By.res("Stream_LinkPreviewTitle") + val linkPreviewDescription = By.res("Stream_LinkPreviewDescription") } } @@ -102,16 +117,18 @@ open class MessageListPage { 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") + val image = By.res("Stream_MediaContent_Image") + val video = By.res("Stream_MediaContent_Video") + val columnWithMultipleMediaAttachments = By.res("Stream_MultipleMediaAttachmentsColumn") val fileImage = By.res("Stream_FileAttachmentImage") - val fileDescription = By.res("Stream_FileAttachmentDescription") + val fileName = By.res("Stream_FileAttachmentName") val fileSize = By.res("Stream_FileAttachmentSize") val fileDownloadButton = By.res("Stream_FileAttachmentDownloadButton") + val columnWithMultipleFileAttachments = By.res("Stream_MultipleFileAttachmentsColumn") val giphy = By.res("Stream_GiphyContent") - val linkAttachmentPreview = By.res("Stream_LinkAttachmentPreview") - val linkAttachmentTitle = By.res("Stream_LinkAttachmentTitle") - val linkAttachmentDescription = By.res("Stream_LinkAttachmentDescription") + val linkPreviewImage = By.res("Stream_LinkAttachmentPreview") + val linkPreviewTitle = By.res("Stream_LinkAttachmentTitle") + val linkPreviewDescription = By.res("Stream_LinkAttachmentDescription") } class Reactions { 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 b3e1967d5ec..7af8d8b0daa 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 @@ -21,6 +21,7 @@ import androidx.test.uiautomator.Direction import io.getstream.chat.android.compose.pages.ChannelListPage import io.getstream.chat.android.compose.pages.LoginPage import io.getstream.chat.android.compose.pages.MessageListPage +import io.getstream.chat.android.compose.pages.MessageListPage.AttachmentPicker 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 @@ -30,6 +31,7 @@ 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 @@ -38,6 +40,7 @@ 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.AttachmentType import io.getstream.chat.android.e2e.test.mockserver.ReactionType import io.getstream.chat.android.e2e.test.robots.ParticipantRobot @@ -96,9 +99,19 @@ class UserRobot { return this } + fun tapOnSendButton(): UserRobot { + Composer.sendButton.findObject().click() + return this + } + + fun tapOnAttachmentCancelIcon(): UserRobot { + Composer.attachmentCancelIcon.waitToAppear().click() + return this + } + fun sendMessage(text: String): UserRobot { typeText(text) - Composer.sendButton.findObject().click() + tapOnSendButton() return this } @@ -289,16 +302,37 @@ class UserRobot { return this } - fun uploadImage(count: Int = 1, send: Boolean = true): UserRobot { + fun uploadAttachment(type: AttachmentType, multiple: Boolean = false, send: Boolean = true): UserRobot { + val count = if (multiple) 2 else 1 repeat(count) { Composer.attachmentsButton.waitToAppear().click() - MessageListPage.AttachmentPicker.sampleImage.waitToAppear().click() - MessageListPage.AttachmentPicker.sendButton.findObject().click() + AttachmentPicker.filesTab.waitToAppear().click() + AttachmentPicker.findFilesButton.waitToAppear().click() + + if (!AttachmentPicker.downloadsView.isDisplayed()) { + AttachmentPicker.rootsButton.waitToAppear().click() + val documentsUiPackageName = device.currentPackageName + By.text("Downloads") + .hasAncestor(By.res("$documentsUiPackageName:id/roots_list")) + .waitToAppear() + .click() + } + + if (type == AttachmentType.FILE) AttachmentPicker.pdf1 else AttachmentPicker.image1 + + if (it == 0) { + val attachment = if (type == AttachmentType.FILE) AttachmentPicker.pdf1 else AttachmentPicker.image1 + attachment.waitToAppear().click() + } else { + val attachment = if (type == AttachmentType.FILE) AttachmentPicker.pdf2 else AttachmentPicker.image2 + attachment.waitToAppear().click() + } } if (send) { Composer.sendButton.waitToAppear().click() } + 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 2a2821d4fd8..ec6c00eb0d3 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 @@ -29,6 +29,7 @@ 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.waitForCount import io.getstream.chat.android.compose.uiautomator.waitForText import io.getstream.chat.android.compose.uiautomator.waitToAppear import io.getstream.chat.android.compose.uiautomator.waitToDisappear @@ -40,10 +41,15 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertNotEquals import org.junit.Assert.assertTrue -fun UserRobot.assertMessage(text: String, isDisplayed: Boolean = true): UserRobot { +fun UserRobot.assertMessage( + text: String, + isDisplayed: Boolean = true, + isClickable: Boolean = false, +): UserRobot { if (isDisplayed) { - assertEquals(text, Message.text.waitToAppear().waitForText(text).text) - assertTrue(Message.text.isDisplayed()) + val textLocator = if (isClickable) Message.clickableText else Message.text + assertEquals(text, textLocator.waitToAppear().waitForText(text).text) + assertTrue(textLocator.isDisplayed()) assertTrue(Message.timestamp.isDisplayed()) } else { MessageListPage.MessageList.messages.findObjects().forEach { @@ -117,7 +123,7 @@ fun UserRobot.assertEditedMessage(text: String): UserRobot { return this } -fun UserRobot.assertDeletedMessage(text: String, hard: Boolean = false): UserRobot { +fun UserRobot.assertDeletedMessage(text: String? = null, hard: Boolean = false): UserRobot { if (hard) { assertFalse(Message.deletedMessage.isDisplayed()) } else { @@ -125,7 +131,9 @@ fun UserRobot.assertDeletedMessage(text: String, hard: Boolean = false): UserRob assertTrue(Message.deletedMessage.isDisplayed()) assertTrue(Message.timestamp.isDisplayed()) } - assertMessage(text, isDisplayed = false) + if (text != null) { + assertMessage(text, isDisplayed = false) + } return this } @@ -241,13 +249,6 @@ fun UserRobot.assertScrollToBottomButton(isDisplayed: Boolean): UserRobot { 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 @@ -375,3 +376,103 @@ fun UserRobot.assertMessages(text: String, count: Int): UserRobot { assertEquals(count, actualCount) return this } + +fun UserRobot.assertImage(isDisplayed: Boolean, count: Int = 1): UserRobot { + if (isDisplayed) { + assertEquals(count, Message.image.waitForCount(count).size) + if (count != 1) { + assertTrue(Message.columnWithMultipleMediaAttachments.isDisplayed()) + } + } else { + assertFalse(Message.image.waitToDisappear().isDisplayed()) + } + return this +} + +fun UserRobot.assertVideo(isDisplayed: Boolean, count: Int = 1): UserRobot { + if (isDisplayed) { + assertEquals(count, Message.video.waitForCount(count).size) + if (count != 1) { + assertTrue(Message.columnWithMultipleMediaAttachments.waitToAppear().isDisplayed()) + } + } else { + assertFalse(Message.video.waitToDisappear().isDisplayed()) + } + return this +} + +fun UserRobot.assertFile(isDisplayed: Boolean, count: Int = 1): UserRobot { + if (isDisplayed) { + assertEquals(count, Message.fileName.waitForCount(count).size) + assertEquals(count, Message.fileSize.findObjects().size) + assertEquals(count, Message.fileDownloadButton.findObjects().size) + assertEquals(count, Message.fileImage.waitForCount(count).size) + if (count > 1) { + assertTrue(Message.columnWithMultipleFileAttachments.isDisplayed()) + } + } else { + assertFalse(Message.fileName.waitToDisappear().isDisplayed()) + assertFalse(Message.fileSize.isDisplayed()) + assertFalse(Message.fileImage.isDisplayed()) + assertFalse(Message.fileDownloadButton.isDisplayed()) + } + return this +} + +fun UserRobot.assertMediaAttachmentInPreview(isDisplayed: Boolean, count: Int = 1): UserRobot { + if (isDisplayed) { + assertEquals(count, Composer.mediaAttachment.waitForCount(count).size) + assertEquals(count, Composer.attachmentCancelIcon.findObjects().size) + if (count != 1) { + assertTrue(Composer.columnWithMultipleMediaAttachments.isDisplayed()) + } + } else { + assertFalse(Composer.mediaAttachment.waitToDisappear().isDisplayed()) + assertFalse(Composer.attachmentCancelIcon.isDisplayed()) + } + return this +} + +fun UserRobot.assertFileAttachmentInPreview(isDisplayed: Boolean, count: Int = 1): UserRobot { + if (isDisplayed) { + assertTrue(Composer.fileName.waitToAppear().isDisplayed()) + assertTrue(Composer.fileSize.isDisplayed()) + assertTrue(Composer.fileImage.isDisplayed()) + assertTrue(Composer.attachmentCancelIcon.isDisplayed()) + if (count > 1) { + assertTrue(Composer.columnWithMultipleFileAttachments.isDisplayed()) + } + } else { + assertFalse(Composer.fileName.waitToDisappear().isDisplayed()) + assertFalse(Composer.fileSize.isDisplayed()) + assertFalse(Composer.fileImage.isDisplayed()) + assertFalse(Composer.attachmentCancelIcon.isDisplayed()) + } + return this +} + +fun UserRobot.assertLinkPreviewInMessageList(isDisplayed: Boolean): UserRobot { + if (isDisplayed) { + assertTrue(Message.linkPreviewImage.waitToAppear().isDisplayed()) + assertTrue(Message.linkPreviewTitle.isDisplayed()) + assertTrue(Message.linkPreviewDescription.isDisplayed()) + } else { + assertFalse(Message.linkPreviewImage.waitToDisappear().isDisplayed()) + assertFalse(Message.linkPreviewTitle.isDisplayed()) + assertFalse(Message.linkPreviewDescription.isDisplayed()) + } + return this +} + +fun UserRobot.assertLinkPreviewInComposer(isDisplayed: Boolean): UserRobot { + if (isDisplayed) { + assertTrue(Composer.linkPreviewImage.waitToAppear().isDisplayed()) + assertTrue(Composer.linkPreviewTitle.isDisplayed()) + assertTrue(Composer.linkPreviewDescription.isDisplayed()) + } else { + assertFalse(Composer.linkPreviewImage.waitToDisappear().isDisplayed()) + assertFalse(Composer.linkPreviewTitle.isDisplayed()) + assertFalse(Composer.linkPreviewDescription.isDisplayed()) + } + return this +} diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/AttachmentsTests.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/AttachmentsTests.kt new file mode 100644 index 00000000000..42766e31356 --- /dev/null +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/AttachmentsTests.kt @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.tests + +import io.getstream.chat.android.compose.robots.assertDeletedMessage +import io.getstream.chat.android.compose.robots.assertFile +import io.getstream.chat.android.compose.robots.assertFileAttachmentInPreview +import io.getstream.chat.android.compose.robots.assertImage +import io.getstream.chat.android.compose.robots.assertMediaAttachmentInPreview +import io.getstream.chat.android.compose.robots.assertVideo +import io.getstream.chat.android.e2e.test.mockserver.AttachmentType +import io.qameta.allure.kotlin.Allure.step +import io.qameta.allure.kotlin.AllureId +import org.junit.Test + +class AttachmentsTests : StreamTestCase() { + + @AllureId("5663") + @Test + fun test_uploadImage() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user attaches an image") { + userRobot.uploadAttachment(type = AttachmentType.IMAGE, send = false) + } + step("THEN image is displayed in preview") { + userRobot.assertMediaAttachmentInPreview(isDisplayed = true) + } + step("WHEN user sends an image") { + userRobot.tapOnSendButton() + } + step("THEN user can see uploaded image") { + userRobot.assertImage(isDisplayed = true) + } + } + + @AllureId("6824") + @Test + fun test_uploadMultipleImages() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user attaches multiple images") { + userRobot.uploadAttachment(type = AttachmentType.IMAGE, multiple = true, send = false) + } + step("THEN images are displayed in preview") { + userRobot.assertMediaAttachmentInPreview(isDisplayed = true, count = 2) + } + step("WHEN user sends the images") { + userRobot.tapOnSendButton() + } + step("THEN user can see uploaded images") { + userRobot.assertImage(isDisplayed = true, count = 2) + } + } + + @AllureId("6825") + @Test + fun test_deleteImage() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user sends an image") { + userRobot.uploadAttachment(type = AttachmentType.IMAGE) + } + step("AND user deletes an image") { + userRobot.deleteMessage() + } + step("THEN user can see deleted message") { + userRobot + .assertImage(isDisplayed = false) + .assertDeletedMessage() + } + } + + @AllureId("6826") + @Test + fun test_uploadFile() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user sends a file") { + userRobot.uploadAttachment(type = AttachmentType.FILE, send = false) + } + step("THEN file is displayed in preview") { + userRobot.assertFileAttachmentInPreview(isDisplayed = true) + } + step("WHEN user sends a file") { + userRobot.tapOnSendButton() + } + step("THEN user can see uploaded file") { + userRobot.assertFile(isDisplayed = true) + } + } + + @AllureId("6827") + @Test + fun test_uploadMultipleFiles() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user attaches multiple files") { + userRobot.uploadAttachment(type = AttachmentType.FILE, multiple = true, send = false) + } + step("THEN files are displayed in preview") { + userRobot.assertFileAttachmentInPreview(isDisplayed = true, count = 2) + } + step("WHEN user sends the files") { + userRobot.tapOnSendButton() + } + step("THEN user can see uploaded files") { + userRobot.assertFile(isDisplayed = true, count = 2) + } + } + + @AllureId("6828") + @Test + fun test_deleteFile() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user sends a file") { + userRobot.uploadAttachment(type = AttachmentType.IMAGE) + } + step("AND user deletes a file") { + userRobot.deleteMessage() + } + step("THEN user can see deleted message") { + userRobot + .assertImage(isDisplayed = false) + .assertDeletedMessage() + } + } + + @AllureId("5664") + @Test + fun test_participantUploadsImage() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN participant uploads an image") { + participantRobot.uploadAttachment(type = AttachmentType.IMAGE) + } + step("THEN user can see uploaded image") { + userRobot.assertImage(isDisplayed = true) + } + } + + @AllureId("5666") + @Test + fun test_participantUploadsVideo() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN participant uploads a video") { + participantRobot.uploadAttachment(type = AttachmentType.VIDEO) + } + step("THEN user can see uploaded video") { + userRobot.assertVideo(isDisplayed = true) + } + } + + @AllureId("6829") + @Test + fun test_participantUploadsFile() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN participant uploads a file") { + participantRobot.uploadAttachment(type = AttachmentType.FILE) + } + step("THEN user can see uploaded file") { + userRobot.assertFile(isDisplayed = true) + } + } +} diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/HyperLinksTests.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/HyperLinksTests.kt new file mode 100644 index 00000000000..e464bda39dc --- /dev/null +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/HyperLinksTests.kt @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.tests + +import io.getstream.chat.android.compose.robots.assertLinkPreviewInComposer +import io.getstream.chat.android.compose.robots.assertLinkPreviewInMessageList +import io.getstream.chat.android.compose.robots.assertMessage +import io.qameta.allure.kotlin.Allure.step +import io.qameta.allure.kotlin.AllureId +import org.junit.Ignore +import org.junit.Test + +class HyperLinksTests : StreamTestCase() { + + private val youtubeVideoLink = "Look at https://youtube.com/watch?v=xOX7MsrbaPY" + private val unsplashImageLink = "Look at https://unsplash.com/photos/1_2d3MRbI9c" + private val giphyGifLink = "Look at https://giphy.com/gifs/test-gw3IWyGkC0rsazTi" + + @AllureId("5691") + @Test + fun test_unsplashLinkPreview() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user types an unsplash url") { + userRobot.typeText(unsplashImageLink) + } + step("THEN user observes a link preview") { + userRobot.assertLinkPreviewInComposer(isDisplayed = true) + } + step("WHEN user taps on the send button") { + userRobot.tapOnSendButton() + } + step("THEN user observes a message with link preview") { + userRobot + .assertMessage(unsplashImageLink, isClickable = true) + .assertLinkPreviewInMessageList(isDisplayed = true) + } + } + + @AllureId("6830") + @Ignore("https://linear.app/stream/issue/AND-309") + @Test + fun test_unsplashLinkWithoutPreview() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user types an unsplash url") { + userRobot.typeText(unsplashImageLink) + } + step("AND user cancels the link preview") { + userRobot.tapOnAttachmentCancelIcon() + } + step("THEN link preview disappears") { + userRobot.assertLinkPreviewInComposer(isDisplayed = false) + } + step("WHEN user taps on the send button") { + userRobot.tapOnSendButton() + } + step("THEN user observes a message with link preview") { + userRobot + .assertMessage(unsplashImageLink, isClickable = true) + .assertLinkPreviewInMessageList(isDisplayed = false) + } + } + + @AllureId("5692") + @Test + fun test_youtubeLinkPreview() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user types a youtube url") { + userRobot.typeText(youtubeVideoLink) + } + step("THEN user observes a link preview") { + userRobot.assertLinkPreviewInComposer(isDisplayed = true) + } + step("WHEN user taps on the send button") { + userRobot.tapOnSendButton() + } + step("THEN user observes a message with link preview") { + userRobot + .assertMessage(youtubeVideoLink, isClickable = true) + .assertLinkPreviewInMessageList(isDisplayed = true) + } + } + + @AllureId("6831") + @Ignore("https://linear.app/stream/issue/AND-309") + @Test + fun test_youtubeLinkWithoutPreview() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user types a youtube url") { + userRobot.typeText(youtubeVideoLink) + } + step("AND user cancels the link preview") { + userRobot.tapOnAttachmentCancelIcon() + } + step("THEN link preview disappears") { + userRobot.assertLinkPreviewInComposer(isDisplayed = false) + } + step("WHEN user taps on the send button") { + userRobot.tapOnSendButton() + } + step("THEN user observes a message with link preview") { + userRobot + .assertMessage(youtubeVideoLink, isClickable = true) + .assertLinkPreviewInMessageList(isDisplayed = false) + } + } + + @AllureId("6832") + @Test + fun test_giphyLinkPreview() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user types a giphy url") { + userRobot.typeText(giphyGifLink) + } + step("THEN user observes a link preview") { + userRobot.assertLinkPreviewInComposer(isDisplayed = true) + } + step("WHEN user taps on the send button") { + userRobot.tapOnSendButton() + } + step("THEN user observes a message with link preview") { + userRobot + .assertMessage(giphyGifLink, isClickable = true) + .assertLinkPreviewInMessageList(isDisplayed = true) + } + } + + @AllureId("6833") + @Ignore("https://linear.app/stream/issue/AND-309") + @Test + fun test_giphyLinkWithoutPreview() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user types a giphy url") { + userRobot.typeText(giphyGifLink) + } + step("AND user cancels the link preview") { + userRobot.tapOnAttachmentCancelIcon() + } + step("THEN link preview disappears") { + userRobot.assertLinkPreviewInComposer(isDisplayed = false) + } + step("WHEN user taps on the send button") { + userRobot.tapOnSendButton() + } + step("THEN user observes a message with link preview") { + userRobot + .assertMessage(giphyGifLink, isClickable = true) + .assertLinkPreviewInMessageList(isDisplayed = false) + } + } + + @AllureId("6834") + @Test + fun test_participantSendsLinkToUnsplash() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN participant sends an unsplash url") { + userRobot.sendMessage(unsplashImageLink) + } + step("THEN user observes a message with link preview") { + userRobot + .assertMessage(unsplashImageLink, isClickable = true) + .assertLinkPreviewInMessageList(isDisplayed = true) + } + } + + @AllureId("6835") + @Test + fun test_participantSendsLinkToYoutube() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN participant sends a youtube url") { + userRobot.sendMessage(youtubeVideoLink) + } + step("THEN user observes a message with link preview") { + userRobot + .assertMessage(youtubeVideoLink, isClickable = true) + .assertLinkPreviewInMessageList(isDisplayed = true) + } + } + + @AllureId("6836") + @Test + fun test_participantSendsLinkToGiphy() { + step("GIVEN user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN participant sends a giphy url") { + userRobot.sendMessage(giphyGifLink) + } + step("THEN user observes a message with link preview") { + userRobot + .assertMessage(giphyGifLink, isClickable = true) + .assertLinkPreviewInMessageList(isDisplayed = true) + } + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentContent.kt index 8d36eb5f99b..9807b5e59ca 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentContent.kt @@ -94,7 +94,7 @@ public fun FileAttachmentContent( interactionSource = remember { MutableInteractionSource() }, onClick = {}, onLongClick = { onItemLongClick(message) }, - ), + ).testTag("Stream_MultipleFileAttachmentsColumn"), ) { for (attachment in message.attachments) { FileAttachmentItem( @@ -166,7 +166,7 @@ private fun FileAttachmentDescription( verticalArrangement = Arrangement.Center, ) { Text( - modifier = Modifier.testTag("Stream_FileAttachmentDescription"), + modifier = Modifier.testTag("Stream_FileAttachmentName"), text = attachment.title ?: attachment.name ?: "", style = ChatTheme.typography.bodyBold, maxLines = 1, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentPreviewContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentPreviewContent.kt index 33a8f07379d..5d359fa60f1 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentPreviewContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentPreviewContent.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.ui.components.CancelIcon @@ -55,7 +56,8 @@ public fun FileAttachmentPreviewContent( ) { LazyRow( modifier = modifier - .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)), + .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) + .testTag("Stream_FileAttachmentPreviewContent"), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.Start), ) { @@ -83,6 +85,7 @@ public fun FileAttachmentPreviewContent( verticalArrangement = Arrangement.Center, ) { Text( + modifier = Modifier.testTag("Stream_FileNameInPreview"), text = attachment.title ?: attachment.name ?: "", style = ChatTheme.typography.bodyBold, maxLines = 1, @@ -95,6 +98,7 @@ public fun FileAttachmentPreviewContent( } if (fileSize != null) { Text( + modifier = Modifier.testTag("Stream_FileSizeInPreview"), text = fileSize, style = ChatTheme.typography.footnote, color = ChatTheme.colors.textLowEmphasis, @@ -103,7 +107,7 @@ public fun FileAttachmentPreviewContent( } CancelIcon( - modifier = Modifier.padding(4.dp), + modifier = Modifier.padding(4.dp).testTag("Stream_AttachmentCancelIcon"), onClick = { onAttachmentRemoved(attachment) }, ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index 3c29f5c76fa..84b25b6f924 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -294,7 +294,8 @@ internal fun RowScope.MultipleMediaAttachments( modifier = Modifier .weight(1f, fill = false) .width(ChatTheme.dimens.attachmentsContentGroupPreviewWidth / 2) - .height(ChatTheme.dimens.attachmentsContentGroupPreviewHeight), + .height(ChatTheme.dimens.attachmentsContentGroupPreviewHeight) + .testTag("Stream_MultipleMediaAttachmentsColumn"), verticalArrangement = Arrangement.spacedBy(gridSpacing), ) { for (attachmentIndex in 0 until maximumNumberOfPreviewedItems step 2) { @@ -459,11 +460,13 @@ internal fun MediaAttachmentContentItem( val downloadAttachmentUriGenerator = ChatTheme.streamDownloadAttachmentUriGenerator val downloadRequestInterceptor = ChatTheme.streamDownloadRequestInterceptor + val testTag = if (isVideo) "Video" else "Image" + Box( modifier = modifier .background(Color.Black) .fillMaxWidth() - .testTag("Stream_MediaContent") + .testTag("Stream_MediaContent_$testTag") .combinedClickable( interactionSource = MutableInteractionSource(), indication = ripple(), diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContent.kt index bfa0c1e0db0..327d377c863 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContent.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import com.skydoves.landscapist.ImageOptions import io.getstream.chat.android.compose.ui.attachments.factory.DefaultPreviewItemOverlayContent @@ -61,7 +62,9 @@ public fun MediaAttachmentPreviewContent( }, ) { LazyRow( - modifier = modifier.clip(ChatTheme.shapes.attachment), + modifier = modifier + .clip(ChatTheme.shapes.attachment) + .testTag("Stream_MediaAttachmentPreviewContent"), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.Start), ) { @@ -94,7 +97,8 @@ private fun MediaAttachmentPreviewItem( Box( modifier = Modifier .size(MediaAttachmentPreviewItemSize.dp) - .clip(RoundedCornerShape(16.dp)), + .clip(RoundedCornerShape(16.dp)) + .testTag("Stream_MediaAttachmentPreviewItem"), contentAlignment = Alignment.Center, ) { StreamImage( @@ -108,7 +112,8 @@ private fun MediaAttachmentPreviewItem( CancelIcon( modifier = Modifier .align(Alignment.TopEnd) - .padding(4.dp), + .padding(4.dp) + .testTag("Stream_AttachmentCancelIcon"), onClick = { onAttachmentRemoved(mediaAttachment) }, ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/attachments/files/FilesPicker.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/attachments/files/FilesPicker.kt index 234d7a1f5ff..ed34fc11f3f 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/attachments/files/FilesPicker.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/attachments/files/FilesPicker.kt @@ -41,6 +41,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -90,6 +91,7 @@ public fun FilesPicker( IconButton( content = { Icon( + modifier = Modifier.testTag("Stream_FindFilesButton"), painter = painterResource(id = R.drawable.stream_compose_ic_more_files), contentDescription = stringResource(id = R.string.stream_compose_send_attachment), tint = ChatTheme.colors.primaryAccent, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/ComposerLinkPreview.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/ComposerLinkPreview.kt index 495cd27256d..a759b1cbe3d 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/ComposerLinkPreview.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/ComposerLinkPreview.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import com.skydoves.landscapist.ImageOptions import io.getstream.chat.android.compose.R @@ -137,7 +138,8 @@ private fun ComposerLinkImagePreview(attachment: Attachment) { modifier = Modifier .height(theme.imageSize.height) .width(theme.imageSize.width) - .clip(theme.imageShape), + .clip(theme.imageShape) + .testTag("Stream_LinkPreviewImage"), imageOptions = ImageOptions(contentScale = ContentScale.Crop), ) } @@ -166,6 +168,7 @@ private fun ComposerLinkTitle(title: String?) { title ?: return val textStyle = ChatTheme.messageComposerTheme.linkPreview.title Text( + modifier = Modifier.testTag("Stream_LinkPreviewTitle"), text = title, style = textStyle.style, color = textStyle.color, @@ -179,6 +182,7 @@ private fun ComposerLinkDescription(description: String?) { description ?: return val textStyle = ChatTheme.messageComposerTheme.linkPreview.subtitle Text( + modifier = Modifier.testTag("Stream_LinkPreviewDescription"), text = description, style = textStyle.style, color = textStyle.color, @@ -198,7 +202,8 @@ private fun ComposerLinkCancelIcon( .background( shape = theme.cancelIcon.backgroundShape, color = theme.cancelIcon.backgroundColor, - ), + ) + .testTag("Stream_AttachmentCancelIcon"), painter = theme.cancelIcon.painter, contentDescription = stringResource(id = R.string.stream_compose_cancel), tint = theme.cancelIcon.tint, 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 cd6ca22711f..1da69792b08 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 @@ -69,6 +69,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 waitForCount (Landroidx/test/uiautomator/BySelector;IJ)Ljava/util/List; + public static synthetic fun waitForCount$default (Landroidx/test/uiautomator/BySelector;IJILjava/lang/Object;)Ljava/util/List; 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; 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 2f59e6bb78b..aebc8f10934 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 @@ -141,7 +141,7 @@ public class ParticipantRobot { } public fun uploadAttachment(type: AttachmentType, count: Int = 1): ParticipantRobot { - mockServer.postRequest("participant/message?$type=$count") + mockServer.postRequest("participant/message?${type.attachment}=$count") return this } @@ -151,7 +151,7 @@ public class ParticipantRobot { last: Boolean = true, ): ParticipantRobot { val quote = if (last) "quote_last=true" else "quote_first=true" - mockServer.postRequest("participant/message?$quote&$type=$count") + mockServer.postRequest("participant/message?$quote&${type.attachment}=$count") return this } @@ -160,7 +160,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.attachment}=$count&thread=true&thread_and_channel=$alsoSendInChannel" mockServer.postRequest(endpoint) return this } @@ -172,7 +172,8 @@ public class ParticipantRobot { last: Boolean = true, ): ParticipantRobot { val quote = if (last) "quote_last=true" else "quote_first=true" - val endpoint = "participant/message?$quote&$type=$count&thread=true&thread_and_channel=$alsoSendInChannel" + val endpoint = "participant/message?" + + "$quote&${type.attachment}=$count&thread=true&thread_and_channel=$alsoSendInChannel" mockServer.postRequest(endpoint) return this } 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 61d7a66f4bc..1033f8f5cb6 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 @@ -56,3 +56,14 @@ public fun UiObject2.waitForText( } return this } + +public fun BySelector.waitForCount(count: Int, timeOutMillis: Long = defaultTimeout): List { + val endTime = System.currentTimeMillis() + timeOutMillis + var elements: List = emptyList() + var success = false + while (!success && System.currentTimeMillis() < endTime) { + elements = findObjects() + success = elements.size == count + } + return elements +}