From 44b07afafa4e00bbb5a1bc39f5582b930f480469 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Thu, 23 Jan 2025 16:57:12 +0000 Subject: [PATCH] [CI] Add Attachments and Link previews related E2E tests --- .github/workflows/e2e-test-cron.yml | 4 +- .github/workflows/e2e-test.yml | 5 +- fastlane/Fastfile | 11 +- fastlane/attachments/file.pdf | Bin 0 -> 39804 bytes fastlane/attachments/file.png | Bin 0 -> 12117 bytes .../android/compose/pages/MessageListPage.kt | 33 ++- .../chat/android/compose/robots/UserRobot.kt | 42 +++- .../robots/UserRobotMessageListAsserts.kt | 125 +++++++++- .../android/compose/tests/AttachmentsTests.kt | 191 +++++++++++++++ .../android/compose/tests/HyperLinksTests.kt | 224 ++++++++++++++++++ .../content/FileAttachmentContent.kt | 4 +- .../content/FileAttachmentPreviewContent.kt | 10 +- .../content/MediaAttachmentContent.kt | 7 +- .../content/MediaAttachmentPreviewContent.kt | 11 +- .../attachments/files/FilesPicker.kt | 2 + .../composer/ComposerLinkPreview.kt | 9 +- .../api/stream-chat-android-e2e-test.api | 2 + .../e2e/test/robots/ParticipantRobot.kt | 9 +- .../chat/android/e2e/test/uiautomator/Wait.kt | 11 + 19 files changed, 653 insertions(+), 47 deletions(-) create mode 100644 fastlane/attachments/file.pdf create mode 100644 fastlane/attachments/file.png create mode 100644 stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/AttachmentsTests.kt create mode 100644 stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/HyperLinksTests.kt 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 bf5f194083d..d171d61ec5b 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/fastlane/Fastfile b/fastlane/Fastfile index 1d8f375b553..0befc5837f4 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 0000000000000000000000000000000000000000..9eb1cfabc37b93e6091ae688d4d7b51be7329102 GIT binary patch literal 39804 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 OcyS*Wmy+hyeg6lxceua+ literal 0 HcmV?d00001 diff --git a/fastlane/attachments/file.png b/fastlane/attachments/file.png new file mode 100644 index 0000000000000000000000000000000000000000..74103e3f83808f387b3d89c4ed5ef98696614f5d GIT binary patch literal 12117 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 q{RgTFHsYc1j~>4N{&+Pyg+XNHPW?* 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..560cda88d75 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,9 @@ 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 +}