diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 26b0ddf7da9..d085f41a54a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -90,6 +90,7 @@ lane :run_e2e_test do |options| start_mock_server(local_server: options[:local_server], branch: options[:mock_server_branch]) install_test_services + update_emulator_settings upload_attachments stream_apk_folder_path = options[:apk_folder_path] || '..' @@ -165,6 +166,11 @@ private_lane :install_test_services do install(tool: :allurectl, chmod: true) if is_ci end +private_lane :update_emulator_settings do + sh('adb shell settings put system show_touches 1') + sh('adb shell settings put system pointer_location 1') +end + desc 'Run fastlane linting' lane :rubocop do next unless is_check_required(sources: sources_matrix[:ruby], force_check: @force_check) diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/QuotedReplyTests.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/QuotedReplyTests.kt index 35c2910bf06..b1ef22b927c 100644 --- a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/QuotedReplyTests.kt +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/QuotedReplyTests.kt @@ -389,6 +389,7 @@ class QuotedReplyTests : StreamTestCase() { } @AllureId("5893") + @Ignore("https://linear.app/stream/issue/AND-960") @Test fun test_quotedReplyNotInList_whenParticipantAddsQuotedReply_Message_InThread() { step("GIVEN user opens the channel") { @@ -405,8 +406,8 @@ class QuotedReplyTests : StreamTestCase() { } step("THEN user observes the quote reply in thread") { userRobot - .openThread() - .assertQuotedMessage(text = quoteReply, quote = sampleText, isDisplayed = true) + .openThread(usingContextMenu = false) + .assertQuotedMessage(text = quoteReply, quote = "1", isDisplayed = true) .assertScrollToBottomButton(isDisplayed = false) } step("WHEN user taps on a quoted message") { @@ -414,7 +415,7 @@ class QuotedReplyTests : StreamTestCase() { } step("THEN user is scrolled up to the quote") { userRobot - .assertQuotedMessage(text = quoteReply, quote = sampleText, isDisplayed = false) + .assertQuotedMessage(text = quoteReply, quote = "1", isDisplayed = false) .assertScrollToBottomButton(isDisplayed = true) } } @@ -557,6 +558,8 @@ class QuotedReplyTests : StreamTestCase() { @AllureId("5898") @Test fun test_quotedReplyInThreadAndAlsoInChannel() { + val quotedText = messagesCount.toString() + step("GIVEN user opens the channel") { backendRobot.generateChannels( channelsCount = 1, @@ -567,17 +570,17 @@ class QuotedReplyTests : StreamTestCase() { userRobot.login().openChannel() } step("WHEN participant adds a quoted reply in thread and also in channel") { - participantRobot.quoteMessageInThread(quoteReply, alsoSendInChannel = true, last = false) + participantRobot.quoteMessageInThread(quoteReply, alsoSendInChannel = true) } step("THEN user observes the quoted reply in channel") { userRobot - .assertQuotedMessage(text = quoteReply, quote = sampleText) + .assertQuotedMessage(text = quoteReply, quote = quotedText) .assertScrollToBottomButton(isDisplayed = false) } step("AND user observes the quoted reply also in thread") { userRobot - .openThread() - .assertQuotedMessage(text = quoteReply, quote = sampleText) + .openThread(usingContextMenu = false) + .assertQuotedMessage(text = quoteReply, quote = quotedText) .assertScrollToBottomButton(isDisplayed = false) } } diff --git a/stream-chat-android-compose-sample/src/e2e/java/io/getstream/chat/android/compose/sample/ui/JwtTestActivity.kt b/stream-chat-android-compose-sample/src/e2e/java/io/getstream/chat/android/compose/sample/ui/JwtTestActivity.kt index d1d1cb5c7a8..c92d045f87f 100644 --- a/stream-chat-android-compose-sample/src/e2e/java/io/getstream/chat/android/compose/sample/ui/JwtTestActivity.kt +++ b/stream-chat-android-compose-sample/src/e2e/java/io/getstream/chat/android/compose/sample/ui/JwtTestActivity.kt @@ -49,7 +49,6 @@ import androidx.lifecycle.lifecycleScope import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.client.token.TokenProvider import io.getstream.chat.android.compose.sample.data.PredefinedUserCredentials -import io.getstream.chat.android.compose.sample.data.PredefinedUserCredentials.API_KEY import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.models.ConnectionState import io.getstream.chat.android.models.User @@ -184,7 +183,7 @@ class JwtTestActivity : AppCompatActivity() { private suspend fun fetchJwtToken(baseUrl: String, userId: String): String { return withContext(Dispatchers.IO) { - val endpoint = "$baseUrl/jwt/get?api_key=$API_KEY&user_id=$userId" + val endpoint = "$baseUrl/jwt/get?platform=android" val request = Request.Builder().url(endpoint).build() OkHttpClient().newCall(request).execute().use { response -> diff --git a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/rules/RetryRule.kt b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/rules/RetryRule.kt index b96e1fed856..4fd36f16d56 100644 --- a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/rules/RetryRule.kt +++ b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/rules/RetryRule.kt @@ -17,8 +17,10 @@ package io.getstream.chat.android.e2e.test.rules import android.database.sqlite.SQLiteDatabase +import android.os.Environment import androidx.test.platform.app.InstrumentationRegistry import io.getstream.chat.android.compose.uiautomator.allureLogcat +import io.getstream.chat.android.compose.uiautomator.allureScreenrecord import io.getstream.chat.android.compose.uiautomator.allureScreenshot import io.getstream.chat.android.compose.uiautomator.allureWindowHierarchy import io.getstream.chat.android.compose.uiautomator.device @@ -44,24 +46,37 @@ public class RetryRule(private val count: Int) : TestRule { return object : Statement() { @Throws(Throwable::class) override fun evaluate() { + val testName = description.displayName val retryAnnotation: Retry? = description.getAnnotation(Retry::class.java) val retryCount = retryAnnotation?.count ?: count val databaseOperations = DatabaseOperations() var caughtThrowable: Throwable? = null + lateinit var videoFilePath: String + lateinit var recordingThread: Thread for (i in 0 until retryCount) { try { - System.err.println("${description.displayName}: run #${i + 1} started.") + System.err.println("$testName: run #${i + 1} started.") device.executeShellCommand("logcat -c") + videoFilePath = "${Environment.getExternalStorageDirectory().absolutePath}/$testName.mp4" + recordingThread = startVideoRecording(videoFilePath) base.evaluate() + stopVideoRecording(videoFilePath, recordingThread) return } catch (t: Throwable) { - System.err.println("${description.displayName}: run #${i + 1} failed.") - databaseOperations.clearDatabases() + System.err.println("$testName: run #${i + 1} failed.") caughtThrowable = t + databaseOperations.clearDatabases() + stopVideoRecording(videoFilePath, recordingThread) device.allureLogcat(name = "logcat_${i + 1}") device.allureScreenshot(name = "screenshot_${i + 1}") device.allureWindowHierarchy(name = "hierarchy_${i + 1}") + device.allureScreenrecord( + name = "record_${i + 1}", + file = File(videoFilePath), + ) + } finally { + device.executeShellCommand("rm $videoFilePath") } } @@ -69,6 +84,46 @@ public class RetryRule(private val count: Int) : TestRule { } } } + + private fun startVideoRecording(remoteVideoPath: String): Thread { + return Thread { + device.executeShellCommand( + "screenrecord --bit-rate 8000000 --time-limit 180 $remoteVideoPath", + ) + }.also { it.start() } + } + + private fun stopVideoRecording(remoteVideoPath: String, thread: Thread) { + device.executeShellCommand("pkill -INT screenrecord") + thread.join(5000) + waitUntil { !isScreenrecordRunning() } + waitUntil { isFileStable(remoteVideoPath) } + } + + private fun isScreenrecordRunning(): Boolean { + val ps = device.executeShellCommand("ps | grep screenrecord || true") + return ps.contains("screenrecord") + } + + private fun isFileStable(path: String): Boolean { + val output = device.executeShellCommand("ls -l $path") + val size = output.trim().split(Regex("\\s+")).getOrNull(4)?.toLongOrNull() ?: 0L + Thread.sleep(200) + val output2 = device.executeShellCommand("ls -l $path") + val size2 = output2.trim().split(Regex("\\s+")).getOrNull(4)?.toLongOrNull() ?: 0L + return size > 0 && size == size2 + } + + @Suppress("TooGenericExceptionThrown") + private fun waitUntil(timeoutMs: Long = 5000, condition: () -> Boolean) { + val start = System.currentTimeMillis() + while (!condition()) { + if (System.currentTimeMillis() - start > timeoutMs) { + throw RuntimeException("Timeout waiting for video recording to finish") + } + Thread.sleep(200) + } + } } public open class DatabaseOperations { 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 f7de97756f9..b68c47e7674 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 @@ -27,6 +27,7 @@ import androidx.test.uiautomator.UiObject2 import io.qameta.allure.kotlin.Allure import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream +import java.io.File public fun UiDevice.stopApp() { executeShellCommand("pm clear $packageName") @@ -155,6 +156,15 @@ public fun UiDevice.allureScreenshot(name: String) { } } +public fun UiDevice.allureScreenrecord(name: String, file: File) { + Allure.attachment( + name = "$name.mp4", + type = "video/mp4", + fileExtension = ".mp4", + content = file.inputStream(), + ) +} + public fun UiDevice.allureLogcat(name: String) { Allure.attachment( name = "$name.txt",