From 14054344f4789b49f557d48d64fe2b763cc94d51 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 07:20:07 +0000 Subject: [PATCH 01/21] Update dependency org.jsoup:jsoup to v1.22.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index edbfdc2489..3a1afc3373 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -191,7 +191,7 @@ serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-jso kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0" showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" } showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" } -jsoup = "org.jsoup:jsoup:1.21.2" +jsoup = "org.jsoup:jsoup:1.22.2" appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = "app.cash.molecule:molecule-runtime:2.2.0" timber = "com.jakewharton.timber:timber:5.0.1" From 88606474770e988cfdcb573cef474cabf824d3b2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Apr 2026 17:01:08 +0200 Subject: [PATCH 02/21] Element Call: remove support for SPA call links. Closes #6578 --- .../call/api/{CallType.kt => CallData.kt} | 25 +- .../call/api/ElementCallEntryPoint.kt | 8 +- .../call/impl/src/main/AndroidManifest.xml | 36 +-- .../call/impl/DefaultElementCallEntryPoint.kt | 14 +- .../RingingCallNotificationCreator.kt | 11 +- .../receivers/DeclineCallBroadcastReceiver.kt | 4 +- .../call/impl/ui/CallScreenPresenter.kt | 82 +++---- .../features/call/impl/ui/CallScreenState.kt | 1 - .../call/impl/ui/CallScreenStateProvider.kt | 2 - .../call/impl/ui/CallTypeExtension.kt | 19 -- .../call/impl/ui/ElementCallActivity.kt | 43 ++-- .../call/impl/ui/IncomingCallActivity.kt | 12 +- .../call/impl/utils/ActiveCallManager.kt | 51 ++-- .../call/impl/utils/CallIntentDataParser.kt | 98 -------- .../call/impl/utils/IntentProvider.kt | 10 +- .../call/DefaultElementCallEntryPointTest.kt | 6 +- .../android/features/call/ui/CallDataTest.kt | 23 ++ .../call/ui/CallScreenPresenterTest.kt | 95 +------- .../android/features/call/ui/CallTypeTest.kt | 45 ---- .../call/utils/CallIntentDataParserTest.kt | 226 ------------------ .../utils/DefaultActiveCallManagerTest.kt | 32 ++- .../call/utils/FakeActiveCallManager.kt | 14 +- .../call/test/FakeElementCallEntryPoint.kt | 14 +- .../messages/impl/MessagesFlowNode.kt | 12 +- .../roomdetails/impl/RoomDetailsFlowNode.kt | 8 +- .../userprofile/impl/UserProfileFlowNode.kt | 4 +- .../NotificationResultProcessor.kt | 8 +- .../DefaultNotificationResultProcessorTest.kt | 8 +- tools/adb/callLinkCustomScheme.sh | 14 -- tools/adb/callLinkCustomScheme2.sh | 14 -- 30 files changed, 203 insertions(+), 736 deletions(-) rename features/call/api/src/main/kotlin/io/element/android/features/call/api/{CallType.kt => CallData.kt} (50%) delete mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallTypeExtension.kt delete mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt create mode 100644 features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallDataTest.kt delete mode 100644 features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallTypeTest.kt delete mode 100644 features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt delete mode 100755 tools/adb/callLinkCustomScheme.sh delete mode 100755 tools/adb/callLinkCustomScheme2.sh diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallData.kt similarity index 50% rename from features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt rename to features/call/api/src/main/kotlin/io/element/android/features/call/api/CallData.kt index 4b09813418..c1dcf573c6 100644 --- a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt +++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallData.kt @@ -14,22 +14,9 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.parcelize.Parcelize -sealed interface CallType : NodeInputs, Parcelable { - @Parcelize - data class ExternalUrl(val url: String) : CallType { - override fun toString(): String { - return "ExternalUrl" - } - } - - @Parcelize - data class RoomCall( - val sessionId: SessionId, - val roomId: RoomId, - val isAudioCall: Boolean - ) : CallType { - override fun toString(): String { - return "RoomCall(sessionId=$sessionId, roomId=$roomId, isAudioCall=$isAudioCall)" - } - } -} +@Parcelize +data class CallData( + val sessionId: SessionId, + val roomId: RoomId, + val isAudioCall: Boolean +) : NodeInputs, Parcelable diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt index caa557f4de..2976635ee2 100644 --- a/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt +++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt @@ -17,13 +17,13 @@ import io.element.android.libraries.matrix.api.core.UserId interface ElementCallEntryPoint { /** * Start a call of the given type. - * @param callType The type of call to start. + * @param callData The data of call to start. */ - fun startCall(callType: CallType) + fun startCall(callData: CallData) /** * Handle an incoming call. - * @param callType The type of call. + * @param callData The data of call. * @param eventId The event id of the event that started the call. * @param senderId The user id of the sender of the event that started the call. * @param roomName The name of the room the call is in. @@ -35,7 +35,7 @@ interface ElementCallEntryPoint { * @param textContent The text content of the notification. If null the default content from the system will be used. */ suspend fun handleIncomingCall( - callType: CallType.RoomCall, + callData: CallData, eventId: EventId, senderId: UserId, roomName: String?, diff --git a/features/call/impl/src/main/AndroidManifest.xml b/features/call/impl/src/main/AndroidManifest.xml index daf1a910c9..c35c6843ff 100644 --- a/features/call/impl/src/main/AndroidManifest.xml +++ b/features/call/impl/src/main/AndroidManifest.xml @@ -30,44 +30,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:taskAffinity="io.element.android.features.call" /> ().inject(this) appCoroutineScope.launch { activeCallManager.hangUpCall( - callType = CallType.RoomCall( + callData = CallData( sessionId = notificationData.sessionId, roomId = notificationData.roomId, isAudioCall = notificationData.audioOnly diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt index da2c57c0ac..b9bd6640b4 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt @@ -23,7 +23,7 @@ import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.compound.theme.ElementTheme -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.impl.data.WidgetMessage import io.element.android.features.call.impl.utils.ActiveCallManager import io.element.android.features.call.impl.utils.CallWidgetProvider @@ -52,7 +52,7 @@ import kotlin.time.Duration.Companion.seconds @AssistedInject class CallScreenPresenter( - @Assisted private val callType: CallType, + @Assisted private val callData: CallData, @Assisted private val navigator: CallScreenNavigator, private val callWidgetProvider: CallWidgetProvider, userAgentProvider: UserAgentProvider, @@ -69,10 +69,9 @@ class CallScreenPresenter( ) : Presenter { @AssistedFactory interface Factory { - fun create(callType: CallType, navigator: CallScreenNavigator): CallScreenPresenter + fun create(callData: CallData, navigator: CallScreenNavigator): CallScreenPresenter } - private val isInWidgetMode = callType is CallType.RoomCall private val userAgent = userAgentProvider.provide() @Composable @@ -90,9 +89,9 @@ class CallScreenPresenter( DisposableEffect(Unit) { coroutineScope.launch { // Sets the call as joined - activeCallManager.joinedCall(callType) + activeCallManager.joinedCall(callData) fetchRoomCallUrl( - inputs = callType, + callData = callData, urlState = urlState, callWidgetDriver = callWidgetDriver, languageTag = languageTag, @@ -100,19 +99,10 @@ class CallScreenPresenter( ) } onDispose { - appCoroutineScope.launch { activeCallManager.hangUpCall(callType) } + appCoroutineScope.launch { activeCallManager.hangUpCall(callData) } } } - - when (callType) { - is CallType.ExternalUrl -> { - // No analytics yet for external calls - } - is CallType.RoomCall -> { - screenTracker.TrackScreen(screen = MobileScreen.ScreenName.RoomCall) - } - } - + screenTracker.TrackScreen(screen = MobileScreen.ScreenName.RoomCall) HandleMatrixClientSyncState() callWidgetDriver.value?.let { driver -> @@ -149,18 +139,15 @@ class CallScreenPresenter( .launchIn(this) } - if (callType is CallType.RoomCall) { - // Note: For external calls isWidgetLoaded will always be false - LaunchedEffect(Unit) { - // Wait for the call to be joined, if it takes too long, we display an error - delay(10.seconds) + LaunchedEffect(Unit) { + // Wait for the call to be joined, if it takes too long, we display an error + delay(10.seconds) - if (!isWidgetLoaded) { - Timber.w("The call took too long to load. Displaying an error before exiting.") + if (!isWidgetLoaded) { + Timber.w("The call took too long to load. Displaying an error before exiting.") - // This will display a simple 'Sorry, an error occurred' dialog and force the user to exit the call - webViewError = "" - } + // This will display a simple 'Sorry, an error occurred' dialog and force the user to exit the call + webViewError = "" } } } @@ -204,37 +191,29 @@ class CallScreenPresenter( webViewError = webViewError, userAgent = userAgent, isCallActive = isWidgetLoaded, - isInWidgetMode = isInWidgetMode, eventSink = ::handleEvent, ) } private suspend fun fetchRoomCallUrl( - inputs: CallType, + callData: CallData, urlState: MutableState>, callWidgetDriver: MutableState, languageTag: String?, theme: String?, ) { urlState.runCatchingUpdatingState { - when (inputs) { - is CallType.ExternalUrl -> { - inputs.url - } - is CallType.RoomCall -> { - val result = callWidgetProvider.getWidget( - sessionId = inputs.sessionId, - roomId = inputs.roomId, - clientId = UUID.randomUUID().toString(), - isAudioCall = inputs.isAudioCall, - languageTag = languageTag, - theme = theme, - ).getOrThrow() - callWidgetDriver.value = result.driver - Timber.d("Call widget driver initialized for sessionId: ${inputs.sessionId}, roomId: ${inputs.roomId}") - result.url - } - } + val result = callWidgetProvider.getWidget( + sessionId = callData.sessionId, + roomId = callData.roomId, + clientId = UUID.randomUUID().toString(), + isAudioCall = callData.isAudioCall, + languageTag = languageTag, + theme = theme, + ).getOrThrow() + callWidgetDriver.value = result.driver + Timber.d("Call widget driver initialized for sessionId: ${callData.sessionId}, roomId: ${callData.roomId}") + result.url } } @@ -242,12 +221,11 @@ class CallScreenPresenter( private fun HandleMatrixClientSyncState() { val coroutineScope = rememberCoroutineScope() DisposableEffect(Unit) { - val roomCallType = callType as? CallType.RoomCall ?: return@DisposableEffect onDispose {} - val client = matrixClientsProvider.getOrNull(roomCallType.sessionId) ?: return@DisposableEffect onDispose { - Timber.w("No MatrixClient found for sessionId, can't send call notification: ${roomCallType.sessionId}") + val client = matrixClientsProvider.getOrNull(callData.sessionId) ?: return@DisposableEffect onDispose { + Timber.w("No MatrixClient found for sessionId, can't send call notification: ${callData.sessionId}") } coroutineScope.launch { - Timber.d("Observing sync state in-call for sessionId: ${roomCallType.sessionId}") + Timber.d("Observing sync state in-call for sessionId: ${callData.sessionId}") client.syncService.syncState .collect { state -> if (state != SyncState.Running) { @@ -256,7 +234,7 @@ class CallScreenPresenter( } } onDispose { - Timber.d("Stopped observing sync state in-call for sessionId: ${roomCallType.sessionId}") + Timber.d("Stopped observing sync state in-call for sessionId: ${callData.sessionId}") // Make sure we mark the call as ended in the app state appForegroundStateService.updateIsInCallState(false) } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt index c07594aebb..3608a2b620 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt @@ -15,6 +15,5 @@ data class CallScreenState( val webViewError: String?, val userAgent: String, val isCallActive: Boolean, - val isInWidgetMode: Boolean, val eventSink: (CallScreenEvents) -> Unit, ) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt index 3e72f96f87..036bb6103a 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt @@ -26,7 +26,6 @@ internal fun aCallScreenState( webViewError: String? = null, userAgent: String = "", isCallActive: Boolean = true, - isInWidgetMode: Boolean = false, eventSink: (CallScreenEvents) -> Unit = {}, ): CallScreenState { return CallScreenState( @@ -34,7 +33,6 @@ internal fun aCallScreenState( webViewError = webViewError, userAgent = userAgent, isCallActive = isCallActive, - isInWidgetMode = isInWidgetMode, eventSink = eventSink, ) } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallTypeExtension.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallTypeExtension.kt deleted file mode 100644 index 0c18c3e1a4..0000000000 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallTypeExtension.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.call.impl.ui - -import io.element.android.features.call.api.CallType -import io.element.android.libraries.matrix.api.core.SessionId - -fun CallType.getSessionId(): SessionId? { - return when (this) { - is CallType.ExternalUrl -> null - is CallType.RoomCall -> sessionId - } -} diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt index 5fa3beb36a..dddcfceb50 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt @@ -35,8 +35,7 @@ import androidx.core.util.Consumer import androidx.lifecycle.Lifecycle import dev.zacsweers.metro.Inject import io.element.android.compound.colors.SemanticColorsLightDark -import io.element.android.features.call.api.CallType -import io.element.android.features.call.api.CallType.ExternalUrl +import io.element.android.features.call.api.CallData import io.element.android.features.call.impl.DefaultElementCallEntryPoint import io.element.android.features.call.impl.di.CallBindings import io.element.android.features.call.impl.pip.PictureInPictureEvents @@ -44,7 +43,6 @@ import io.element.android.features.call.impl.pip.PictureInPicturePresenter import io.element.android.features.call.impl.pip.PictureInPictureState import io.element.android.features.call.impl.pip.PipView import io.element.android.features.call.impl.services.CallForegroundService -import io.element.android.features.call.impl.utils.CallIntentDataParser import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.libraries.androidutils.browser.ConsoleMessageLogger import io.element.android.libraries.architecture.Presenter @@ -64,7 +62,6 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator, PipView { - @Inject lateinit var callIntentDataParser: CallIntentDataParser @Inject lateinit var presenterFactory: CallScreenPresenter.Factory @Inject lateinit var appPreferencesStore: AppPreferencesStore @Inject lateinit var featureFlagService: FeatureFlagService @@ -80,7 +77,7 @@ class ElementCallActivity : private val requestPermissionsLauncher = registerPermissionResultLauncher() - private val webViewTarget = mutableStateOf(null) + private val webViewTarget = mutableStateOf(null) private var eventSink: ((CallScreenEvents) -> Unit)? = null @@ -98,7 +95,7 @@ class ElementCallActivity : window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) } - setCallType(intent) + setCallData(intent) // If presenter is not created at this point, it means we have no call to display, the Activity is finishing, so return early if (!::presenter.isInitialized) { return @@ -111,8 +108,8 @@ class ElementCallActivity : setContent { val pipState = pictureInPicturePresenter.present() ListenToAndroidEvents(pipState) - val colors by remember(webViewTarget.value?.getSessionId()) { - enterpriseService.semanticColorsFlow(sessionId = webViewTarget.value?.getSessionId()) + val colors by remember(webViewTarget.value?.sessionId) { + enterpriseService.semanticColorsFlow(sessionId = webViewTarget.value?.sessionId) }.collectAsState(SemanticColorsLightDark.default) ElementThemeApp( appPreferencesStore = appPreferencesStore, @@ -123,9 +120,8 @@ class ElementCallActivity : ) { val state = presenter.present() eventSink = state.eventSink - LaunchedEffect(state.isCallActive, state.isInWidgetMode) { - // Note when not in WidgetMode, isCallActive will never be true, so consider the call is active - if (state.isCallActive || !state.isInWidgetMode) { + LaunchedEffect(state.isCallActive) { + if (state.isCallActive) { setCallIsActive() } } @@ -188,7 +184,7 @@ class ElementCallActivity : override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - setCallType(intent) + setCallData(intent) } override fun onDestroy() { @@ -207,25 +203,24 @@ class ElementCallActivity : finish() } - private fun setCallType(intent: Intent?) { - val callType = intent?.let { - IntentCompat.getParcelableExtra(intent, DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, CallType::class.java) - ?: intent.dataString?.let(::parseUrl)?.let(::ExternalUrl) + private fun setCallData(intent: Intent?) { + val callData = intent?.let { + IntentCompat.getParcelableExtra(intent, DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, CallData::class.java) } - val currentCallType = webViewTarget.value - if (currentCallType == null) { - if (callType == null) { + val currentCallData = webViewTarget.value + if (currentCallData == null) { + if (callData == null) { Timber.tag(loggerTag.value).d("Re-opened the activity but we have no url to load or a cached one, finish the activity") finish() } else { Timber.tag(loggerTag.value).d("Set the call type and create the presenter") - webViewTarget.value = callType - presenter = presenterFactory.create(callType, this) + webViewTarget.value = callData + presenter = presenterFactory.create(callData, this) } } else { - if (callType == null) { + if (callData == null) { Timber.tag(loggerTag.value).d("Coming back from notification, do nothing") - } else if (callType != currentCallType) { + } else if (callData != currentCallData) { Timber.tag(loggerTag.value).d("User starts another call, restart the Activity") setIntent(intent) recreate() @@ -236,8 +231,6 @@ class ElementCallActivity : } } - private fun parseUrl(url: String?): String? = callIntentDataParser.parse(url) - private fun registerPermissionResultLauncher(): ActivityResultLauncher> { return registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt index 2c4deab65e..1d6989fb3c 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt @@ -19,7 +19,7 @@ import androidx.core.content.IntentCompat import androidx.lifecycle.lifecycleScope import dev.zacsweers.metro.Inject import io.element.android.compound.colors.SemanticColorsLightDark -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.call.impl.di.CallBindings import io.element.android.features.call.impl.notifications.CallNotificationData @@ -118,10 +118,10 @@ class IncomingCallActivity : AppCompatActivity() { private fun onAnswer(notificationData: CallNotificationData) { elementCallEntryPoint.startCall( - CallType.RoomCall( - notificationData.sessionId, - notificationData.roomId, - isAudioCall = notificationData.audioOnly + CallData( + sessionId = notificationData.sessionId, + roomId = notificationData.roomId, + isAudioCall = notificationData.audioOnly, ) ) } @@ -129,7 +129,7 @@ class IncomingCallActivity : AppCompatActivity() { private fun onCancel() { val activeCall = activeCallManager.activeCall.value ?: return appCoroutineScope.launch { - activeCallManager.hangUpCall(callType = activeCall.callType) + activeCallManager.hangUpCall(callData = activeCall.callData) } } } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt index 99679a8afb..685fc932fe 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt @@ -20,7 +20,7 @@ import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn import io.element.android.appconfig.ElementCallConfig -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.api.CurrentCall import io.element.android.features.call.impl.notifications.CallNotificationData import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator @@ -73,20 +73,20 @@ interface ActiveCallManager { /** * Called to hang up the active call. It will hang up the call and remove any existing UI and the active call. - * @param callType The type of call that the user hangs up, either an external url one or a room one. + * @param callData The data about the call. * @param notificationData The data for the incoming call notification. */ suspend fun hangUpCall( - callType: CallType, + callData: CallData, notificationData: CallNotificationData? = null, ) /** * Called after the user joined a call. It will remove any existing UI and set the call state as [CallState.InCall]. * - * @param callType The type of call that the user joined, either an external url one or a room one. + * @param callData The data about the call. */ - suspend fun joinedCall(callType: CallType) + suspend fun joinedCall(callData: CallData) } @SingleIn(AppScope::class) @@ -143,7 +143,7 @@ class DefaultActiveCallManager( return } activeCall.value = ActiveCall( - callType = CallType.RoomCall( + callData = CallData( sessionId = notificationData.sessionId, roomId = notificationData.roomId, isAudioCall = notificationData.audioOnly, @@ -198,17 +198,17 @@ class DefaultActiveCallManager( } override suspend fun hangUpCall( - callType: CallType, + callData: CallData, notificationData: CallNotificationData?, ) = mutex.withLock { - Timber.tag(tag).d("Hang up call: $callType") + Timber.tag(tag).d("Hang up call: $callData") cancelIncomingCallNotification() val currentActiveCall = activeCall.value ?: run { // activeCall.value can be null if the application has been killed while the call was ringing // Build a currentActiveCall with the provided parameters. notificationData?.let { ActiveCall( - callType = callType, + callData = callData, callState = CallState.Ringing( notificationData = notificationData, ) @@ -219,8 +219,8 @@ class DefaultActiveCallManager( return@withLock } - if (currentActiveCall.callType != callType) { - Timber.tag(tag).w("Call type $callType does not match the active call type, ignoring") + if (currentActiveCall.callData != callData) { + Timber.tag(tag).w("Call type $callData does not match the active call type, ignoring") return@withLock } if (currentActiveCall.callState is CallState.Ringing) { @@ -244,8 +244,8 @@ class DefaultActiveCallManager( activeCall.value = null } - override suspend fun joinedCall(callType: CallType) = mutex.withLock { - Timber.tag(tag).d("Joined call: $callType") + override suspend fun joinedCall(callData: CallData) = mutex.withLock { + Timber.tag(tag).d("Joined call: $callData") cancelIncomingCallNotification() if (activeWakeLock?.isHeld == true) { Timber.tag(tag).d("Releasing partial wakelock after joining call") @@ -254,7 +254,7 @@ class DefaultActiveCallManager( timedOutCallJob?.cancel() activeCall.value = ActiveCall( - callType = callType, + callData = callData, callState = CallState.InCall, ) } @@ -307,15 +307,15 @@ class DefaultActiveCallManager( private fun observeRingingCall() { activeCall .filterNotNull() - .filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall } + .filter { it.callState is CallState.Ringing } .flatMapLatest { activeCall -> - val callType = activeCall.callType as CallType.RoomCall + val callData = activeCall.callData val ringingInfo = activeCall.callState as CallState.Ringing - val client = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull() ?: run { + val client = matrixClientProvider.getOrRestore(callData.sessionId).getOrNull() ?: run { Timber.tag(tag).d("Couldn't find session for incoming call: $activeCall") return@flatMapLatest flowOf() } - val room = client.getRoom(callType.roomId) ?: run { + val room = client.getRoom(callData.roomId) ?: run { Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall") return@flatMapLatest flowOf() } @@ -346,17 +346,17 @@ class DefaultActiveCallManager( // has joined the call from another session. activeCall .filterNotNull() - .filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall } + .filter { it.callState is CallState.Ringing } .flatMapLatest { activeCall -> - val callType = activeCall.callType as CallType.RoomCall + val callData = activeCall.callData // Get a flow of updated `hasRoomCall` and `activeRoomCallParticipants` values for the room - val room = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull()?.getRoom(callType.roomId) ?: run { + val room = matrixClientProvider.getOrRestore(callData.sessionId).getOrNull()?.getRoom(callData.roomId) ?: run { Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall") return@flatMapLatest flowOf() } room.roomInfoFlow.map { Timber.tag(tag).d("Has room call status changed for ringing call: ${it.hasRoomCall}") - it.hasRoomCall to (callType.sessionId in it.activeRoomCallParticipants) + it.hasRoomCall to (callData.sessionId in it.activeRoomCallParticipants) } } // We only want to check if the room active call status changes @@ -388,10 +388,7 @@ class DefaultActiveCallManager( // Nothing to do } is CallState.InCall -> { - when (val callType = value.callType) { - is CallType.ExternalUrl -> defaultCurrentCallService.onCallStarted(CurrentCall.ExternalUrl(callType.url)) - is CallType.RoomCall -> defaultCurrentCallService.onCallStarted(CurrentCall.RoomCall(callType.roomId)) - } + defaultCurrentCallService.onCallStarted(CurrentCall.RoomCall(value.callData.roomId)) } } } @@ -404,7 +401,7 @@ class DefaultActiveCallManager( * Represents an active call. */ data class ActiveCall( - val callType: CallType, + val callData: CallData, val callState: CallState, ) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt deleted file mode 100644 index f5433c15a0..0000000000 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.call.impl.utils - -import android.net.Uri -import androidx.core.net.toUri -import dev.zacsweers.metro.Inject - -@Inject -class CallIntentDataParser { - private val validHttpSchemes = sequenceOf("https") - private val knownHosts = sequenceOf( - "call.element.io", - ) - - fun parse(data: String?): String? { - val parsedUrl = data?.toUri() ?: return null - val scheme = parsedUrl.scheme - return when { - scheme in validHttpSchemes -> parsedUrl - scheme == "element" && parsedUrl.host == "call" -> { - parsedUrl.getUrlParameter() - } - scheme == "io.element.call" && parsedUrl.host == null -> { - parsedUrl.getUrlParameter() - } - // This should never be possible, but we still need to take into account the possibility - else -> null - } - ?.takeIf { it.host in knownHosts } - ?.withCustomParameters() - } - - private fun Uri.getUrlParameter(): Uri? { - return getQueryParameter("url") - ?.let { urlParameter -> - urlParameter.toUri().takeIf { uri -> - uri.scheme in validHttpSchemes && !uri.host.isNullOrBlank() - } - } - } -} - -/** - * Ensure the uri has the following parameters and value in the fragment: - * - appPrompt=false - * - confineToRoom=true - * to ensure that the rendering will bo correct on the embedded Webview. - */ -private fun Uri.withCustomParameters(): String { - val builder = buildUpon() - // Remove the existing query parameters - builder.clearQuery() - queryParameterNames.forEach { - if (it == APP_PROMPT_PARAMETER || it == CONFINE_TO_ROOM_PARAMETER) return@forEach - builder.appendQueryParameter(it, getQueryParameter(it)) - } - // Remove the existing fragment parameters, and build the new fragment - val currentFragment = fragment ?: "" - // Reset the current fragment - builder.fragment("") - val queryFragmentPosition = currentFragment.lastIndexOf("?") - val newFragment = if (queryFragmentPosition == -1) { - // No existing query, build it. - "$currentFragment?$APP_PROMPT_PARAMETER=false&$CONFINE_TO_ROOM_PARAMETER=true" - } else { - buildString { - append(currentFragment.substring(0, queryFragmentPosition + 1)) - val queryFragment = currentFragment.substring(queryFragmentPosition + 1) - // Replace the existing parameters - val newQueryFragment = queryFragment - .replace("$APP_PROMPT_PARAMETER=true", "$APP_PROMPT_PARAMETER=false") - .replace("$CONFINE_TO_ROOM_PARAMETER=false", "$CONFINE_TO_ROOM_PARAMETER=true") - append(newQueryFragment) - // Ensure the parameters are there - if (!newQueryFragment.contains("$APP_PROMPT_PARAMETER=false")) { - if (newQueryFragment.isNotEmpty()) { - append("&") - } - append("$APP_PROMPT_PARAMETER=false") - } - if (!newQueryFragment.contains("$CONFINE_TO_ROOM_PARAMETER=true")) { - append("&$CONFINE_TO_ROOM_PARAMETER=true") - } - } - } - // We do not want to encode the Fragment part, so append it manually - return builder.build().toString() + "#" + newFragment -} - -private const val APP_PROMPT_PARAMETER = "appPrompt" -private const val CONFINE_TO_ROOM_PARAMETER = "confineToRoom" diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt index 0f74ba86d4..c6c607cbbc 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt @@ -12,21 +12,21 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import androidx.core.app.PendingIntentCompat -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.impl.DefaultElementCallEntryPoint import io.element.android.features.call.impl.ui.ElementCallActivity internal object IntentProvider { - fun createIntent(context: Context, callType: CallType): Intent = Intent(context, ElementCallActivity::class.java).apply { - putExtra(DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, callType) + fun createIntent(context: Context, callData: CallData): Intent = Intent(context, ElementCallActivity::class.java).apply { + putExtra(DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, callData) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION) } - fun getPendingIntent(context: Context, callType: CallType): PendingIntent { + fun getPendingIntent(context: Context, callData: CallData): PendingIntent { return PendingIntentCompat.getActivity( context, DefaultElementCallEntryPoint.REQUEST_CODE, - createIntent(context, callType), + createIntent(context, callData), PendingIntent.FLAG_CANCEL_CURRENT, false )!! diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt index 85cec8c586..f21447cc85 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt @@ -11,7 +11,7 @@ package io.element.android.features.call import android.content.Intent import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.impl.DefaultElementCallEntryPoint import io.element.android.features.call.impl.notifications.CallNotificationData import io.element.android.features.call.impl.ui.ElementCallActivity @@ -37,7 +37,7 @@ class DefaultElementCallEntryPointTest { @Test fun `startCall - starts ElementCallActivity setup with the needed extras`() = runTest { val entryPoint = createEntryPoint() - entryPoint.startCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, isAudioCall = false)) + entryPoint.startCall(CallData(A_SESSION_ID, A_ROOM_ID, isAudioCall = false)) val expectedIntent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, ElementCallActivity::class.java) val intent = shadowOf(RuntimeEnvironment.getApplication()).nextStartedActivity @@ -53,7 +53,7 @@ class DefaultElementCallEntryPointTest { val entryPoint = createEntryPoint(activeCallManager = activeCallManager) entryPoint.handleIncomingCall( - callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, isAudioCall = false), + callData = CallData(A_SESSION_ID, A_ROOM_ID, isAudioCall = false), eventId = AN_EVENT_ID, senderId = A_USER_ID_2, roomName = "roomName", diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallDataTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallDataTest.kt new file mode 100644 index 0000000000..f0cdd44082 --- /dev/null +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallDataTest.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.call.ui + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.call.api.CallData +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import org.junit.Test + +class CallDataTest { + @Test + fun `RoomCall stringification does not contain the URL`() { + assertThat(CallData(A_SESSION_ID, A_ROOM_ID, false).toString()) + .isEqualTo("CallData(sessionId=$A_SESSION_ID, roomId=$A_ROOM_ID, isAudioCall=false)") + } +} diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt index b6b0120451..c2c576999c 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt @@ -13,7 +13,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.impl.ui.CallScreenEvents import io.element.android.features.call.impl.ui.CallScreenNavigator import io.element.android.features.call.impl.ui.CallScreenPresenter @@ -59,38 +59,13 @@ class CallScreenPresenterTest { val warmUpRule = WarmUpRule() @Test - fun `present - with CallType ExternalUrl just loads the URL and sets the call as active`() = runTest { - val analyticsLambda = lambdaRecorder {} - val joinedCallLambda = lambdaRecorder {} - val presenter = createCallScreenPresenter( - callType = CallType.ExternalUrl("https://call.element.io"), - screenTracker = FakeScreenTracker(analyticsLambda), - activeCallManager = FakeActiveCallManager(joinedCallResult = joinedCallLambda), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - // Wait until the URL is loaded - advanceTimeBy(1.seconds) - skipItems(2) - val initialState = awaitItem() - assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io")) - assertThat(initialState.webViewError).isNull() - assertThat(initialState.isInWidgetMode).isFalse() - assertThat(initialState.isCallActive).isFalse() - analyticsLambda.assertions().isNeverCalled() - joinedCallLambda.assertions().isCalledOnce() - } - } - - @Test - fun `present - with CallType RoomCall sets call as active, loads URL and runs WidgetDriver`() = runTest { + fun `present - with CallData sets call as active, loads URL and runs WidgetDriver`() = runTest { val widgetDriver = FakeMatrixWidgetDriver() val widgetProvider = FakeCallWidgetProvider(widgetDriver) val analyticsLambda = lambdaRecorder {} - val joinedCallLambda = lambdaRecorder {} + val joinedCallLambda = lambdaRecorder {} val presenter = createCallScreenPresenter( - callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), + callData = CallData(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, widgetProvider = widgetProvider, screenTracker = FakeScreenTracker(analyticsLambda), @@ -107,7 +82,6 @@ class CallScreenPresenterTest { val initialState = awaitItem() assertThat(initialState.urlState).isInstanceOf(AsyncData.Loading::class.java) assertThat(initialState.isCallActive).isFalse() - assertThat(initialState.isInWidgetMode).isTrue() assertThat(widgetProvider.getWidgetCalled).isTrue() assertThat(widgetDriver.runCalledCount).isEqualTo(1) analyticsLambda.assertions().isCalledOnce().with(value(MobileScreen.ScreenName.RoomCall)) @@ -123,7 +97,7 @@ class CallScreenPresenterTest { fun `present - set message interceptor, send and receive messages`() = runTest { val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( - callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), + callData = CallData(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, screenTracker = FakeScreenTracker {}, ) @@ -154,7 +128,7 @@ class CallScreenPresenterTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( - callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), + callData = CallData(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), @@ -188,7 +162,7 @@ class CallScreenPresenterTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( - callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), + callData = CallData(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), @@ -223,7 +197,7 @@ class CallScreenPresenterTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( - callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), + callData = CallData(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), @@ -260,7 +234,7 @@ class CallScreenPresenterTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( - callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), + callData = CallData(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), @@ -300,7 +274,7 @@ class CallScreenPresenterTest { val matrixClient = FakeMatrixClient(syncService = syncService) val appForegroundStateService = FakeAppForegroundStateService() val presenter = createCallScreenPresenter( - callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), + callData = CallData(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), @@ -338,53 +312,8 @@ class CallScreenPresenterTest { } } - @Test - fun `present - error from WebView are updating the state`() = runTest { - val presenter = createCallScreenPresenter( - callType = CallType.ExternalUrl("https://call.element.io"), - activeCallManager = FakeActiveCallManager(), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - // Wait until the URL is loaded - advanceTimeBy(1.seconds) - skipItems(2) - val initialState = awaitItem() - initialState.eventSink(CallScreenEvents.OnWebViewError("A Webview error")) - val finalState = awaitItem() - assertThat(finalState.webViewError).isEqualTo("A Webview error") - } - } - - @Test - fun `present - error from WebView are ignored if Element Call is loaded`() = runTest { - val presenter = createCallScreenPresenter( - callType = CallType.ExternalUrl("https://call.element.io"), - activeCallManager = FakeActiveCallManager(), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - // Wait until the URL is loaded - skipItems(1) - val initialState = awaitItem() - - val messageInterceptor = FakeWidgetMessageInterceptor() - initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) - // Emit a message - messageInterceptor.givenInterceptedMessage("A message") - // WebView emits an error, but it will be ignored - initialState.eventSink(CallScreenEvents.OnWebViewError("A Webview error")) - val finalState = awaitItem() - assertThat(finalState.webViewError).isNull() - - cancelAndIgnoreRemainingEvents() - } - } - private fun TestScope.createCallScreenPresenter( - callType: CallType, + callData: CallData, navigator: CallScreenNavigator = FakeCallScreenNavigator(), widgetDriver: FakeMatrixWidgetDriver = FakeMatrixWidgetDriver(), widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver), @@ -401,7 +330,7 @@ class CallScreenPresenterTest { } val clock = SystemClock { 0 } return CallScreenPresenter( - callType = callType, + callData = callData, navigator = navigator, callWidgetProvider = widgetProvider, userAgentProvider = userAgentProvider, diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallTypeTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallTypeTest.kt deleted file mode 100644 index c83408bd3b..0000000000 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallTypeTest.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.call.ui - -import com.google.common.truth.Truth.assertThat -import io.element.android.features.call.api.CallType -import io.element.android.features.call.impl.ui.getSessionId -import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_SESSION_ID -import org.junit.Test - -class CallTypeTest { - @Test - fun `getSessionId returns null for ExternalUrl`() { - assertThat(CallType.ExternalUrl("aURL").getSessionId()).isNull() - } - - @Test - fun `getSessionId returns the sessionId for RoomCall`() { - assertThat( - CallType.RoomCall( - sessionId = A_SESSION_ID, - roomId = A_ROOM_ID, - isAudioCall = false, - ).getSessionId() - ).isEqualTo(A_SESSION_ID) - } - - @Test - fun `ExternalUrl stringification does not contain the URL`() { - assertThat(CallType.ExternalUrl("aURL").toString()).isEqualTo("ExternalUrl") - } - - @Test - fun `RoomCall stringification does not contain the URL`() { - assertThat(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false).toString()) - .isEqualTo("RoomCall(sessionId=$A_SESSION_ID, roomId=$A_ROOM_ID, isAudioCall=false)") - } -} diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt deleted file mode 100644 index 43f7f931f1..0000000000 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.call.utils - -import com.google.common.truth.Truth.assertThat -import io.element.android.features.call.impl.utils.CallIntentDataParser -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import java.net.URLEncoder - -@RunWith(RobolectricTestRunner::class) -class CallIntentDataParserTest { - private val callIntentDataParser = CallIntentDataParser() - - @Test - fun `a null data returns null`() { - val url: String? = null - assertThat(callIntentDataParser.parse(url)).isNull() - } - - @Test - fun `empty data returns null`() { - doTest("", null) - } - - @Test - fun `invalid data returns null`() { - doTest("!", null) - } - - @Test - fun `data with no scheme returns null`() { - doTest("test", null) - } - - @Test - fun `Element Call http urls returns null`() { - doTest("http://call.element.io", null) - doTest("http://call.element.io/some-actual-call?with=parameters", null) - } - - @Test - fun `Element Call urls with unknown host returns null`() { - // Check valid host first, should not return null - doTest("https://call.element.io", "https://call.element.io#?appPrompt=false&confineToRoom=true") - // Unknown host should return null - doTest("https://unknown.io", null) - doTest("https://call.unknown.io", null) - doTest("https://call.element.com", null) - doTest("https://call.element.io.tld", null) - } - - @Test - fun `Element Call urls will be returned as is`() { - doTest( - url = "https://call.element.io", - expectedResult = "https://call.element.io#?$EXTRA_PARAMS" - ) - } - - @Test - fun `Element Call url with url param gets url extracted`() { - doTest( - url = VALID_CALL_URL_WITH_PARAM, - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" - ) - } - - @Test - fun `HTTP and HTTPS urls that don't come from EC return null`() { - doTest("http://app.element.io", null) - doTest("https://app.element.io", null) - doTest("http://", null) - doTest("https://", null) - } - - @Test - fun `Element Call url with no url returns null`() { - val embeddedUrl = VALID_CALL_URL_WITH_PARAM - val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") - val url = "io.element.call:/?no_url=$encodedUrl" - assertThat(callIntentDataParser.parse(url)).isNull() - } - - @Test - fun `element scheme with no call host returns null`() { - val embeddedUrl = VALID_CALL_URL_WITH_PARAM - val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") - val url = "element://no-call?url=$encodedUrl" - assertThat(callIntentDataParser.parse(url)).isNull() - } - - @Test - fun `element scheme with no data returns null`() { - val url = "element://call?url=" - assertThat(callIntentDataParser.parse(url)).isNull() - } - - @Test - fun `Element Call url with no data returns null`() { - val url = "io.element.call:/?url=" - assertThat(callIntentDataParser.parse(url)).isNull() - } - - @Test - fun `element invalid scheme returns null`() { - val embeddedUrl = VALID_CALL_URL_WITH_PARAM - val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") - val url = "bad.scheme:/?url=$encodedUrl" - assertThat(callIntentDataParser.parse(url)).isNull() - } - - @Test - fun `Element Call url with url extra param appPrompt gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM&appPrompt=true", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" - ) - } - - @Test - fun `Element Call url with url extra param in fragment appPrompt gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=true", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&confineToRoom=true" - ) - } - - @Test - fun `Element Call url with url extra param in fragment appPrompt and other gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=true&otherParam=maybe", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&otherParam=maybe&confineToRoom=true" - ) - } - - @Test - fun `Element Call url with url extra param confineToRoom gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM&confineToRoom=false", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" - ) - } - - @Test - fun `Element Call url with url extra param in fragment confineToRoom gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=false", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=true&appPrompt=false" - ) - } - - @Test - fun `Element Call url with url extra param in fragment confineToRoom and more gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=false&otherParam=maybe", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=true&otherParam=maybe&appPrompt=false" - ) - } - - @Test - fun `Element Call url with url fragment gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#fragment", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#fragment?$EXTRA_PARAMS" - ) - } - - @Test - fun `Element Call url with url fragment with params gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#fragment?otherParam=maybe", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#fragment?otherParam=maybe&$EXTRA_PARAMS" - ) - } - - @Test - fun `Element Call url with url fragment with other params gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#?otherParam=maybe", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?otherParam=maybe&$EXTRA_PARAMS" - ) - } - - @Test - fun `Element Call url with empty fragment`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" - ) - } - - @Test - fun `Element Call url with empty fragment query`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#?", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" - ) - } - - private fun doTest(url: String, expectedResult: String?) { - // Test direct parsing - assertThat(callIntentDataParser.parse(url)).isEqualTo(expectedResult) - - // Test embedded url, scheme 1 - val encodedUrl = URLEncoder.encode(url, "utf-8") - val urlScheme1 = "element://call?url=$encodedUrl" - assertThat(callIntentDataParser.parse(urlScheme1)).isEqualTo(expectedResult) - - // Test embedded url, scheme 2 - val urlScheme2 = "io.element.call:/?url=$encodedUrl" - assertThat(callIntentDataParser.parse(urlScheme2)).isEqualTo(expectedResult) - } - - companion object { - const val VALID_CALL_URL_WITH_PARAM = "https://call.element.io/some-actual-call?with=parameters" - const val EXTRA_PARAMS = "appPrompt=false&confineToRoom=true" - } -} diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt index f9f6206ec7..3712904b03 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt @@ -13,7 +13,7 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.content.getSystemService import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator import io.element.android.features.call.impl.notifications.aCallNotificationData import io.element.android.features.call.impl.utils.ActiveCall @@ -77,7 +77,7 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isEqualTo( ActiveCall( - callType = CallType.RoomCall( + callData = CallData( sessionId = callNotificationData.sessionId, roomId = callNotificationData.roomId, isAudioCall = false, @@ -104,7 +104,7 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isEqualTo( ActiveCall( - callType = CallType.RoomCall( + callData = CallData( sessionId = callNotificationData.sessionId, roomId = callNotificationData.roomId, isAudioCall = true, @@ -132,7 +132,7 @@ class DefaultActiveCallManagerTest { manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2)) assertThat(manager.activeCall.value).isEqualTo(activeCall) - assertThat((manager.activeCall.value?.callType as? CallType.RoomCall)?.roomId).isNotEqualTo(A_ROOM_ID_2) + assertThat(manager.activeCall.value?.callData?.roomId).isNotEqualTo(A_ROOM_ID_2) advanceTimeBy(1) @@ -178,7 +178,7 @@ class DefaultActiveCallManagerTest { } @Test - fun `hangUpCall - removes existing call if the CallType matches`() = runTest { + fun `hangUpCall - removes existing call if the CallData matches`() = runTest { setupShadowPowerManager() val notificationManagerCompat = mockk(relaxed = true) val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) @@ -188,7 +188,7 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isNotNull() assertThat(manager.activeWakeLock?.isHeld).isTrue() - manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false)) + manager.hangUpCall(CallData(notificationData.sessionId, notificationData.roomId, false)) assertThat(manager.activeCall.value).isNull() assertThat(manager.activeWakeLock?.isHeld).isFalse() @@ -215,7 +215,7 @@ class DefaultActiveCallManagerTest { val notificationData = aCallNotificationData(roomId = A_ROOM_ID) manager.registerIncomingCall(notificationData) - manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false)) + manager.hangUpCall(CallData(notificationData.sessionId, notificationData.roomId, false)) coVerify { room.declineCall(notificationEventId = notificationData.eventId) @@ -242,7 +242,7 @@ class DefaultActiveCallManagerTest { val notificationData = aCallNotificationData(roomId = A_ROOM_ID) // Do not register the incoming call, so the manager doesn't know about it manager.hangUpCall( - callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false), + callData = CallData(notificationData.sessionId, notificationData.roomId, false), notificationData = notificationData, ) coVerify { @@ -320,7 +320,7 @@ class DefaultActiveCallManagerTest { } @Test - fun `hangUpCall - does nothing if the CallType doesn't match`() = runTest { + fun `hangUpCall - does nothing if the CallData doesn't match`() = runTest { setupShadowPowerManager() val notificationManagerCompat = mockk(relaxed = true) val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) @@ -329,7 +329,13 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isNotNull() assertThat(manager.activeWakeLock?.isHeld).isTrue() - manager.hangUpCall(CallType.ExternalUrl("https://example.com")) + manager.hangUpCall( + CallData( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID_2, + isAudioCall = true, + ) + ) assertThat(manager.activeCall.value).isNotNull() assertThat(manager.activeWakeLock?.isHeld).isTrue() @@ -344,10 +350,10 @@ class DefaultActiveCallManagerTest { val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) assertThat(manager.activeCall.value).isNull() - manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, true)) + manager.joinedCall(CallData(A_SESSION_ID, A_ROOM_ID, true)) assertThat(manager.activeCall.value).isEqualTo( ActiveCall( - callType = CallType.RoomCall( + callData = CallData( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, isAudioCall = true, @@ -450,7 +456,7 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isEqualTo( ActiveCall( - callType = CallType.RoomCall( + callData = CallData( sessionId = callNotificationData.sessionId, roomId = callNotificationData.roomId, isAudioCall = false, diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt index 2d0e126ab5..c2c38284a9 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt @@ -8,7 +8,7 @@ package io.element.android.features.call.utils -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.impl.notifications.CallNotificationData import io.element.android.features.call.impl.utils.ActiveCall import io.element.android.features.call.impl.utils.ActiveCallManager @@ -17,8 +17,8 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeActiveCallManager( var registerIncomingCallResult: (CallNotificationData) -> Unit = {}, - var hangUpCallResult: (CallType, CallNotificationData?) -> Unit = { _, _ -> }, - var joinedCallResult: (CallType) -> Unit = {}, + var hangUpCallResult: (CallData, CallNotificationData?) -> Unit = { _, _ -> }, + var joinedCallResult: (CallData) -> Unit = {}, ) : ActiveCallManager { override val activeCall = MutableStateFlow(null) @@ -26,12 +26,12 @@ class FakeActiveCallManager( registerIncomingCallResult(notificationData) } - override suspend fun hangUpCall(callType: CallType, notificationData: CallNotificationData?) = simulateLongTask { - hangUpCallResult(callType, notificationData) + override suspend fun hangUpCall(callData: CallData, notificationData: CallNotificationData?) = simulateLongTask { + hangUpCallResult(callData, notificationData) } - override suspend fun joinedCall(callType: CallType) = simulateLongTask { - joinedCallResult(callType) + override suspend fun joinedCall(callData: CallData) = simulateLongTask { + joinedCallResult(callData) } fun setActiveCall(value: ActiveCall?) { diff --git a/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt index fdf3ca566b..13b61feacb 100644 --- a/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt +++ b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt @@ -8,16 +8,16 @@ package io.element.android.features.call.test -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.tests.testutils.lambda.lambdaError class FakeElementCallEntryPoint( - var startCallResult: (CallType) -> Unit = { lambdaError() }, + var startCallResult: (CallData) -> Unit = { lambdaError() }, var handleIncomingCallResult: ( - CallType.RoomCall, + CallData, EventId, UserId, String?, @@ -27,12 +27,12 @@ class FakeElementCallEntryPoint( String?, ) -> Unit = { _, _, _, _, _, _, _, _ -> lambdaError() } ) : ElementCallEntryPoint { - override fun startCall(callType: CallType) { - startCallResult(callType) + override fun startCall(callData: CallData) { + startCallResult(callData) } override suspend fun handleIncomingCall( - callType: CallType.RoomCall, + callData: CallData, eventId: EventId, senderId: UserId, roomName: String?, @@ -44,7 +44,7 @@ class FakeElementCallEntryPoint( textContent: String?, ) { handleIncomingCallResult( - callType, + callData, eventId, senderId, roomName, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 36e94ec456..c7b046dc14 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -24,7 +24,7 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.Interaction import io.element.android.annotations.ContributesNode -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint @@ -277,13 +277,13 @@ class MessagesFlowNode( } override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) { - val callType = CallType.RoomCall( + val callData = CallData( sessionId = sessionId, roomId = roomId, - isAudioCall = isAudioCall + isAudioCall = isAudioCall, ) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) - elementCallEntryPoint.startCall(callType) + elementCallEntryPoint.startCall(callData) } override fun navigateToPinnedMessagesList() { @@ -506,13 +506,13 @@ class MessagesFlowNode( } override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) { - val callType = CallType.RoomCall( + val callData = CallData( sessionId = sessionId, roomId = roomId, isAudioCall = isAudioCall ) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) - elementCallEntryPoint.startCall(callType) + elementCallEntryPoint.startCall(callData) } override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index c3ae902ba9..818287ab68 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -25,7 +25,7 @@ import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.Interaction import io.element.android.annotations.ContributesNode import io.element.android.appconfig.LearnMoreConfig -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint @@ -225,13 +225,13 @@ class RoomDetailsFlowNode( } override fun navigateToRoomCall(callIntent: CallIntent) { - val inputs = CallType.RoomCall( + val callData = CallData( sessionId = room.sessionId, roomId = room.roomId, isAudioCall = callIntent == CallIntent.AUDIO ) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) - elementCallEntryPoint.startCall(inputs) + elementCallEntryPoint.startCall(callData) } override fun navigateToReportRoom() { @@ -288,7 +288,7 @@ class RoomDetailsFlowNode( override fun startCall(dmRoomId: RoomId, callIntent: CallIntent) { elementCallEntryPoint.startCall( - CallType.RoomCall( + CallData( roomId = dmRoomId, sessionId = room.sessionId, isAudioCall = callIntent == CallIntent.AUDIO diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt index aaafbe04be..aff9e3502d 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt @@ -20,7 +20,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.userprofile.api.UserProfileEntryPoint import io.element.android.features.userprofile.impl.root.UserProfileNode @@ -86,7 +86,7 @@ class UserProfileFlowNode( override fun startCall(dmRoomId: RoomId, callIntent: CallIntent) { elementCallEntryPoint.startCall( - CallType.RoomCall( + CallData( sessionId = sessionId, roomId = dmRoomId, isAudioCall = callIntent == CallIntent.AUDIO diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResultProcessor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResultProcessor.kt index 0dd2761446..a97937f5d0 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResultProcessor.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResultProcessor.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.push.impl.notifications import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.matrix.api.core.EventId @@ -215,9 +215,9 @@ class DefaultNotificationResultProcessor( private suspend fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) { Timber.i("## handleInternal() : Incoming call.") elementCallEntryPoint.handleIncomingCall( - callType = CallType.RoomCall( - notifiableEvent.sessionId, - notifiableEvent.roomId, + callData = CallData( + sessionId = notifiableEvent.sessionId, + roomId = notifiableEvent.roomId, isAudioCall = notifiableEvent.callIntent == CallIntent.AUDIO ), eventId = notifiableEvent.eventId, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationResultProcessorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationResultProcessorTest.kt index 6acb375bac..91f29dd28e 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationResultProcessorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationResultProcessorTest.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.push.impl.notifications import com.google.common.truth.Truth.assertThat -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.test.FakeElementCallEntryPoint import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -104,7 +104,7 @@ class DefaultNotificationResultProcessorTest { @Test fun `when ringing call PushData is received, the incoming call will be handled`() = runTest { val handleIncomingCallLambda = lambdaRecorder< - CallType.RoomCall, + CallData, EventId, UserId, String?, @@ -140,7 +140,7 @@ class DefaultNotificationResultProcessorTest { fun `when notify call PushData is received, the incoming call will be treated as a normal notification`() = runTest { val onNotifiableEventsReceived = lambdaRecorder, Unit> {} val handleIncomingCallLambda = lambdaRecorder< - CallType.RoomCall, + CallData, EventId, UserId, String?, @@ -176,7 +176,7 @@ class DefaultNotificationResultProcessorTest { fun `when notify call PushData is received, the incoming call will be treated as a normal notification even if notification are disabled`() = runTest { val onNotifiableEventsReceived = lambdaRecorder, Unit> {} val handleIncomingCallLambda = lambdaRecorder< - CallType.RoomCall, + CallData, EventId, UserId, String?, diff --git a/tools/adb/callLinkCustomScheme.sh b/tools/adb/callLinkCustomScheme.sh deleted file mode 100755 index 7e6c9f02d3..0000000000 --- a/tools/adb/callLinkCustomScheme.sh +++ /dev/null @@ -1,14 +0,0 @@ -#! /bin/bash - -# Copyright (c) 2025 Element Creations Ltd. -# Copyright 2023-2024 New Vector Ltd. -# -# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. -# Please see LICENSE files in the repository root for full details. - -# Format is: -# element://call?url=some-encoded-url -# For instance -# element://call?url=https%3A%2F%2Fcall.element.io%2FTestElementCall - -adb shell am start -a android.intent.action.VIEW -d element://call?url=https%3A%2F%2Fcall.element.io%2FTestElementCall diff --git a/tools/adb/callLinkCustomScheme2.sh b/tools/adb/callLinkCustomScheme2.sh deleted file mode 100755 index 43f427f22f..0000000000 --- a/tools/adb/callLinkCustomScheme2.sh +++ /dev/null @@ -1,14 +0,0 @@ -#! /bin/bash - -# Copyright (c) 2025 Element Creations Ltd. -# Copyright 2023-2024 New Vector Ltd. -# -# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. -# Please see LICENSE files in the repository root for full details. - -# Format is: -# io.element.call:/?url=some-encoded-url -# For instance -# io.element.call:/?url=https%3A%2F%2Fcall.element.io%2FTestElementCall - -adb shell am start -a android.intent.action.VIEW -d io.element.call:/?url=https%3A%2F%2Fcall.element.io%2FTestElementCall From 2bcf10dd0bb78fe34309e76c0fca3f4e68dad5c9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Apr 2026 17:04:12 +0200 Subject: [PATCH 03/21] `CallScreenEvents` -> `CallScreenEvent` --- .../ui/{CallScreenEvents.kt => CallScreenEvent.kt} | 8 ++++---- .../features/call/impl/ui/CallScreenPresenter.kt | 8 ++++---- .../features/call/impl/ui/CallScreenState.kt | 2 +- .../call/impl/ui/CallScreenStateProvider.kt | 2 +- .../features/call/impl/ui/CallScreenView.kt | 10 +++++----- .../features/call/impl/ui/ElementCallActivity.kt | 6 +++--- .../features/call/ui/CallScreenPresenterTest.kt | 14 +++++++------- 7 files changed, 25 insertions(+), 25 deletions(-) rename features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/{CallScreenEvents.kt => CallScreenEvent.kt} (78%) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvent.kt similarity index 78% rename from features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvent.kt index 8fbbce896f..357559c3f9 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvent.kt @@ -10,8 +10,8 @@ package io.element.android.features.call.impl.ui import io.element.android.features.call.impl.utils.WidgetMessageInterceptor -sealed interface CallScreenEvents { - data object Hangup : CallScreenEvents - data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvents - data class OnWebViewError(val description: String?) : CallScreenEvents +sealed interface CallScreenEvent { + data object Hangup : CallScreenEvent + data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvent + data class OnWebViewError(val description: String?) : CallScreenEvent } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt index b9bd6640b4..7d8e20967f 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt @@ -152,9 +152,9 @@ class CallScreenPresenter( } } - fun handleEvent(event: CallScreenEvents) { + fun handleEvent(event: CallScreenEvent) { when (event) { - is CallScreenEvents.Hangup -> { + is CallScreenEvent.Hangup -> { val widgetId = callWidgetDriver.value?.id val interceptor = messageInterceptor.value if (widgetId != null && interceptor != null && isWidgetLoaded) { @@ -174,10 +174,10 @@ class CallScreenPresenter( } } } - is CallScreenEvents.SetupMessageChannels -> { + is CallScreenEvent.SetupMessageChannels -> { messageInterceptor.value = event.widgetMessageInterceptor } - is CallScreenEvents.OnWebViewError -> { + is CallScreenEvent.OnWebViewError -> { if (!ignoreWebViewError) { webViewError = event.description.orEmpty() } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt index 3608a2b620..86b4cc439f 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt @@ -15,5 +15,5 @@ data class CallScreenState( val webViewError: String?, val userAgent: String, val isCallActive: Boolean, - val eventSink: (CallScreenEvents) -> Unit, + val eventSink: (CallScreenEvent) -> Unit, ) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt index 036bb6103a..155c5d3380 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt @@ -26,7 +26,7 @@ internal fun aCallScreenState( webViewError: String? = null, userAgent: String = "", isCallActive: Boolean = true, - eventSink: (CallScreenEvents) -> Unit = {}, + eventSink: (CallScreenEvent) -> Unit = {}, ): CallScreenState { return CallScreenState( urlState = urlState, diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt index f8657a9ece..a945f3c844 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt @@ -68,7 +68,7 @@ internal fun CallScreenView( if (pipState.supportPip) { pipState.eventSink.invoke(PictureInPictureEvents.EnterPictureInPicture) } else { - state.eventSink(CallScreenEvents.Hangup) + state.eventSink(CallScreenEvent.Hangup) } } @@ -84,7 +84,7 @@ internal fun CallScreenView( append(stringResource(CommonStrings.error_unknown)) state.webViewError.takeIf { it.isNotEmpty() }?.let { append("\n\n").append(it) } }, - onSubmit = { state.eventSink(CallScreenEvents.Hangup) }, + onSubmit = { state.eventSink(CallScreenEvent.Hangup) }, ) } else { var webViewAudioManager by remember { mutableStateOf(null) } @@ -123,14 +123,14 @@ internal fun CallScreenView( Timber.d("Can't start in-call audio mode since the app is already in it.") } }, - onError = { state.eventSink(CallScreenEvents.OnWebViewError(it)) }, + onError = { state.eventSink(CallScreenEvent.OnWebViewError(it)) }, ) webViewAudioManager = WebViewAudioManager( webView = webView, coroutineScope = coroutineScope, onInvalidAudioDeviceAdded = { invalidAudioDeviceReason = it }, ) - state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor)) + state.eventSink(CallScreenEvent.SetupMessageChannels(interceptor)) val pipController = WebViewPipController(webView) pipState.eventSink(PictureInPictureEvents.SetPipController(pipController)) }, @@ -147,7 +147,7 @@ internal fun CallScreenView( Timber.e(state.urlState.error, "WebView failed to load URL: ${state.urlState.error.message}") ErrorDialog( content = state.urlState.error.message.orEmpty(), - onSubmit = { state.eventSink(CallScreenEvents.Hangup) }, + onSubmit = { state.eventSink(CallScreenEvent.Hangup) }, ) } is AsyncData.Success -> Unit diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt index dddcfceb50..bce882ec2d 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt @@ -79,7 +79,7 @@ class ElementCallActivity : private val webViewTarget = mutableStateOf(null) - private var eventSink: ((CallScreenEvents) -> Unit)? = null + private var eventSink: ((CallScreenEvent) -> Unit)? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -172,7 +172,7 @@ class ElementCallActivity : pipEventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(isInPictureInPictureMode)) if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { Timber.tag(loggerTag.value).d("Exiting PiP mode: Hangup the call") - eventSink?.invoke(CallScreenEvents.Hangup) + eventSink?.invoke(CallScreenEvent.Hangup) } } addOnPictureInPictureModeChangedListener(onPictureInPictureModeChangedListener) @@ -280,7 +280,7 @@ class ElementCallActivity : } override fun hangUp() { - eventSink?.invoke(CallScreenEvents.Hangup) + eventSink?.invoke(CallScreenEvent.Hangup) } } diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt index c2c576999c..99f10b46fc 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt @@ -14,7 +14,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.features.call.api.CallData -import io.element.android.features.call.impl.ui.CallScreenEvents +import io.element.android.features.call.impl.ui.CallScreenEvent import io.element.android.features.call.impl.ui.CallScreenNavigator import io.element.android.features.call.impl.ui.CallScreenPresenter import io.element.android.features.call.impl.utils.WidgetMessageSerializer @@ -109,7 +109,7 @@ class CallScreenPresenterTest { advanceTimeBy(1.seconds) val initialState = awaitItem() - initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvent.SetupMessageChannels(messageInterceptor)) // And incoming message from the Widget Driver is passed to the WebView widgetDriver.givenIncomingMessage("A message") @@ -143,9 +143,9 @@ class CallScreenPresenterTest { // Give it time to load the URL and WidgetDriver advanceTimeBy(1.seconds) - initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvent.SetupMessageChannels(messageInterceptor)) - initialState.eventSink(CallScreenEvents.Hangup) + initialState.eventSink(CallScreenEvent.Hangup) // Let background coroutines run and the widget drive be received runCurrent() @@ -177,7 +177,7 @@ class CallScreenPresenterTest { // Give it time to load the URL and WidgetDriver advanceTimeBy(1.seconds) - initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvent.SetupMessageChannels(messageInterceptor)) messageInterceptor.givenInterceptedMessage("""{"action":"io.element.close","api":"fromWidget","widgetId":"1","requestId":"1"}""") @@ -212,7 +212,7 @@ class CallScreenPresenterTest { skipItems(2) val initialState = awaitItem() assertThat(initialState.isCallActive).isFalse() - initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvent.SetupMessageChannels(messageInterceptor)) messageInterceptor.givenInterceptedMessage( """ { @@ -249,7 +249,7 @@ class CallScreenPresenterTest { skipItems(2) val initialState = awaitItem() assertThat(initialState.isCallActive).isFalse() - initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvent.SetupMessageChannels(messageInterceptor)) skipItems(2) // Wait for the timeout to trigger From 944d8965f66989f92c2a03653debb82bfd987899 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Apr 2026 17:04:49 +0200 Subject: [PATCH 04/21] `PictureInPictureEvents` -> `PictureInPictureEvent` --- ...ctureEvents.kt => PictureInPictureEvent.kt} | 8 ++++---- .../call/impl/pip/PictureInPicturePresenter.kt | 8 ++++---- .../call/impl/pip/PictureInPictureState.kt | 2 +- .../impl/pip/PictureInPictureStateProvider.kt | 2 +- .../features/call/impl/ui/CallScreenView.kt | 6 +++--- .../call/impl/ui/ElementCallActivity.kt | 6 +++--- .../impl/pip/PictureInPicturePresenterTest.kt | 18 +++++++++--------- 7 files changed, 25 insertions(+), 25 deletions(-) rename features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/{PictureInPictureEvents.kt => PictureInPictureEvent.kt} (75%) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvents.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvent.kt similarity index 75% rename from features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvents.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvent.kt index 9522d44b22..1af13e148e 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvents.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvent.kt @@ -10,8 +10,8 @@ package io.element.android.features.call.impl.pip import io.element.android.features.call.impl.utils.PipController -sealed interface PictureInPictureEvents { - data class SetPipController(val pipController: PipController) : PictureInPictureEvents - data object EnterPictureInPicture : PictureInPictureEvents - data class OnPictureInPictureModeChanged(val isInPip: Boolean) : PictureInPictureEvents +sealed interface PictureInPictureEvent { + data class SetPipController(val pipController: PipController) : PictureInPictureEvent + data object EnterPictureInPicture : PictureInPictureEvent + data class OnPictureInPictureModeChanged(val isInPip: Boolean) : PictureInPictureEvent } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt index 5125b464dc..2dc16d7f26 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt @@ -36,17 +36,17 @@ class PictureInPicturePresenter( var isInPictureInPicture by remember { mutableStateOf(false) } var pipController by remember { mutableStateOf(null) } - fun handleEvent(event: PictureInPictureEvents) { + fun handleEvent(event: PictureInPictureEvent) { when (event) { - is PictureInPictureEvents.SetPipController -> { + is PictureInPictureEvent.SetPipController -> { pipController = event.pipController } - PictureInPictureEvents.EnterPictureInPicture -> { + PictureInPictureEvent.EnterPictureInPicture -> { coroutineScope.launch { switchToPip(pipController) } } - is PictureInPictureEvents.OnPictureInPictureModeChanged -> { + is PictureInPictureEvent.OnPictureInPictureModeChanged -> { Timber.tag(loggerTag.value).d("onPictureInPictureModeChanged: ${event.isInPip}") isInPictureInPicture = event.isInPip if (event.isInPip) { diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureState.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureState.kt index b1fef4f28b..108589edb9 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureState.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureState.kt @@ -11,5 +11,5 @@ package io.element.android.features.call.impl.pip data class PictureInPictureState( val supportPip: Boolean, val isInPictureInPicture: Boolean, - val eventSink: (PictureInPictureEvents) -> Unit, + val eventSink: (PictureInPictureEvent) -> Unit, ) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureStateProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureStateProvider.kt index 6324820eec..f4a78294b6 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureStateProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureStateProvider.kt @@ -11,7 +11,7 @@ package io.element.android.features.call.impl.pip fun aPictureInPictureState( supportPip: Boolean = false, isInPictureInPicture: Boolean = false, - eventSink: (PictureInPictureEvents) -> Unit = {}, + eventSink: (PictureInPictureEvent) -> Unit = {}, ): PictureInPictureState { return PictureInPictureState( supportPip = supportPip, diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt index a945f3c844..1c68a62f55 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.viewinterop.AndroidView import io.element.android.features.call.impl.R -import io.element.android.features.call.impl.pip.PictureInPictureEvents +import io.element.android.features.call.impl.pip.PictureInPictureEvent import io.element.android.features.call.impl.pip.PictureInPictureState import io.element.android.features.call.impl.pip.aPictureInPictureState import io.element.android.features.call.impl.utils.InvalidAudioDeviceReason @@ -66,7 +66,7 @@ internal fun CallScreenView( ) { fun handleBack() { if (pipState.supportPip) { - pipState.eventSink.invoke(PictureInPictureEvents.EnterPictureInPicture) + pipState.eventSink.invoke(PictureInPictureEvent.EnterPictureInPicture) } else { state.eventSink(CallScreenEvent.Hangup) } @@ -132,7 +132,7 @@ internal fun CallScreenView( ) state.eventSink(CallScreenEvent.SetupMessageChannels(interceptor)) val pipController = WebViewPipController(webView) - pipState.eventSink(PictureInPictureEvents.SetPipController(pipController)) + pipState.eventSink(PictureInPictureEvent.SetPipController(pipController)) }, onDestroyWebView = { // Reset audio mode diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt index bce882ec2d..367328ed10 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt @@ -38,7 +38,7 @@ import io.element.android.compound.colors.SemanticColorsLightDark import io.element.android.features.call.api.CallData import io.element.android.features.call.impl.DefaultElementCallEntryPoint import io.element.android.features.call.impl.di.CallBindings -import io.element.android.features.call.impl.pip.PictureInPictureEvents +import io.element.android.features.call.impl.pip.PictureInPictureEvent import io.element.android.features.call.impl.pip.PictureInPicturePresenter import io.element.android.features.call.impl.pip.PictureInPictureState import io.element.android.features.call.impl.pip.PipView @@ -159,7 +159,7 @@ class ElementCallActivity : if (requestPermissionCallback != null) { Timber.tag(loggerTag.value).w("Ignoring onUserLeaveHint event because user is asked to grant permissions") } else { - pipEventSink(PictureInPictureEvents.EnterPictureInPicture) + pipEventSink(PictureInPictureEvent.EnterPictureInPicture) } } addOnUserLeaveHintListener(listener) @@ -169,7 +169,7 @@ class ElementCallActivity : } DisposableEffect(Unit) { val onPictureInPictureModeChangedListener = Consumer { _: PictureInPictureModeChangedInfo -> - pipEventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(isInPictureInPictureMode)) + pipEventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(isInPictureInPictureMode)) if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { Timber.tag(loggerTag.value).d("Exiting PiP mode: Hangup the call") eventSink?.invoke(CallScreenEvent.Hangup) diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt index c087fa3c35..97aa63947b 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt @@ -58,13 +58,13 @@ class PictureInPicturePresenterTest { }.test { val initialState = awaitItem() assertThat(initialState.isInPictureInPicture).isFalse() - initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture) + initialState.eventSink(PictureInPictureEvent.EnterPictureInPicture) enterPipModeResult.assertions().isCalledOnce() - initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(true)) + initialState.eventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(true)) val pipState = awaitItem() assertThat(pipState.isInPictureInPicture).isTrue() // User stops pip - initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(false)) + initialState.eventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(false)) val finalState = awaitItem() assertThat(finalState.isInPictureInPicture).isFalse() } @@ -84,8 +84,8 @@ class PictureInPicturePresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink(PictureInPictureEvents.SetPipController(FakePipController(canEnterPipResult = { false }))) - initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture) + initialState.eventSink(PictureInPictureEvent.SetPipController(FakePipController(canEnterPipResult = { false }))) + initialState.eventSink(PictureInPictureEvent.EnterPictureInPicture) handUpResult.assertions().isCalledOnce() } } @@ -107,7 +107,7 @@ class PictureInPicturePresenterTest { }.test { val initialState = awaitItem() initialState.eventSink( - PictureInPictureEvents.SetPipController( + PictureInPictureEvent.SetPipController( FakePipController( canEnterPipResult = { true }, enterPipResult = enterPipResult, @@ -115,16 +115,16 @@ class PictureInPicturePresenterTest { ) ) ) - initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture) + initialState.eventSink(PictureInPictureEvent.EnterPictureInPicture) enterPipModeResult.assertions().isCalledOnce() enterPipResult.assertions().isNeverCalled() - initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(true)) + initialState.eventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(true)) val pipState = awaitItem() assertThat(pipState.isInPictureInPicture).isTrue() enterPipResult.assertions().isCalledOnce() // User stops pip exitPipResult.assertions().isNeverCalled() - initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(false)) + initialState.eventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(false)) val finalState = awaitItem() assertThat(finalState.isInPictureInPicture).isFalse() exitPipResult.assertions().isCalledOnce() From 4bb33fc36a6cb5b7238378b663214169fcaf512d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Apr 2026 17:06:49 +0200 Subject: [PATCH 05/21] Use test extension on presenters. --- .../impl/pip/PictureInPicturePresenterTest.kt | 24 +++++------------- .../call/ui/CallScreenPresenterTest.kt | 25 ++++++------------- 2 files changed, 13 insertions(+), 36 deletions(-) diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt index 97aa63947b..c3d7fdf17b 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt @@ -8,11 +8,9 @@ package io.element.android.features.call.impl.pip -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test import kotlinx.coroutines.test.runTest import org.junit.Test @@ -20,9 +18,7 @@ class PictureInPicturePresenterTest { @Test fun `when pip is not supported, the state value supportPip is false`() = runTest { val presenter = createPictureInPicturePresenter(supportPip = false) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() assertThat(initialState.supportPip).isFalse() } @@ -35,9 +31,7 @@ class PictureInPicturePresenterTest { supportPip = true, pipView = FakePipView(setPipParamsResult = { }), ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() assertThat(initialState.supportPip).isTrue() } @@ -53,9 +47,7 @@ class PictureInPicturePresenterTest { enterPipModeResult = enterPipModeResult, ), ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() assertThat(initialState.isInPictureInPicture).isFalse() initialState.eventSink(PictureInPictureEvent.EnterPictureInPicture) @@ -80,9 +72,7 @@ class PictureInPicturePresenterTest { handUpResult = handUpResult ), ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() initialState.eventSink(PictureInPictureEvent.SetPipController(FakePipController(canEnterPipResult = { false }))) initialState.eventSink(PictureInPictureEvent.EnterPictureInPicture) @@ -102,9 +92,7 @@ class PictureInPicturePresenterTest { enterPipModeResult = enterPipModeResult ), ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() initialState.eventSink( PictureInPictureEvent.SetPipController( diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt index 99f10b46fc..276e6670f1 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt @@ -39,6 +39,7 @@ import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancelAndJoin @@ -71,9 +72,7 @@ class CallScreenPresenterTest { screenTracker = FakeScreenTracker(analyticsLambda), activeCallManager = FakeActiveCallManager(joinedCallResult = joinedCallLambda), ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { // Wait until the URL is loaded advanceTimeBy(1.seconds) skipItems(1) @@ -102,9 +101,7 @@ class CallScreenPresenterTest { screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { // Give it time to load the URL and WidgetDriver advanceTimeBy(1.seconds) @@ -135,9 +132,7 @@ class CallScreenPresenterTest { screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() // Give it time to load the URL and WidgetDriver @@ -169,9 +164,7 @@ class CallScreenPresenterTest { screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() // Give it time to load the URL and WidgetDriver @@ -204,9 +197,7 @@ class CallScreenPresenterTest { screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { // Give it time to load the URL and WidgetDriver advanceTimeBy(1.seconds) skipItems(2) @@ -241,9 +232,7 @@ class CallScreenPresenterTest { screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { // Give it time to load the URL and WidgetDriver advanceTimeBy(1.seconds) skipItems(2) From 723b7486bcd8742f4c1b1d9aa1cff003adce3f07 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 28 Apr 2026 15:19:03 +0100 Subject: [PATCH 06/21] Improve detection of completion for Link new device flow The SDK emits a Done progress once complete, but our listener might have been deallocated before receiving the done. --- .../features/linknewdevice/impl/LinkNewDeviceFlowNode.kt | 5 +---- .../matrix/impl/linknewdevice/RustLinkDesktopHandler.kt | 2 ++ .../matrix/impl/linknewdevice/RustLinkMobileHandler.kt | 2 ++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt index 54baee6663..c48678f96c 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt @@ -145,10 +145,7 @@ class LinkNewDeviceFlowNode( LinkMobileStep.Starting -> { // This step is not received at the moment, so do nothing } - LinkMobileStep.SyncingSecrets -> { - // LinkMobileStep.Done is not received at the moment, so consider that the flow is done here - callback.onDone() - } + LinkMobileStep.SyncingSecrets -> Unit is LinkMobileStep.WaitingForAuth -> { navigateToBrowser(linkMobileStep.verificationUri) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandler.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandler.kt index 211bdc3d4e..83e657eb07 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandler.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandler.kt @@ -54,6 +54,8 @@ class RustLinkDesktopHandler( } } ) + // We emit Done in case the progress listener was deallocated before scan() sent the Done + _linkDesktopStep.emit(LinkDesktopStep.Done) } catch (e: QrCodeDecodeException) { Timber.tag(tag.value).w(e, "Invalid QR code scanned") _linkDesktopStep.emit( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandler.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandler.kt index 0189987d96..6d212d4784 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandler.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandler.kt @@ -49,6 +49,8 @@ class RustLinkMobileHandler( } } ) + // We emit Done in case the progress listener was deallocated before generate() sent the Done + _linkMobileStep.emit(LinkMobileStep.Done) } catch (e: HumanQrGrantLoginException) { Timber.tag(tag.value).w(e, "Error during QR login grant") _linkMobileStep.emit(LinkMobileStep.Error(e.map())) From 3b21d698eefee5a427b8f0cd6f6352ed923b2b99 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 29 Apr 2026 12:05:16 +0100 Subject: [PATCH 07/21] Fix tests Co-Authored-By: Benoit Marty <3940906+bmarty@users.noreply.github.com> --- .../FakeFfiGrantLoginWithQrCodeHandler.kt | 4 +- .../RustLinkDesktopHandlerTest.kt | 39 ++++++++++++++++++- .../RustLinkMobileHandlerTest.kt | 39 ++++++++++++++++++- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiGrantLoginWithQrCodeHandler.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiGrantLoginWithQrCodeHandler.kt index cd0733695b..0899b16325 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiGrantLoginWithQrCodeHandler.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiGrantLoginWithQrCodeHandler.kt @@ -16,8 +16,8 @@ import org.matrix.rustcomponents.sdk.NoHandle import org.matrix.rustcomponents.sdk.QrCodeData class FakeFfiGrantLoginWithQrCodeHandler( - private val generateResult: () -> Unit = {}, - private val scanResult: (QrCodeData) -> Unit = {}, + private val generateResult: suspend () -> Unit = {}, + private val scanResult: suspend (QrCodeData) -> Unit = {}, ) : GrantLoginWithQrCodeHandler(NoHandle) { private var generateProgressListener: GrantGeneratedQrLoginProgressListener? = null private var scanProgressListener: GrantQrLoginProgressListener? = null diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandlerTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandlerTest.kt index a180e4d515..fd635e2da6 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandlerTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandlerTest.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.linknewdevice.ErrorType import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopStep import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiGrantLoginWithQrCodeHandler import io.element.android.libraries.matrix.test.QR_CODE_DATA +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher @@ -29,7 +30,13 @@ import org.matrix.rustcomponents.sdk.QrCodeDecodeException class RustLinkDesktopHandlerTest { @Test fun `handleScannedQrCode function works as expected`() = runTest { - val handler = FakeFfiGrantLoginWithQrCodeHandler() + val completable = CompletableDeferred() + val handler = FakeFfiGrantLoginWithQrCodeHandler( + scanResult = { + // Ensure that the coroutine is hold + completable.await() + } + ) val sut = createRustLinkDesktopHandler( handler, ) @@ -53,6 +60,36 @@ class RustLinkDesktopHandlerTest { handler.emitScanProgress(progress) assertThat(awaitItem()).isEqualTo(expectedStep) } + // scan returns, no new event is emitted + completable.complete(Unit) + expectNoEvents() + } + } + + @Test + fun `when scan does not emits the Done state, the code emits it`() = runTest { + val completable = CompletableDeferred() + val handler = FakeFfiGrantLoginWithQrCodeHandler( + scanResult = { + // Ensure that the coroutine is hold + completable.await() + } + ) + val sut = createRustLinkDesktopHandler( + handler, + ) + sut.linkDesktopStep.test { + val initialItem = awaitItem() + assertThat(initialItem).isEqualTo(LinkDesktopStep.Uninitialized) + backgroundScope.launch { + sut.handleScannedQrCode(QR_CODE_DATA) + } + runCurrent() + handler.emitScanProgress(GrantQrLoginProgress.Starting) + assertThat(awaitItem()).isEqualTo(LinkDesktopStep.Starting) + // scan returns, Done event is emitted + completable.complete(Unit) + assertThat(awaitItem()).isEqualTo(LinkDesktopStep.Done) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandlerTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandlerTest.kt index aa13996e8a..9f71cb7169 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandlerTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandlerTest.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiCheckCodeS import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiGrantLoginWithQrCodeHandler import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiQrCodeData import io.element.android.libraries.matrix.test.QR_CODE_DATA_RECIPROCATE +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher @@ -30,7 +31,13 @@ import org.matrix.rustcomponents.sdk.HumanQrGrantLoginException class RustLinkMobileHandlerTest { @Test fun `start function works as expected`() = runTest { - val handler = FakeFfiGrantLoginWithQrCodeHandler() + val completable = CompletableDeferred() + val handler = FakeFfiGrantLoginWithQrCodeHandler( + generateResult = { + // Ensure that the coroutine is hold + completable.await() + } + ) val sut = createRustLinkMobileHandler( handler, ) @@ -56,6 +63,36 @@ class RustLinkMobileHandlerTest { handler.emitGenerateProgress(progress) assertThat(awaitItem()).isInstanceOf(expectedStepClass) } + // generate returns, no new event is emitted + completable.complete(Unit) + expectNoEvents() + } + } + + @Test + fun `when generates does not emits the Done state, the code emits it`() = runTest { + val completable = CompletableDeferred() + val handler = FakeFfiGrantLoginWithQrCodeHandler( + generateResult = { + // Ensure that the coroutine is hold + completable.await() + } + ) + val sut = createRustLinkMobileHandler( + handler, + ) + sut.linkMobileStep.test { + val initialItem = awaitItem() + assertThat(initialItem).isEqualTo(LinkMobileStep.Uninitialized) + backgroundScope.launch { + sut.start() + } + runCurrent() + handler.emitGenerateProgress(GrantGeneratedQrLoginProgress.Starting) + assertThat(awaitItem()).isEqualTo(LinkMobileStep.Starting) + // generate returns, Done event is emitted + completable.complete(Unit) + assertThat(awaitItem()).isEqualTo(LinkMobileStep.Done) } } From 6ba4679908ae1a3c7b0f698c10fda649aff9f8a5 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Thu, 30 Apr 2026 00:08:33 +0800 Subject: [PATCH 08/21] Change native back button behavior in EC view (close settings in EC with os native back) (#6642) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change native back button behavior in EC view: - inject escape into webview instead of going back. - the webview will call back when no other modal is open. * call down and up in the webview + make sure that we fall back to close pip in case the webview did not handle the esc action. --------- Co-authored-by: Jorge Martín --- .../call/impl/ui/CallScreenBackPressPolicy.kt | 26 +++ .../features/call/impl/ui/CallScreenView.kt | 36 +++-- .../call/ui/CallScreenBackPressPolicyTest.kt | 96 +++++++++++ .../features/call/ui/CallScreenViewTest.kt | 150 ++++++++++++++++++ 4 files changed, 298 insertions(+), 10 deletions(-) create mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenBackPressPolicy.kt create mode 100644 features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenBackPressPolicyTest.kt create mode 100644 features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenBackPressPolicy.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenBackPressPolicy.kt new file mode 100644 index 0000000000..cd47cd8bb1 --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenBackPressPolicy.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.call.impl.ui +internal sealed interface CallScreenBackPressAction { + data object DispatchEscapeToWebView : CallScreenBackPressAction + data object EnterPictureInPicture : CallScreenBackPressAction +} + +internal object CallScreenBackPressPolicy { + fun resolve( + supportPip: Boolean, + hasWebView: Boolean, + fromNative: Boolean, + ): CallScreenBackPressAction? { + return when { + hasWebView && fromNative -> CallScreenBackPressAction.DispatchEscapeToWebView + hasWebView && supportPip -> CallScreenBackPressAction.EnterPictureInPicture + else -> null + } + } +} diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt index f8657a9ece..a54b726a78 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt @@ -64,11 +64,15 @@ internal fun CallScreenView( requestPermissions: (Array, RequestPermissionCallback) -> Unit, modifier: Modifier = Modifier, ) { - fun handleBack() { - if (pipState.supportPip) { - pipState.eventSink.invoke(PictureInPictureEvents.EnterPictureInPicture) - } else { - state.eventSink(CallScreenEvents.Hangup) + var callWebView by remember { mutableStateOf(null) } + + fun handleBack(fromNative: Boolean = false) { + when (CallScreenBackPressPolicy.resolve(supportPip = pipState.supportPip, hasWebView = callWebView != null, fromNative)) { + CallScreenBackPressAction.EnterPictureInPicture -> + pipState.eventSink(PictureInPictureEvents.EnterPictureInPicture) + CallScreenBackPressAction.DispatchEscapeToWebView -> + callWebView?.dispatchEscKeyEvent() + null -> Timber.d("Back press with unsupported pip is a no-op") } } @@ -76,7 +80,7 @@ internal fun CallScreenView( modifier = modifier, ) { padding -> BackHandler { - handleBack() + handleBack(fromNative = true) } if (state.webViewError != null) { ErrorDialog( @@ -111,6 +115,7 @@ internal fun CallScreenView( }, onConsoleMessage = onConsoleMessage, onCreateWebView = { webView -> + callWebView = webView webView.addBackHandler(onBackPressed = ::handleBack) val interceptor = WebViewWidgetMessageInterceptor( webView = webView, @@ -135,6 +140,7 @@ internal fun CallScreenView( pipState.eventSink(PictureInPictureEvents.SetPipController(pipController)) }, onDestroyWebView = { + callWebView = null // Reset audio mode webViewAudioManager?.onCallStopped() } @@ -143,6 +149,7 @@ internal fun CallScreenView( AsyncData.Uninitialized, is AsyncData.Loading -> ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait)) + is AsyncData.Failure -> { Timber.e(state.urlState.error, "WebView failed to load URL: ${state.urlState.error.message}") ErrorDialog( @@ -150,6 +157,7 @@ internal fun CallScreenView( onSubmit = { state.eventSink(CallScreenEvents.Hangup) }, ) } + is AsyncData.Success -> Unit } } @@ -248,15 +256,18 @@ private fun WebView.setup( private fun WebView.addBackHandler(onBackPressed: () -> Unit) { addJavascriptInterface( - object { - @Suppress("unused") - @JavascriptInterface - fun onBackPressed() = onBackPressed() + JavascriptBackHandler { + onBackPressed() }, "backHandler" ) } +private fun WebView.dispatchEscKeyEvent() { + dispatchKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_ESCAPE)) + dispatchKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_ESCAPE)) +} + @PreviewsDayNight @Composable internal fun CallScreenViewPreview( @@ -275,3 +286,8 @@ internal fun CallScreenViewPreview( internal fun InvalidAudioDeviceDialogPreview() = ElementPreview { InvalidAudioDeviceDialog(invalidAudioDeviceReason = InvalidAudioDeviceReason.BT_AUDIO_DEVICE_DISABLED) {} } + +internal fun interface JavascriptBackHandler { + @JavascriptInterface + fun onBackPressed() +} diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenBackPressPolicyTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenBackPressPolicyTest.kt new file mode 100644 index 0000000000..f07f7039d3 --- /dev/null +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenBackPressPolicyTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.call.ui + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.call.impl.ui.CallScreenBackPressAction +import io.element.android.features.call.impl.ui.CallScreenBackPressPolicy +import org.junit.Test + +class CallScreenBackPressPolicyTest { + @Test + fun `resolve returns dispatch escape when a web view is available and native button is pressed`() { + val result = CallScreenBackPressPolicy.resolve( + supportPip = false, + hasWebView = true, + fromNative = true, + ) + + assertThat(result).isEqualTo(CallScreenBackPressAction.DispatchEscapeToWebView) + } + + @Test + fun `resolve dispatch escape when there is a web view and pip is supported on native button press`() { + val result = CallScreenBackPressPolicy.resolve( + supportPip = true, + hasWebView = true, + fromNative = true, + ) + + assertThat(result).isEqualTo(CallScreenBackPressAction.DispatchEscapeToWebView) + } + + @Test + fun `resolve returns hangup when there is no web view and pip is not supported from native button`() { + val result = CallScreenBackPressPolicy.resolve( + supportPip = false, + hasWebView = false, + fromNative = true, + ) + + assertThat(result).isNull() + } + + @Test + fun `resolve returns hangup when there is no web view even though pip is supported from native button`() { + val result = CallScreenBackPressPolicy.resolve( + supportPip = true, + hasWebView = false, + fromNative = true, + ) + + assertThat(result).isNull() + } + + @Test + fun `resolve goes to pip if its not from native but from the webview`() { + val result = CallScreenBackPressPolicy.resolve( + supportPip = true, + hasWebView = true, + fromNative = false, + ) + + assertThat(result).isEqualTo(CallScreenBackPressAction.EnterPictureInPicture) + } + @Test + fun `resolve hangs up if its not from native but from the webview and pip is not supported`() { + val result = CallScreenBackPressPolicy.resolve( + supportPip = false, + hasWebView = true, + fromNative = false, + ) + + assertThat(result).isNull() + } + + @Test + fun `invalid cases (event comes from webview but there is now webview) all result in hangup`() { + val withPipSupport = CallScreenBackPressPolicy.resolve( + supportPip = true, + hasWebView = false, + fromNative = false, + ) + assertThat(withPipSupport).isNull() + val withOutPipSupport = CallScreenBackPressPolicy.resolve( + supportPip = false, + hasWebView = false, + fromNative = false, + ) + assertThat(withOutPipSupport).isNull() + } +} diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt new file mode 100644 index 0000000000..35b90a6716 --- /dev/null +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.call.ui + +import android.view.KeyEvent +import android.webkit.WebView +import androidx.activity.ComponentActivity +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.call.impl.pip.PictureInPictureEvents +import io.element.android.features.call.impl.pip.aPictureInPictureState +import io.element.android.features.call.impl.ui.CallScreenEvents +import io.element.android.features.call.impl.ui.CallScreenView +import io.element.android.features.call.impl.ui.JavascriptBackHandler +import io.element.android.features.call.impl.ui.aCallScreenState +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.pressBackKey +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import org.robolectric.annotation.Implementation +import org.robolectric.annotation.Implements +import org.robolectric.annotation.Resetter +import org.robolectric.shadows.ShadowWebView + +@RunWith(AndroidJUnit4::class) +class CallScreenViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `pressing back key triggers hangup when no web view is available and pip is unsupported`() { + val callEvents = EventsRecorder() + + rule.setCallScreenView( + state = aCallScreenState(eventSink = callEvents), + useInspectionMode = true, + ) + + rule.pressBackKey() + + callEvents.assertEmpty() + } + + @Config(shadows = [RecordingShadowWebView::class]) + @Test + fun `pressing back key dispatches escape key events to web view when pip is unsupported`() { + rule.setCallScreenView( + state = aCallScreenState(), + useInspectionMode = false, + ) + + rule.pressBackKey() + + val dispatchedEvents = RecordingShadowWebView.dispatchedEvents + assertEquals(2, dispatchedEvents.size) + assertEquals(KeyEvent.ACTION_DOWN, dispatchedEvents[0].action) + assertEquals(KeyEvent.KEYCODE_ESCAPE, dispatchedEvents[0].keyCode) + assertEquals(KeyEvent.ACTION_UP, dispatchedEvents[1].action) + assertEquals(KeyEvent.KEYCODE_ESCAPE, dispatchedEvents[1].keyCode) + } + + @Config(shadows = [RecordingShadowWebView::class]) + @Test + fun `web view javascript back handler emits pip event when pip is supported`() { + val pipEvents = EventsRecorder() + + rule.setCallScreenView( + state = aCallScreenState(), + useInspectionMode = false, + pipState = aPictureInPictureState( + supportPip = true, + eventSink = pipEvents, + ), + ) + + rule.runOnIdle { + RecordingShadowWebView.invokeJavascriptBackHandler() + } + + pipEvents.assertSize(2) + pipEvents.assertTrue(0) { it is PictureInPictureEvents.SetPipController } + pipEvents.assertTrue(1) { it is PictureInPictureEvents.EnterPictureInPicture } + } +} + +private fun AndroidComposeTestRule.setCallScreenView( + state: io.element.android.features.call.impl.ui.CallScreenState, + useInspectionMode: Boolean, + pipState: io.element.android.features.call.impl.pip.PictureInPictureState = aPictureInPictureState(supportPip = false), +) { + setContent { + // Inspection mode disables AndroidView creation; keep it configurable per test. + CompositionLocalProvider(LocalInspectionMode provides useInspectionMode) { + CallScreenView( + state = state, + pipState = pipState, + onConsoleMessage = {}, + requestPermissions = { _, _ -> }, + ) + } + } +} + +@Implements(WebView::class) +internal class RecordingShadowWebView : ShadowWebView() { + companion object { + val dispatchedEvents = mutableListOf() + private var backHandlerJavascriptInterface: JavascriptBackHandler? = null + + @Resetter + @JvmStatic + @Suppress("unused") + fun resetRecordedEvents() { + dispatchedEvents.clear() + backHandlerJavascriptInterface = null + } + + fun invokeJavascriptBackHandler() { + val backHandler = checkNotNull(backHandlerJavascriptInterface) { "Expected backHandler JavaScript interface to be registered" } + backHandler.onBackPressed() + } + } + + @Implementation + protected override fun addJavascriptInterface(`object`: Any, name: String) { + super.addJavascriptInterface(`object`, name) + if (name == "backHandler") { + backHandlerJavascriptInterface = `object` as? JavascriptBackHandler + } + } + + @Implementation + @Suppress("unused") + fun dispatchKeyEvent(event: KeyEvent): Boolean { + dispatchedEvents += KeyEvent(event) + return false + } +} From 7940b8f86511d45e19ba1be4900d5ac708c6fecd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:11:36 +0000 Subject: [PATCH 09/21] Update dependency io.sentry:sentry-android to v8.40.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b942901e19..19b9c29468 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -222,7 +222,7 @@ color_picker = "io.mhssn:colorpicker:1.0.0" # Analytics posthog = "com.posthog:posthog-android:3.39.0" -sentry = "io.sentry:sentry-android:8.39.1" +sentry = "io.sentry:sentry-android:8.40.0" # main branch can be tested replacing the version with main-SNAPSHOT matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.33.2" From da36323006643ac0c59f87e6f699055cc2138caa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 20:27:57 +0000 Subject: [PATCH 10/21] Update dependency androidx.compose:compose-bom to v2026.04.01 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b942901e19..85cea1c436 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ camera = "1.6.0" work = "2.11.2" # Compose -compose_bom = "2026.03.01" +compose_bom = "2026.04.01" # Coroutines coroutines = "1.10.2" From 4d0be69b4c22945270dfceeb675410f221234ff3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Apr 2026 09:52:10 +0200 Subject: [PATCH 11/21] In the module `:libraries:matrix.api`, change the dependencies to: - libraries.sessionStorage.api - projects.libraries.architecture from `api` to `implementation`. Modules who need `:libraries:matrix.api` do not necessarily need to use the session storage api. --- appconfig/build.gradle.kts | 2 ++ appnav/build.gradle.kts | 1 + features/enterprise/test/build.gradle.kts | 1 + features/home/impl/build.gradle.kts | 1 + features/invite/impl/build.gradle.kts | 1 + features/invite/test/build.gradle.kts | 1 + features/location/test/build.gradle.kts | 2 +- features/logout/impl/build.gradle.kts | 1 + features/messages/test/build.gradle.kts | 1 + features/poll/test/build.gradle.kts | 1 + features/preferences/impl/build.gradle.kts | 1 + features/preferences/test/build.gradle.kts | 1 + features/signedout/impl/build.gradle.kts | 1 + libraries/matrix/api/build.gradle.kts | 4 ++-- libraries/matrix/impl/build.gradle.kts | 2 ++ libraries/matrix/test/build.gradle.kts | 1 + libraries/matrixmedia/impl/build.gradle.kts | 2 ++ libraries/mediaupload/test/build.gradle.kts | 1 + libraries/mediaviewer/test/build.gradle.kts | 1 + libraries/preferences/impl/build.gradle.kts | 1 + libraries/pushproviders/firebase/build.gradle.kts | 1 + libraries/recentemojis/impl/build.gradle.kts | 1 + libraries/session-storage/test/build.gradle.kts | 1 + libraries/voiceplayer/api/build.gradle.kts | 1 + libraries/voiceplayer/impl/build.gradle.kts | 1 + libraries/workmanager/api/build.gradle.kts | 2 +- libraries/workmanager/impl/build.gradle.kts | 1 + services/analyticsproviders/sentry/build.gradle.kts | 2 ++ 28 files changed, 33 insertions(+), 4 deletions(-) diff --git a/appconfig/build.gradle.kts b/appconfig/build.gradle.kts index 45496acb77..64b9b76a14 100644 --- a/appconfig/build.gradle.kts +++ b/appconfig/build.gradle.kts @@ -48,6 +48,8 @@ android { } dependencies { + implementation(libs.coroutines.core) implementation(libs.androidx.annotationjvm) + implementation(libs.androidx.corektx) implementation(projects.libraries.matrix.api) } diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 6be468b0d1..7440ecd2bf 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixmedia.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.uiCommon) implementation(projects.libraries.uiStrings) implementation(projects.features.login.api) diff --git a/features/enterprise/test/build.gradle.kts b/features/enterprise/test/build.gradle.kts index 542e73717a..c37fc53de3 100644 --- a/features/enterprise/test/build.gradle.kts +++ b/features/enterprise/test/build.gradle.kts @@ -15,6 +15,7 @@ android { dependencies { api(projects.features.enterprise.api) + implementation(projects.libraries.architecture) implementation(projects.libraries.compound) implementation(projects.libraries.matrix.api) implementation(projects.tests.testutils) diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts index b36ee6aed2..0635da39a5 100644 --- a/features/home/impl/build.gradle.kts +++ b/features/home/impl/build.gradle.kts @@ -46,6 +46,7 @@ dependencies { implementation(projects.libraries.permissions.noop) implementation(projects.libraries.preferences.api) implementation(projects.libraries.push.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.features.announcement.api) implementation(projects.features.invite.api) implementation(projects.features.networkmonitor.api) diff --git a/features/invite/impl/build.gradle.kts b/features/invite/impl/build.gradle.kts index 80b98464f7..e033f2740c 100644 --- a/features/invite/impl/build.gradle.kts +++ b/features/invite/impl/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) implementation(projects.services.analytics.api) diff --git a/features/invite/test/build.gradle.kts b/features/invite/test/build.gradle.kts index 2df267f155..080ed765bb 100644 --- a/features/invite/test/build.gradle.kts +++ b/features/invite/test/build.gradle.kts @@ -16,6 +16,7 @@ android { dependencies { implementation(libs.coroutines.core) + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.test) implementation(projects.tests.testutils) diff --git a/features/location/test/build.gradle.kts b/features/location/test/build.gradle.kts index f84e8ba772..e51737d40c 100644 --- a/features/location/test/build.gradle.kts +++ b/features/location/test/build.gradle.kts @@ -16,7 +16,7 @@ android { dependencies { api(projects.features.location.api) + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) - implementation(libs.appyx.core) implementation(projects.tests.testutils) } diff --git a/features/logout/impl/build.gradle.kts b/features/logout/impl/build.gradle.kts index 8de7718980..d5356ced63 100644 --- a/features/logout/impl/build.gradle.kts +++ b/features/logout/impl/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) implementation(projects.libraries.dateformatter.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.workmanager.api) api(projects.features.logout.api) diff --git a/features/messages/test/build.gradle.kts b/features/messages/test/build.gradle.kts index b839f8de06..f89dd8de06 100644 --- a/features/messages/test/build.gradle.kts +++ b/features/messages/test/build.gradle.kts @@ -16,6 +16,7 @@ android { dependencies { api(projects.features.messages.impl) + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.test) implementation(projects.libraries.audio.test) implementation(projects.libraries.mediaplayer.test) diff --git a/features/poll/test/build.gradle.kts b/features/poll/test/build.gradle.kts index a3779809d7..d0adc8e94f 100644 --- a/features/poll/test/build.gradle.kts +++ b/features/poll/test/build.gradle.kts @@ -15,6 +15,7 @@ android { } dependencies { + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) api(projects.features.poll.api) implementation(libs.kotlinx.collections.immutable) diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index ad28c90966..889e030213 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -68,6 +68,7 @@ dependencies { implementation(projects.libraries.permissions.api) implementation(projects.libraries.push.api) implementation(projects.libraries.pushproviders.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.uiUtils) implementation(projects.libraries.fullscreenintent.api) implementation(projects.features.rageshake.api) diff --git a/features/preferences/test/build.gradle.kts b/features/preferences/test/build.gradle.kts index 7e3da4a6e8..a066fe4707 100644 --- a/features/preferences/test/build.gradle.kts +++ b/features/preferences/test/build.gradle.kts @@ -14,6 +14,7 @@ android { } dependencies { + implementation(projects.libraries.architecture) implementation(projects.features.preferences.api) implementation(projects.tests.testutils) } diff --git a/features/signedout/impl/build.gradle.kts b/features/signedout/impl/build.gradle.kts index 3c8aac5e25..b3801288be 100644 --- a/features/signedout/impl/build.gradle.kts +++ b/features/signedout/impl/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.uiStrings) testCommonDependencies(libs) diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index 1c70006cc8..2a7586d6c9 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -49,9 +49,9 @@ dependencies { implementation(projects.libraries.core) implementation(projects.services.analytics.api) implementation(libs.serialization.json) - api(projects.libraries.sessionStorage.api) + implementation(projects.libraries.sessionStorage.api) implementation(libs.coroutines.core) - api(projects.libraries.architecture) + implementation(projects.libraries.architecture) testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index 67386cc592..92e8a3ba9e 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -32,10 +32,12 @@ dependencies { implementation(projects.appconfig) implementation(projects.libraries.androidutils) + implementation(projects.libraries.architecture) implementation(projects.libraries.di) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.network) implementation(projects.libraries.preferences.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.workmanager.api) implementation(projects.services.analytics.api) implementation(projects.services.toolbox.api) diff --git a/libraries/matrix/test/build.gradle.kts b/libraries/matrix/test/build.gradle.kts index 63836d857a..ccfa56f1aa 100644 --- a/libraries/matrix/test/build.gradle.kts +++ b/libraries/matrix/test/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { api(projects.libraries.matrix.api) api(libs.coroutines.core) implementation(libs.coroutines.test) + implementation(projects.libraries.architecture) implementation(projects.services.analytics.api) implementation(projects.tests.testutils) implementation(libs.kotlinx.collections.immutable) diff --git a/libraries/matrixmedia/impl/build.gradle.kts b/libraries/matrixmedia/impl/build.gradle.kts index 82afc2f62c..56ccc79afc 100644 --- a/libraries/matrixmedia/impl/build.gradle.kts +++ b/libraries/matrixmedia/impl/build.gradle.kts @@ -19,8 +19,10 @@ android { setupDependencyInjection() dependencies { + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixmedia.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.designsystem) implementation(libs.coil.compose) implementation(libs.coil.gif) diff --git a/libraries/mediaupload/test/build.gradle.kts b/libraries/mediaupload/test/build.gradle.kts index 7e729089c7..f271ae1316 100644 --- a/libraries/mediaupload/test/build.gradle.kts +++ b/libraries/mediaupload/test/build.gradle.kts @@ -15,6 +15,7 @@ android { } dependencies { + implementation(libs.coroutines.core) api(projects.libraries.mediaupload.api) implementation(projects.libraries.core) implementation(projects.tests.testutils) diff --git a/libraries/mediaviewer/test/build.gradle.kts b/libraries/mediaviewer/test/build.gradle.kts index 1918714d7b..87665e6d69 100644 --- a/libraries/mediaviewer/test/build.gradle.kts +++ b/libraries/mediaviewer/test/build.gradle.kts @@ -18,6 +18,7 @@ android { dependencies { api(projects.libraries.mediaviewer.impl) + implementation(projects.libraries.architecture) implementation(projects.libraries.core) implementation(projects.tests.testutils) implementation(projects.libraries.matrix.api) diff --git a/libraries/preferences/impl/build.gradle.kts b/libraries/preferences/impl/build.gradle.kts index c567471da4..0478d303ea 100644 --- a/libraries/preferences/impl/build.gradle.kts +++ b/libraries/preferences/impl/build.gradle.kts @@ -25,4 +25,5 @@ dependencies { implementation(projects.libraries.di) implementation(projects.libraries.core) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.sessionStorage.api) } diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index 49ce7135d5..ffa4e9fa70 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { implementation(projects.libraries.di) implementation(projects.libraries.matrix.api) implementation(projects.libraries.push.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.uiStrings) implementation(projects.libraries.troubleshoot.api) implementation(projects.services.toolbox.api) diff --git a/libraries/recentemojis/impl/build.gradle.kts b/libraries/recentemojis/impl/build.gradle.kts index a1a72c8672..061a7ecd89 100644 --- a/libraries/recentemojis/impl/build.gradle.kts +++ b/libraries/recentemojis/impl/build.gradle.kts @@ -21,6 +21,7 @@ setupDependencyInjection() dependencies { api(projects.libraries.recentemojis.api) + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(libs.kotlinx.collections.immutable) implementation(libs.matrix.emojibase.bindings) diff --git a/libraries/session-storage/test/build.gradle.kts b/libraries/session-storage/test/build.gradle.kts index cfdc3018a9..7a89746812 100644 --- a/libraries/session-storage/test/build.gradle.kts +++ b/libraries/session-storage/test/build.gradle.kts @@ -14,6 +14,7 @@ android { } dependencies { + implementation(libs.coroutines.core) implementation(projects.libraries.matrix.api) implementation(projects.libraries.sessionStorage.api) } diff --git a/libraries/voiceplayer/api/build.gradle.kts b/libraries/voiceplayer/api/build.gradle.kts index f37c263d83..e058210b7d 100644 --- a/libraries/voiceplayer/api/build.gradle.kts +++ b/libraries/voiceplayer/api/build.gradle.kts @@ -16,5 +16,6 @@ android { dependencies { implementation(libs.androidx.annotationjvm) implementation(libs.coroutines.core) + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) } diff --git a/libraries/voiceplayer/impl/build.gradle.kts b/libraries/voiceplayer/impl/build.gradle.kts index 4aa00e188b..8fe79fb774 100644 --- a/libraries/voiceplayer/impl/build.gradle.kts +++ b/libraries/voiceplayer/impl/build.gradle.kts @@ -21,6 +21,7 @@ setupDependencyInjection() dependencies { api(projects.libraries.voiceplayer.api) + implementation(projects.libraries.architecture) implementation(projects.libraries.audio.api) implementation(projects.libraries.core) implementation(projects.libraries.di) diff --git a/libraries/workmanager/api/build.gradle.kts b/libraries/workmanager/api/build.gradle.kts index b53ed40394..238dc57664 100644 --- a/libraries/workmanager/api/build.gradle.kts +++ b/libraries/workmanager/api/build.gradle.kts @@ -15,6 +15,6 @@ android { dependencies { api(libs.androidx.workmanager.runtime) - + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) } diff --git a/libraries/workmanager/impl/build.gradle.kts b/libraries/workmanager/impl/build.gradle.kts index 878edb6fe2..c1874bfa74 100644 --- a/libraries/workmanager/impl/build.gradle.kts +++ b/libraries/workmanager/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.matrix.api) implementation(projects.libraries.di) + implementation(projects.libraries.sessionStorage.api) testCommonDependencies(libs, false) testImplementation(projects.libraries.sessionStorage.test) diff --git a/services/analyticsproviders/sentry/build.gradle.kts b/services/analyticsproviders/sentry/build.gradle.kts index 02dde35ef4..3350df864b 100644 --- a/services/analyticsproviders/sentry/build.gradle.kts +++ b/services/analyticsproviders/sentry/build.gradle.kts @@ -50,6 +50,8 @@ setupDependencyInjection() dependencies { implementation(libs.sentry) + implementation(libs.coroutines.core) + implementation(libs.androidx.annotationjvm) implementation(projects.libraries.core) implementation(projects.libraries.di) implementation(projects.libraries.matrix.api) From 078e942a28ef6fe761172615274e8953bd76da93 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Apr 2026 09:54:54 +0200 Subject: [PATCH 12/21] Cleanup dependencies --- libraries/matrix/api/build.gradle.kts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index 2a7586d6c9..2a326527df 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -44,14 +44,12 @@ android { } dependencies { - implementation(projects.libraries.di) - implementation(projects.libraries.androidutils) - implementation(projects.libraries.core) - implementation(projects.services.analytics.api) - implementation(libs.serialization.json) - implementation(projects.libraries.sessionStorage.api) implementation(libs.coroutines.core) + implementation(libs.serialization.json) + implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) + implementation(projects.libraries.sessionStorage.api) + implementation(projects.services.analytics.api) testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) From 4a79b272ef9bc33aaad05557f97fc105838eca34 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 30 Apr 2026 10:51:29 +0200 Subject: [PATCH 13/21] Fix ANRs when receiving push notifications (#6696) In Sentry there are some reports of methods called when notifications are fetched that end up having ANRs. This looked weird because everything is asynchronous... but it's still running with a `Main` dispatcher. Using the `Default/computation` one instead should be the right call. --- .../libraries/push/impl/push/DefaultPushHandler.kt | 9 ++++++--- .../libraries/push/impl/push/DefaultPushHandlerTest.kt | 9 +++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index 44cf6edefc..d5c3f04348 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -11,6 +11,7 @@ package io.element.android.libraries.push.impl.push import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.push.impl.db.PushRequest @@ -35,6 +36,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag) @@ -53,6 +55,7 @@ class DefaultPushHandler( private val workManagerScheduler: WorkManagerScheduler, private val syncPendingNotificationsRequestFactory: SyncPendingNotificationsRequestBuilder.Factory, resultProcessor: NotificationResultProcessor, + private val dispatchers: CoroutineDispatchers, ) : PushHandler { init { resultProcessor.start() @@ -64,7 +67,7 @@ class DefaultPushHandler( * @param pushData the data received in the push. * @param providerInfo the provider info. */ - override suspend fun handle(pushData: PushData, providerInfo: String): Boolean { + override suspend fun handle(pushData: PushData, providerInfo: String): Boolean = withContext(dispatchers.computation) { // Start measuring how long it takes to display a notification from when the push is received Timber.d("Calculating push-to-notification for event ${pushData.eventId}") val parent = analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.PushToNotification(pushData.eventId.value)) @@ -81,7 +84,7 @@ class DefaultPushHandler( } // Diagnostic Push - return if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) { + if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) { pushHistoryService.onDiagnosticPush(providerInfo) diagnosticPushHandler.handlePush() false @@ -90,7 +93,7 @@ class DefaultPushHandler( } } - override suspend fun handleInvalid(providerInfo: String, data: String) { + override suspend fun handleInvalid(providerInfo: String, data: String) = withContext(dispatchers.computation) { incrementPushDataStore.incrementPushCounter() pushHistoryService.onInvalidPushReceived(providerInfo, data) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt index a16568d400..f0dee4446c 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt @@ -11,6 +11,7 @@ package io.element.android.libraries.push.impl.push import app.cash.turbine.test +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -40,7 +41,9 @@ import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -212,7 +215,7 @@ class DefaultPushHandlerTest { .isCalledOnce() } - private fun createDefaultPushHandler( + private fun TestScope.createDefaultPushHandler( incrementPushCounterResult: () -> Unit = { lambdaError() }, userPushStore: FakeUserPushStore = FakeUserPushStore(), pushClientSecret: PushClientSecret = FakePushClientSecret(), @@ -227,6 +230,7 @@ class DefaultPushHandlerTest { start = {}, stop = {}, ), + dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), ): DefaultPushHandler { return DefaultPushHandler( incrementPushDataStore = object : IncrementPushDataStore { @@ -246,7 +250,8 @@ class DefaultPushHandlerTest { resultProcessor = resultProcessor, syncPendingNotificationsRequestFactory = SyncPendingNotificationsRequestBuilder.Factory { FakeSyncPendingNotificationsRequestBuilder() - } + }, + dispatchers = dispatchers, ) } } From c9bc8791e95a10c8e64d91fa25040b6034760aa9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 09:19:59 +0000 Subject: [PATCH 14/21] Update kotlin --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b942901e19..c42858bc64 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,9 +5,9 @@ # Project android_gradle_plugin = "8.13.2" # When updating this, please also update the version in the file ./idea/kotlinc.xml -kotlin = "2.3.20" +kotlin = "2.3.21" kotlinpoet = "2.3.0" -ksp = "2.3.6" +ksp = "2.3.7" firebaseAppDistribution = "5.2.1" # AndroidX From 13775f4fbdf7af89603785e6072d2f902dd3d401 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Apr 2026 14:09:41 +0200 Subject: [PATCH 15/21] Update kotlin version --- .idea/kotlinc.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 76f6344777..f393d5cdd1 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,7 @@ - From 30fd90abb9450e267ba1f9addd69c9d778b2fbca Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 30 Apr 2026 16:01:24 +0200 Subject: [PATCH 16/21] Mitigate a deadlock when loading room timelines (#6674) This may be happening because we were not destroying focused event timelines used for the media viewer/gallery when necessary, and having several of those back paginating *may* have caused a deadlock in the event cache. --- .../impl/datasource/MediaGalleryDataSource.kt | 13 +++++++++---- .../impl/gallery/MediaGalleryPresenter.kt | 2 +- .../impl/viewer/MediaViewerDataSource.kt | 6 ++++-- .../mediaviewer/impl/viewer/MediaViewerPresenter.kt | 2 +- .../impl/viewer/SingleMediaGalleryDataSource.kt | 3 ++- .../impl/datasource/FakeMediaGalleryDataSource.kt | 3 ++- .../TimelineMediaGalleryDataSourceTest.kt | 12 ++++++------ .../impl/viewer/MediaViewerDataSourceTest.kt | 2 +- .../impl/viewer/SingleMediaGalleryDataSourceTest.kt | 4 ++-- 9 files changed, 28 insertions(+), 19 deletions(-) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt index 722e14a790..f5418c76c9 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -27,10 +28,11 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach +import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean interface MediaGalleryDataSource { - fun start() + fun start(coroutineScope: CoroutineScope) fun groupedMediaItemsFlow(): Flow> fun getLastData(): AsyncData suspend fun loadMore(direction: Timeline.PaginationDirection) @@ -58,7 +60,7 @@ class TimelineMediaGalleryDataSource( private val isStarted = AtomicBoolean(false) @OptIn(ExperimentalCoroutinesApi::class) - override fun start() { + override fun start(coroutineScope: CoroutineScope) { if (!isStarted.compareAndSet(false, true)) { return } @@ -96,9 +98,12 @@ class TimelineMediaGalleryDataSource( groupedMediaItemsFlow.emit(AsyncData.Success(groupedMediaItems)) } .onCompletion { - timeline?.close() + timeline?.let { + Timber.d("Timeline media gallery data source flow completed for room ${room.roomId}, closing timeline") + it.close() + } } - .launchIn(room.roomCoroutineScope) + .launchIn(coroutineScope) } override suspend fun loadMore(direction: Timeline.PaginationDirection) { diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt index ac9b365099..e7caa12f2e 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt @@ -78,7 +78,7 @@ class MediaGalleryPresenter( .collectAsState(AsyncData.Uninitialized) LaunchedEffect(Unit) { - mediaGalleryDataSource.start() + mediaGalleryDataSource.start(this) } val permissions by room.permissionsAsState(MediaPermissions.DEFAULT) { perms -> diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt index a9fb5d645c..24e48531f0 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt @@ -35,6 +35,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext @@ -62,11 +63,12 @@ class MediaViewerDataSource( private val localMediaStates: MutableMap>> = mutableMapOf() - fun setup() { - galleryDataSource.start() + fun setup(coroutineScope: CoroutineScope) { + galleryDataSource.start(coroutineScope) } fun dispose() { + Timber.d("Disposing MediaViewerDataSource, closing ${mediaFiles.size} media files") mediaFiles.forEach { it.close() } mediaFiles.clear() localMediaStates.clear() diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt index b7631a7039..60f03bb1e0 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt @@ -88,7 +88,7 @@ class MediaViewerPresenter( var mediaBottomSheetState by remember { mutableStateOf(MediaBottomSheetState.Hidden) } DisposableEffect(Unit) { - dataSource.setup() + dataSource.setup(coroutineScope) onDispose { dataSource.dispose() } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt index f243ac4fd7..7bbf397171 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt @@ -20,12 +20,13 @@ import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryData import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems import io.element.android.libraries.mediaviewer.impl.model.MediaItem import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flowOf class SingleMediaGalleryDataSource( private val data: GroupedMediaItems, ) : MediaGalleryDataSource { - override fun start() = Unit + override fun start(coroutineScope: CoroutineScope) = Unit override fun groupedMediaItemsFlow() = flowOf(AsyncData.Success(data)) override fun getLastData(): AsyncData = AsyncData.Success(data) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt index c612bba1bc..4a64f02a33 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt @@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems import io.element.android.tests.testutils.lambda.lambdaError +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -21,7 +22,7 @@ class FakeMediaGalleryDataSource( private val loadMoreLambda: (Timeline.PaginationDirection) -> Unit = { lambdaError() }, private val deleteItemLambda: (EventId) -> Unit = { lambdaError() }, ) : MediaGalleryDataSource { - override fun start() = startLambda() + override fun start(coroutineScope: CoroutineScope) = startLambda() private val groupedMediaItemsFlow = MutableSharedFlow>( replay = 1 diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt index bb8419dde5..528fc1da70 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt @@ -80,7 +80,7 @@ class TimelineMediaGalleryDataSourceTest { roomCoroutineScope = backgroundScope, ) ) - sut.start() + sut.start(backgroundScope) assertThat(sut.getLastData()).isEqualTo(AsyncData.Uninitialized) sut.groupedMediaItemsFlow().test { assertThat(awaitItem().isLoading()).isTrue() @@ -95,7 +95,7 @@ class TimelineMediaGalleryDataSourceTest { ) assertThat(sut.getLastData().isSuccess()).isTrue() // Also test that starting again should have no effect - sut.start() + sut.start(backgroundScope) } } // Ensure that the timeline has been closed on flow completion @@ -117,7 +117,7 @@ class TimelineMediaGalleryDataSourceTest { roomCoroutineScope = backgroundScope, ) ) - sut.start() + sut.start(backgroundScope) sut.groupedMediaItemsFlow().test { skipItems(2) sut.loadMore(Timeline.PaginationDirection.BACKWARDS) @@ -140,7 +140,7 @@ class TimelineMediaGalleryDataSourceTest { roomCoroutineScope = backgroundScope, ) ) - sut.start() + sut.start(backgroundScope) sut.groupedMediaItemsFlow().test { skipItems(2) sut.deleteItem(AN_EVENT_ID) @@ -159,7 +159,7 @@ class TimelineMediaGalleryDataSourceTest { roomCoroutineScope = backgroundScope, ) ) - sut.start() + sut.start(backgroundScope) sut.groupedMediaItemsFlow().test { assertThat(awaitItem().isLoading()).isTrue() assertThat(sut.getLastData().isLoading()).isTrue() @@ -181,7 +181,7 @@ class TimelineMediaGalleryDataSourceTest { roomCoroutineScope = backgroundScope, ) ) - sut.start() + sut.start(backgroundScope) sut.groupedMediaItemsFlow().test { assertThat(awaitItem().isLoading()).isTrue() assertThat(sut.getLastData().isLoading()).isTrue() diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt index 44eed2733f..c3f1ab9a0d 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt @@ -50,7 +50,7 @@ class MediaViewerDataSourceTest { val sut = createMediaViewerDataSource( galleryDataSource = galleryDataSource, ) - sut.setup() + sut.setup(backgroundScope) startLambda.assertions().isCalledOnce() } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt index c6460cb70a..8c0a7c05d0 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt @@ -37,9 +37,9 @@ class SingleMediaGalleryDataSourceTest { val warmUpRule = WarmUpRule() @Test - fun `function start is no op`() { + fun `function start is no op`() = runTest { val sut = SingleMediaGalleryDataSource(aGroupedMediaItems()) - sut.start() + sut.start(backgroundScope) } @Test From 11b9efa2c98ad73faa1e05c840e5c8b8fb45431f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Apr 2026 15:54:33 +0200 Subject: [PATCH 17/21] Migrate to v2 testing APIs --- .../FullscreenAnnouncementViewTest.kt | 25 ++- .../features/call/ui/CallScreenViewTest.kt | 32 ++- .../impl/AccountDeactivationViewTest.kt | 55 +++--- .../forward/impl/ForwardMessagesViewTest.kt | 23 ++- .../ChooseSessionVerificationModeViewTest.kt | 37 ++-- .../impl/filters/RoomListFiltersViewTest.kt | 20 +- .../impl/roomlist/RoomListContextMenuTest.kt | 48 ++--- .../roomlist/RoomListDeclineInviteMenuTest.kt | 33 ++-- .../home/impl/roomlist/RoomListViewTest.kt | 84 ++++---- .../impl/spacefilters/SpaceFiltersViewTest.kt | 34 ++-- .../DeclineAndBlockViewTest.kt | 49 +++-- .../joinroom/impl/JoinRoomViewTest.kt | 121 ++++++------ .../banner/KnockRequestsBannerViewTest.kt | 40 ++-- .../impl/list/KnockRequestsListViewTest.kt | 61 +++--- .../screens/desktop/DesktopNoticeViewTest.kt | 36 ++-- .../impl/screens/error/ErrorViewTest.kt | 32 ++- .../screens/number/EnterNumberViewTest.kt | 40 ++-- .../impl/screens/qrcode/ShowQrCodeViewTest.kt | 20 +- .../screens/root/LinkNewDeviceRootViewTest.kt | 38 ++-- .../impl/screens/scan/ScanQrCodeViewTest.kt | 26 ++- .../impl/share/ShareLocationViewTest.kt | 61 +++--- .../impl/show/ShowLocationViewTest.kt | 57 +++--- .../impl/unlock/keypad/PinKeypadTest.kt | 33 ++-- .../ChooseAccountProviderViewTest.kt | 32 ++- .../loginpassword/LoginPasswordViewTest.kt | 97 +++++---- .../screens/onboarding/OnboardingViewTest.kt | 100 +++++----- .../QrCodeConfirmationViewTest.kt | 26 ++- .../qrcode/error/QrCodeErrorViewTest.kt | 32 ++- .../qrcode/intro/QrCodeIntroViewTest.kt | 36 ++-- .../screens/qrcode/scan/QrCodeScanViewTest.kt | 24 ++- .../features/logout/impl/LogoutViewTest.kt | 49 +++-- .../direct/DefaultDirectLogoutViewTest.kt | 43 ++-- .../messages/impl/MessagesViewTest.kt | 173 ++++++++-------- .../identity/IdentityChangeStateViewTest.kt | 43 ++-- .../ResolveVerifiedUserSendFailureViewTest.kt | 25 ++- .../messages/impl/link/LinkViewTest.kt | 30 ++- .../banner/PinnedMessagesBannerViewTest.kt | 25 ++- .../pinned/list/PinnedMessagesListViewTest.kt | 32 ++- .../DefaultHtmlConverterProviderTest.kt | 12 +- .../impl/timeline/TimelineViewTest.kt | 65 +++--- .../event/TimelineItemPollViewTest.kt | 28 +-- .../components/event/TimelineTextViewTest.kt | 38 ++-- .../timeline/protection/ProtectedViewTest.kt | 27 ++- .../poll/impl/history/PollHistoryViewTest.kt | 56 +++--- .../preferences/impl/about/AboutViewTest.kt | 31 ++- .../impl/advanced/AdvancedSettingsViewTest.kt | 108 +++++----- .../impl/blockedusers/BlockedUserViewTest.kt | 38 ++-- .../developer/DeveloperSettingsViewTest.kt | 38 ++-- .../AppDeveloperSettingsPageTest.kt | 44 ++--- .../NotificationSettingsViewTest.kt | 86 ++++---- .../impl/root/PreferencesRootViewTest.kt | 187 +++++++++--------- .../editprofile/EditUserProfileViewTest.kt | 54 ++--- .../reportroom/impl/ReportRoomViewTest.kt | 43 ++-- .../ChangeRoomPermissionsViewTest.kt | 75 ++++--- .../impl/roles/ChangeRolesViewTest.kt | 111 ++++++----- .../impl/root/RolesAndPermissionsViewTest.kt | 86 ++++---- .../impl/RoomAliasHelperViewTest.kt | 29 ++- .../roomdetails/impl/RoomDetailsViewTest.kt | 149 +++++++------- .../impl/RoomDetailsEditViewTest.kt | 95 +++++---- .../impl/root/RoomDirectoryViewTest.kt | 29 ++- .../impl/RoomMemberModerationViewTest.kt | 85 ++++---- .../SecureBackupEnterRecoveryKeyViewTest.kt | 57 +++--- .../password/ResetIdentityPasswordViewTest.kt | 40 ++-- .../reset/root/ResetIdentityRootViewTest.kt | 44 ++--- .../EditRoomAddressViewTest.kt | 49 +++-- .../ManageAuthorizedSpacesViewTest.kt | 37 ++-- .../impl/root/SecurityAndPrivacyViewTest.kt | 102 +++++----- .../impl/addroom/AddRoomToSpaceViewTest.kt | 45 +++-- .../features/space/impl/root/SpaceViewTest.kt | 85 ++++---- .../JoinBaseRoomByAddressViewTest.kt | 28 ++- .../startchat/impl/root/StartChatViewTest.kt | 52 +++-- .../userprofile/UserProfileViewTest.kt | 98 +++++---- .../shared/blockuser/BlockUserDialogsTest.kt | 33 ++-- .../incoming/IncomingVerificationViewTest.kt | 85 ++++---- .../outgoing/OutgoingVerificationViewTest.kt | 67 ++++--- .../MediaDeleteConfirmationBottomSheetTest.kt | 28 ++- .../details/MediaDetailsBottomSheetTest.kt | 52 +++-- .../impl/viewer/MediaViewerViewTest.kt | 72 +++---- .../markdown/MarkdownTextInputTest.kt | 127 +++++------- .../impl/TroubleshootNotificationsViewTest.kt | 38 ++-- .../impl/history/PushHistoryViewTest.kt | 58 +++--- .../testutils/RobolectricDispatcherCleaner.kt | 8 +- ...nticsNodeInteractionsProviderExtensions.kt | 26 +-- 83 files changed, 2197 insertions(+), 2320 deletions(-) diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementViewTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementViewTest.kt index b69037e61a..b7932898a8 100644 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementViewTest.kt +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.announcement.impl.fullscreen import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.impl.AnnouncementEvent @@ -20,43 +23,39 @@ import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class FullscreenAnnouncementViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on back sends a AnnouncementEvent`() { + fun `clicking on back sends a AnnouncementEvent`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setFullscreenAnnouncementView( + setFullscreenAnnouncementView( anAnnouncementState( announcement = Announcement.Fullscreen.Space, eventSink = eventsRecorder, ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space)) } @Test - fun `clicking on Continue sends a AnnouncementEvent`() { + fun `clicking on Continue sends a AnnouncementEvent`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setFullscreenAnnouncementView( + setFullscreenAnnouncementView( anAnnouncementState( announcement = Announcement.Fullscreen.Space, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space)) } } -private fun AndroidComposeTestRule.setFullscreenAnnouncementView( +private fun AndroidComposeUiTest.setFullscreenAnnouncementView( state: AnnouncementState, ) { setContent { diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt index 35b90a6716..fed9f90de0 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt @@ -5,6 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.call.ui import android.view.KeyEvent @@ -12,8 +14,9 @@ import android.webkit.WebView import androidx.activity.ComponentActivity import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.call.impl.pip.PictureInPictureEvents import io.element.android.features.call.impl.pip.aPictureInPictureState @@ -24,9 +27,7 @@ import io.element.android.features.call.impl.ui.aCallScreenState import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.pressBackKey import org.junit.Assert.assertEquals -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.Implementation @@ -36,32 +37,29 @@ import org.robolectric.shadows.ShadowWebView @RunWith(AndroidJUnit4::class) class CallScreenViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `pressing back key triggers hangup when no web view is available and pip is unsupported`() { + fun `pressing back key triggers hangup when no web view is available and pip is unsupported`() = runAndroidComposeUiTest { val callEvents = EventsRecorder() - rule.setCallScreenView( + setCallScreenView( state = aCallScreenState(eventSink = callEvents), useInspectionMode = true, ) - rule.pressBackKey() + pressBackKey() callEvents.assertEmpty() } @Config(shadows = [RecordingShadowWebView::class]) @Test - fun `pressing back key dispatches escape key events to web view when pip is unsupported`() { - rule.setCallScreenView( + fun `pressing back key dispatches escape key events to web view when pip is unsupported`() = runAndroidComposeUiTest { + setCallScreenView( state = aCallScreenState(), useInspectionMode = false, ) - rule.pressBackKey() + pressBackKey() val dispatchedEvents = RecordingShadowWebView.dispatchedEvents assertEquals(2, dispatchedEvents.size) @@ -73,10 +71,10 @@ class CallScreenViewTest { @Config(shadows = [RecordingShadowWebView::class]) @Test - fun `web view javascript back handler emits pip event when pip is supported`() { + fun `web view javascript back handler emits pip event when pip is supported`() = runAndroidComposeUiTest { val pipEvents = EventsRecorder() - rule.setCallScreenView( + setCallScreenView( state = aCallScreenState(), useInspectionMode = false, pipState = aPictureInPictureState( @@ -85,7 +83,7 @@ class CallScreenViewTest { ), ) - rule.runOnIdle { + runOnIdle { RecordingShadowWebView.invokeJavascriptBackHandler() } @@ -95,7 +93,7 @@ class CallScreenViewTest { } } -private fun AndroidComposeTestRule.setCallScreenView( +private fun AndroidComposeUiTest.setCallScreenView( state: io.element.android.features.call.impl.ui.CallScreenState, useInspectionMode: Boolean, pipState: io.element.android.features.call.impl.pip.PictureInPictureState = aPictureInPictureState(supportPip = false), diff --git a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt index 26c942da1f..c672fd666b 100644 --- a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt +++ b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt @@ -6,13 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.logout.impl import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.deactivation.impl.R import io.element.android.libraries.architecture.AsyncAction @@ -26,33 +29,29 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressTag -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class AccountDeactivationViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setAccountDeactivationView( + setAccountDeactivationView( state = anAccountDeactivationState(eventSink = eventsRecorder), onBackClick = it, ) - rule.pressBack() + pressBack() } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on Deactivate emits the expected Event`() { + fun `clicking on Deactivate emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAccountDeactivationView( + setAccountDeactivationView( state = anAccountDeactivationState( deactivateFormState = aDeactivateFormState( password = A_PASSWORD, @@ -60,14 +59,14 @@ class AccountDeactivationViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_delete) + clickOn(CommonStrings.action_delete) eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false)) } @Test - fun `clicking on Deactivate on the confirmation dialog emits the expected Event`() { + fun `clicking on Deactivate on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAccountDeactivationView( + setAccountDeactivationView( state = anAccountDeactivationState( deactivateFormState = aDeactivateFormState( password = A_PASSWORD, @@ -76,14 +75,14 @@ class AccountDeactivationViewTest { eventSink = eventsRecorder, ), ) - rule.pressTag(TestTags.dialogPositive.value) + pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false)) } @Test - fun `clicking on retry on the confirmation dialog emits the expected Event`() { + fun `clicking on retry on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAccountDeactivationView( + setAccountDeactivationView( state = anAccountDeactivationState( deactivateFormState = aDeactivateFormState( password = A_PASSWORD, @@ -92,26 +91,26 @@ class AccountDeactivationViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_retry) + clickOn(CommonStrings.action_retry) eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(true)) } @Test - fun `switching on the erase all switch emits the expected Event`() { + fun `switching on the erase all switch emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAccountDeactivationView( + setAccountDeactivationView( state = anAccountDeactivationState( eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_deactivate_account_delete_all_messages) + clickOn(R.string.screen_deactivate_account_delete_all_messages) eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(true)) } @Test - fun `switching off the erase all switch emits the expected Event`() { + fun `switching off the erase all switch emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAccountDeactivationView( + setAccountDeactivationView( state = anAccountDeactivationState( deactivateFormState = aDeactivateFormState( eraseData = true, @@ -119,15 +118,15 @@ class AccountDeactivationViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_deactivate_account_delete_all_messages) + clickOn(R.string.screen_deactivate_account_delete_all_messages) eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(false)) } @Config(qualifiers = "h1024dp") @Test - fun `typing text in the password field emits the expected Event`() { + fun `typing text in the password field emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAccountDeactivationView( + setAccountDeactivationView( state = anAccountDeactivationState( deactivateFormState = aDeactivateFormState( password = A_PASSWORD, @@ -135,12 +134,12 @@ class AccountDeactivationViewTest { eventSink = eventsRecorder, ), ) - rule.onNodeWithTag(TestTags.loginPassword.value).performTextInput("A") + onNodeWithTag(TestTags.loginPassword.value).performTextInput("A") eventsRecorder.assertSingle(AccountDeactivationEvents.SetPassword("A$A_PASSWORD")) } } -private fun AndroidComposeTestRule.setAccountDeactivationView( +private fun AndroidComposeUiTest.setAccountDeactivationView( state: AccountDeactivationState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt index f1e9bd8fc6..57a9f65099 100644 --- a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt +++ b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.forward.impl import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId @@ -21,34 +24,30 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressTag -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ForwardMessagesViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `cancel error emits the expected event`() { + fun `cancel error emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setForwardMessagesView( + setForwardMessagesView( aForwardMessagesState( forwardAction = AsyncAction.Failure(AN_EXCEPTION), eventSink = eventsRecorder ), ) - rule.pressTag(TestTags.dialogPositive.value) + pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(ForwardMessagesEvents.ClearError) } @Test - fun `success invokes onForwardSuccess`() { + fun `success invokes onForwardSuccess`() = runAndroidComposeUiTest { val data = listOf(A_ROOM_ID) val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam?>(data) { callback -> - rule.setForwardMessagesView( + setForwardMessagesView( aForwardMessagesState( forwardAction = AsyncAction.Success(data), eventSink = eventsRecorder @@ -59,7 +58,7 @@ class ForwardMessagesViewTest { } } -private fun AndroidComposeTestRule.setForwardMessagesView( +private fun AndroidComposeUiTest.setForwardMessagesView( state: ForwardMessagesState, onForwardSuccess: (List) -> Unit = EnsureNeverCalledWithParam(), ) { diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt index 521bf91b37..6e74f58f66 100644 --- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.ftue.impl.sessionverification.choosemode import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.ftue.impl.R import io.element.android.libraries.architecture.AsyncData @@ -18,65 +21,61 @@ import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class ChooseSessionVerificationModeViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Config(qualifiers = "h1024dp") @Test - fun `clicking on learn more invokes the expected callback`() { + fun `clicking on learn more invokes the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setChooseSelfVerificationModeView( + setChooseSelfVerificationModeView( aChooseSelfVerificationModeState(), onLearnMoreClick = callback, ) - rule.clickOn(CommonStrings.action_learn_more) + clickOn(CommonStrings.action_learn_more) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on use another device calls the callback`() { + fun `clicking on use another device calls the callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setChooseSelfVerificationModeView( + setChooseSelfVerificationModeView( aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canUseAnotherDevice = true))), onUseAnotherDevice = callback, ) - rule.clickOn(R.string.screen_identity_use_another_device) + clickOn(R.string.screen_identity_use_another_device) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on enter recovery key calls the callback`() { + fun `clicking on enter recovery key calls the callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setChooseSelfVerificationModeView( + setChooseSelfVerificationModeView( aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canUseRecoveryKey = true))), onEnterRecoveryKey = callback, ) - rule.clickOn(R.string.screen_identity_confirmation_use_recovery_key) + clickOn(R.string.screen_identity_confirmation_use_recovery_key) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on cannot confirm calls the reset keys callback`() { + fun `clicking on cannot confirm calls the reset keys callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setChooseSelfVerificationModeView( + setChooseSelfVerificationModeView( aChooseSelfVerificationModeState(), onResetKey = callback, ) - rule.clickOn(R.string.screen_identity_confirmation_cannot_confirm) + clickOn(R.string.screen_identity_confirmation_cannot_confirm) } } - private fun AndroidComposeTestRule.setChooseSelfVerificationModeView( + private fun AndroidComposeUiTest.setChooseSelfVerificationModeView( state: ChooseSelfVerificationModeState, onLearnMoreClick: () -> Unit = EnsureNeverCalled(), onUseAnotherDevice: () -> Unit = EnsureNeverCalled(), diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersViewTest.kt index 4c361b47f3..de5760c1bd 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersViewTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersViewTest.kt @@ -6,10 +6,13 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.home.impl.filters import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.home.impl.R import io.element.android.features.home.impl.filters.selection.FilterSelectionState @@ -17,23 +20,20 @@ import io.element.android.libraries.testtags.TestTags import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressTag -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RoomListFiltersViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on filters generates expected Event`() { + fun `clicking on filters generates expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setContent { + setContent { RoomListFiltersView( state = aRoomListFiltersState(eventSink = eventsRecorder), ) } - rule.clickOn(R.string.screen_roomlist_filter_rooms) + clickOn(R.string.screen_roomlist_filter_rooms) eventsRecorder.assertList( listOf( RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms), @@ -42,9 +42,9 @@ class RoomListFiltersViewTest { } @Test - fun `clicking on clear filters generates expected Event`() { + fun `clicking on clear filters generates expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setContent { + setContent { RoomListFiltersView( state = aRoomListFiltersState( filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) }, @@ -52,7 +52,7 @@ class RoomListFiltersViewTest { ), ) } - rule.pressTag(TestTags.homeScreenClearFilters.value) + pressTag(TestTags.homeScreenClearFilters.value) eventsRecorder.assertList( listOf( RoomListFiltersEvent.ClearSelectedFilters, diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt index 6be5fe4c16..5fa2adf9d6 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.home.impl.roomlist import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.home.impl.R import io.element.android.libraries.matrix.api.core.RoomId @@ -20,23 +23,20 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.setSafeContent -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RoomListContextMenuTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on Mark as read generates expected Events`() { + fun `clicking on Mark as read generates expected Events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(hasNewContent = true) - rule.setRoomListContextMenu( + setRoomListContextMenu( contextMenu = contextMenu, eventSink = eventsRecorder, ) - rule.clickOn(R.string.screen_roomlist_mark_as_read) + clickOn(R.string.screen_roomlist_mark_as_read) eventsRecorder.assertList( listOf( RoomListEvent.HideContextMenu, @@ -46,14 +46,14 @@ class RoomListContextMenuTest { } @Test - fun `clicking on Mark as unread generates expected Events`() { + fun `clicking on Mark as unread generates expected Events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(hasNewContent = false) - rule.setRoomListContextMenu( + setRoomListContextMenu( contextMenu = contextMenu, eventSink = eventsRecorder, ) - rule.clickOn(R.string.screen_roomlist_mark_as_unread) + clickOn(R.string.screen_roomlist_mark_as_unread) eventsRecorder.assertList( listOf( RoomListEvent.HideContextMenu, @@ -63,14 +63,14 @@ class RoomListContextMenuTest { } @Test - fun `clicking on Leave room generates expected Events`() { + fun `clicking on Leave room generates expected Events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(isDm = false) - rule.setRoomListContextMenu( + setRoomListContextMenu( contextMenu = contextMenu, eventSink = eventsRecorder, ) - rule.clickOn(CommonStrings.action_leave_room) + clickOn(CommonStrings.action_leave_room) eventsRecorder.assertList( listOf( RoomListEvent.HideContextMenu, @@ -80,48 +80,48 @@ class RoomListContextMenuTest { } @Test - fun `clicking on Report room invokes the expected callback and generates expected Event`() { + fun `clicking on Report room invokes the expected callback and generates expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown() val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit) - rule.setRoomListContextMenu( + setRoomListContextMenu( contextMenu = contextMenu, canReportRoom = true, eventSink = eventsRecorder, onRoomSettingsClick = EnsureNeverCalledWithParam(), onReportRoomClick = callback, ) - rule.clickOn(CommonStrings.action_report_room) + clickOn(CommonStrings.action_report_room) eventsRecorder.assertSingle(RoomListEvent.HideContextMenu) callback.assertSuccess() } @Test - fun `clicking on Settings invokes the expected callback and generates expected Event`() { + fun `clicking on Settings invokes the expected callback and generates expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown() val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit) - rule.setRoomListContextMenu( + setRoomListContextMenu( contextMenu = contextMenu, eventSink = eventsRecorder, onRoomSettingsClick = callback, ) - rule.clickOn(CommonStrings.common_settings) + clickOn(CommonStrings.common_settings) eventsRecorder.assertSingle(RoomListEvent.HideContextMenu) callback.assertSuccess() } @Test - fun `clicking on Favourites generates expected Event`() { + fun `clicking on Favourites generates expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(isDm = false, isFavorite = false) val callback = EnsureNeverCalledWithParam() - rule.setRoomListContextMenu( + setRoomListContextMenu( contextMenu = contextMenu, eventSink = eventsRecorder, onRoomSettingsClick = callback, ) - rule.clickOn(CommonStrings.common_favourite) + clickOn(CommonStrings.common_favourite) eventsRecorder.assertList( listOf( RoomListEvent.SetRoomIsFavorite(contextMenu.roomId, true), @@ -129,7 +129,7 @@ class RoomListContextMenuTest { ) } - private fun AndroidComposeTestRule<*, *>.setRoomListContextMenu( + private fun AndroidComposeUiTest.setRoomListContextMenu( contextMenu: RoomListState.ContextMenu.Shown, canReportRoom: Boolean = false, eventSink: (RoomListEvent) -> Unit, diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenuTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenuTest.kt index d7f509fda4..c8bba05e52 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenuTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenuTest.kt @@ -6,10 +6,12 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.home.impl.roomlist -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.home.impl.model.aRoomListRoomSummary import io.element.android.libraries.ui.strings.CommonStrings @@ -18,19 +20,16 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.setSafeContent -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RoomListDeclineInviteMenuTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on decline emits the expected Events`() { + fun `clicking on decline emits the expected Events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) - rule.setSafeContent { + setSafeContent { RoomListDeclineInviteMenu( menu = menu, canReportRoom = false, @@ -38,7 +37,7 @@ class RoomListDeclineInviteMenuTest { eventSink = eventsRecorder, ) } - rule.clickOn(CommonStrings.action_decline) + clickOn(CommonStrings.action_decline) eventsRecorder.assertList( listOf( RoomListEvent.HideDeclineInviteMenu, @@ -48,10 +47,10 @@ class RoomListDeclineInviteMenuTest { } @Test - fun `clicking on decline and block when canReportRoom=true, it emits the expected Events and callback`() { + fun `clicking on decline and block when canReportRoom=true, it emits the expected Events and callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) - rule.setSafeContent { + setSafeContent { RoomListDeclineInviteMenu( menu = menu, canReportRoom = true, @@ -59,16 +58,16 @@ class RoomListDeclineInviteMenuTest { eventSink = eventsRecorder, ) } - rule.clickOn(CommonStrings.action_decline_and_block) + clickOn(CommonStrings.action_decline_and_block) val expectedEvents = listOf(RoomListEvent.HideDeclineInviteMenu) eventsRecorder.assertList(expectedEvents) } @Test - fun `clicking on decline and block when canReportRoom=false, it emits the expected Events`() { + fun `clicking on decline and block when canReportRoom=false, it emits the expected Events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) - rule.setSafeContent { + setSafeContent { RoomListDeclineInviteMenu( menu = menu, canReportRoom = false, @@ -76,7 +75,7 @@ class RoomListDeclineInviteMenuTest { eventSink = eventsRecorder, ) } - rule.clickOn(CommonStrings.action_decline_and_block) + clickOn(CommonStrings.action_decline_and_block) val expectedEvents = listOf( RoomListEvent.HideDeclineInviteMenu, RoomListEvent.DeclineInvite(menu.roomSummary, blockUser = true), @@ -85,10 +84,10 @@ class RoomListDeclineInviteMenuTest { } @Test - fun `clicking on cancel emits the expected Event`() { + fun `clicking on cancel emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) - rule.setSafeContent { + setSafeContent { RoomListDeclineInviteMenu( menu = menu, canReportRoom = false, @@ -96,7 +95,7 @@ class RoomListDeclineInviteMenuTest { eventSink = eventsRecorder, ) } - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertList(listOf(RoomListEvent.HideDeclineInviteMenu)) } } diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt index 8402a921ca..b8d61994fa 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt @@ -6,16 +6,19 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.home.impl.roomlist import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.longClick import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.home.impl.HomeView import io.element.android.features.home.impl.R @@ -32,22 +35,17 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.setSafeContent -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class RoomListViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Config(qualifiers = "h1024dp") @Test - fun `displaying the view automatically sends a couple of UpdateVisibleRangeEvents`() { + fun `displaying the view automatically sends a couple of UpdateVisibleRangeEvents`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomListView( + setRoomListView( state = aRoomListState( contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), eventSink = eventsRecorder, @@ -62,9 +60,9 @@ class RoomListViewTest { } @Test - fun `clicking on close recovery key banner emits the expected Event`() { + fun `clicking on close recovery key banner emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomListView( + setRoomListView( state = aRoomListState( contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), eventSink = eventsRecorder, @@ -74,15 +72,15 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - val close = rule.activity.getString(CommonStrings.action_close) - rule.onNodeWithContentDescription(close).performClick() + val close = activity!!.getString(CommonStrings.action_close) + onNodeWithContentDescription(close).performClick() eventsRecorder.assertSingle(RoomListEvent.DismissBanner) } @Test - fun `clicking on close setup key banner emits the expected Event`() { + fun `clicking on close setup key banner emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomListView( + setRoomListView( state = aRoomListState( contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery), eventSink = eventsRecorder, @@ -92,16 +90,16 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - val close = rule.activity.getString(CommonStrings.action_close) - rule.onNodeWithContentDescription(close).performClick() + val close = activity!!.getString(CommonStrings.action_close) + onNodeWithContentDescription(close).performClick() eventsRecorder.assertSingle(RoomListEvent.DismissBanner) } @Test - fun `clicking on continue recovery key banner invokes the expected callback`() { + fun `clicking on continue recovery key banner invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() ensureCalledOnce { callback -> - rule.setRoomListView( + setRoomListView( state = aRoomListState( contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), eventSink = eventsRecorder, @@ -112,17 +110,17 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertEmpty() } } @Test - fun `clicking on continue setup key banner invokes the expected callback`() { + fun `clicking on continue setup key banner invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() ensureCalledOnce { callback -> - rule.setRoomListView( + setRoomListView( state = aRoomListState( contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery), eventSink = eventsRecorder, @@ -131,28 +129,28 @@ class RoomListViewTest { ) // Remove automatic initial events eventsRecorder.clear() - rule.clickOn(R.string.banner_set_up_recovery_submit) + clickOn(R.string.banner_set_up_recovery_submit) eventsRecorder.assertEmpty() } } @Test - fun `clicking on start chat when the session has no room invokes the expected callback`() { + fun `clicking on start chat when the session has no room invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setRoomListView( + setRoomListView( state = aRoomListState( eventSink = eventsRecorder, contentState = anEmptyContentState(), ), onCreateRoomClick = callback, ) - rule.clickOn(CommonStrings.action_start_chat) + clickOn(CommonStrings.action_start_chat) } } @Test - fun `clicking on a room invokes the expected callback`() { + fun `clicking on a room invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomListState( eventSink = eventsRecorder, @@ -161,7 +159,7 @@ class RoomListViewTest { it.displayType == RoomSummaryDisplayType.ROOM } ensureCalledOnceWithParam(room0.roomId) { callback -> - rule.setRoomListView( + setRoomListView( state = state, onRoomClick = callback, ) @@ -169,14 +167,14 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - rule.onNodeWithText(room0.latestEvent.content().toString()).performClick() + onNodeWithText(room0.latestEvent.content().toString()).performClick() } eventsRecorder.assertEmpty() } @Test - fun `clicking on a room twice invokes the expected callback only once`() { + fun `clicking on a room twice invokes the expected callback only once`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomListState( eventSink = eventsRecorder, @@ -185,13 +183,13 @@ class RoomListViewTest { it.displayType == RoomSummaryDisplayType.ROOM } ensureCalledOnceWithParam(room0.roomId) { callback -> - rule.setRoomListView( + setRoomListView( state = state, onRoomClick = callback, ) // Remove automatic initial events eventsRecorder.clear() - rule.onNodeWithText(room0.latestEvent.content().toString()) + onNodeWithText(room0.latestEvent.content().toString()) .performClick() .performClick() } @@ -199,7 +197,7 @@ class RoomListViewTest { } @Test - fun `long clicking on a room emits the expected Event`() { + fun `long clicking on a room emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomListState( eventSink = eventsRecorder, @@ -207,18 +205,18 @@ class RoomListViewTest { val room0 = state.contentAsRooms().summaries.first { it.displayType == RoomSummaryDisplayType.ROOM } - rule.setRoomListView( + setRoomListView( state = state, ) // Remove automatic initial events eventsRecorder.clear() - rule.onNodeWithText(room0.latestEvent.content().toString()).performTouchInput { longClick() } + onNodeWithText(room0.latestEvent.content().toString()).performTouchInput { longClick() } eventsRecorder.assertSingle(RoomListEvent.ShowContextMenu(room0)) } @Test - fun `clicking on a room setting invokes the expected callback and emits expected Event`() { + fun `clicking on a room setting invokes the expected callback and emits expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomListState( contextMenu = aContextMenuShown(), @@ -226,7 +224,7 @@ class RoomListViewTest { ) val room0 = (state.contextMenu as RoomListState.ContextMenu.Shown).roomId ensureCalledOnceWithParam(room0) { callback -> - rule.setRoomListView( + setRoomListView( state = state, onRoomSettingsClick = callback, ) @@ -234,14 +232,14 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - rule.clickOn(CommonStrings.common_settings) + clickOn(CommonStrings.common_settings) } eventsRecorder.assertSingle(RoomListEvent.HideContextMenu) } @Test - fun `clicking on accept and decline invite emits the expected Events`() { + fun `clicking on accept and decline invite emits the expected Events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomListState( eventSink = eventsRecorder, @@ -249,13 +247,13 @@ class RoomListViewTest { val invitedRoom = state.contentAsRooms().summaries.first { it.displayType == RoomSummaryDisplayType.INVITE } - rule.setRoomListView(state = state) + setRoomListView(state = state) // Remove automatic initial events eventsRecorder.clear() - rule.clickOn(CommonStrings.action_accept) - rule.clickOn(CommonStrings.action_decline) + clickOn(CommonStrings.action_accept) + clickOn(CommonStrings.action_decline) eventsRecorder.assertList( listOf( RoomListEvent.AcceptInvite(invitedRoom), @@ -265,7 +263,7 @@ class RoomListViewTest { } } -private fun AndroidComposeTestRule.setRoomListView( +private fun AndroidComposeUiTest.setRoomListView( state: RoomListState, onRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onSettingsClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersViewTest.kt index 5c1325b107..d612d765b6 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersViewTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersViewTest.kt @@ -5,34 +5,32 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.home.impl.spacefilters import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.tests.testutils.EventsRecorder -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SpaceFiltersViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `clicking on a filter with alias shows display name and alias`() { + fun `clicking on a filter with alias shows display name and alias`() = runAndroidComposeUiTest { val filter = aSpaceServiceFilter( displayName = "Test Space", canonicalAlias = A_ROOM_ALIAS, ) val eventsRecorder = EventsRecorder() - rule.setSpaceFiltersView( + setSpaceFiltersView( state = aSelectingSpaceFiltersState( availableFilters = listOf(filter), eventSink = eventsRecorder, @@ -40,20 +38,20 @@ class SpaceFiltersViewTest { ) // Both display name and alias should be visible - rule.onNodeWithText(filter.spaceRoom.displayName).assertExists() - rule.onNodeWithText(A_ROOM_ALIAS.value).assertExists() + onNodeWithText(filter.spaceRoom.displayName).assertExists() + onNodeWithText(A_ROOM_ALIAS.value).assertExists() - rule.onNodeWithText(filter.spaceRoom.displayName).performClick() + onNodeWithText(filter.spaceRoom.displayName).performClick() eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter)) } @Test - fun `multiple filters are displayed and clickable`() { + fun `multiple filters are displayed and clickable`() = runAndroidComposeUiTest { val filter1 = aSpaceServiceFilter(displayName = "Space One") val filter2 = aSpaceServiceFilter(displayName = "Space Two") val eventsRecorder = EventsRecorder() - rule.setSpaceFiltersView( + setSpaceFiltersView( state = aSelectingSpaceFiltersState( availableFilters = listOf(filter1, filter2), eventSink = eventsRecorder, @@ -61,17 +59,17 @@ class SpaceFiltersViewTest { ) // Both filters should be visible - rule.onNodeWithText(filter1.spaceRoom.displayName).assertExists() - rule.onNodeWithText(filter2.spaceRoom.displayName).assertExists() + onNodeWithText(filter1.spaceRoom.displayName).assertExists() + onNodeWithText(filter2.spaceRoom.displayName).assertExists() // Click on second filter - rule.onNodeWithText(filter2.spaceRoom.displayName).performClick() + onNodeWithText(filter2.spaceRoom.displayName).performClick() eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter2)) } } -private fun AndroidComposeTestRule.setSpaceFiltersView( +private fun AndroidComposeUiTest.setSpaceFiltersView( state: SpaceFiltersState, ) { setContent { diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt index 299fec8565..e915696de4 100644 --- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt @@ -6,13 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.invite.impl.declineandblock import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.invite.impl.R import io.element.android.libraries.ui.strings.CommonStrings @@ -21,98 +24,94 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class DeclineAndBlockViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on back invoke the expected callback`() { + fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setDeclineAndBlockView( + setDeclineAndBlockView( aDeclineAndBlockState( eventSink = eventsRecorder, ), onBackClick = it ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on decline when enabled emits the expected event`() { + fun `clicking on decline when enabled emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDeclineAndBlockView( + setDeclineAndBlockView( aDeclineAndBlockState( blockUser = true, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_decline) + clickOn(CommonStrings.action_decline) eventsRecorder.assertSingle(DeclineAndBlockEvents.Decline) } @Test - fun `clicking on decline when disabled does not emit event`() { + fun `clicking on decline when disabled does not emit event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setDeclineAndBlockView( + setDeclineAndBlockView( aDeclineAndBlockState( blockUser = false, reportRoom = false, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_decline) + clickOn(CommonStrings.action_decline) eventsRecorder.assertEmpty() } @Test - fun `clicking on block option emits the expected event`() { + fun `clicking on block option emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDeclineAndBlockView( + setDeclineAndBlockView( aDeclineAndBlockState( blockUser = true, eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_decline_and_block_block_user_option_title) + clickOn(R.string.screen_decline_and_block_block_user_option_title) eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleBlockUser) } @Test - fun `clicking on report room option emits the expected event`() { + fun `clicking on report room option emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDeclineAndBlockView( + setDeclineAndBlockView( aDeclineAndBlockState( reportRoom = true, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_report_room) + clickOn(CommonStrings.action_report_room) eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleReportRoom) } @Test - fun `typing text in the reason field emits the expected Event`() { + fun `typing text in the reason field emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDeclineAndBlockView( + setDeclineAndBlockView( aDeclineAndBlockState( reportRoom = true, reportReason = "", eventSink = eventsRecorder, ), ) - rule.onNodeWithText("").performTextInput("Spam!") + onNodeWithText("").performTextInput("Spam!") eventsRecorder.assertSingle(DeclineAndBlockEvents.UpdateReportReason("Spam!")) } } -private fun AndroidComposeTestRule.setDeclineAndBlockView( +private fun AndroidComposeUiTest.setDeclineAndBlockView( state: DeclineAndBlockState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt index 0a3b1ca3c6..e60d7da691 100644 --- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.joinroom.impl import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.invite.api.InviteData import io.element.android.features.invite.test.anInviteData @@ -26,116 +29,112 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class JoinRoomViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on back invoke the expected callback`() { + fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( eventSink = eventsRecorder, ), onBackClick = it ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on Join room on CanJoin room emits the expected Event`() { + fun `clicking on Join room on CanJoin room emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin), eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_join_room_join_action) + clickOn(R.string.screen_join_room_join_action) eventsRecorder.assertSingle(JoinRoomEvents.JoinRoom) } @Test - fun `clicking on Knock room on CanKnock room emits the expected Event`() { + fun `clicking on Knock room on CanKnock room emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock), knockMessage = "Knock knock", eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_join_room_knock_action) + clickOn(R.string.screen_join_room_knock_action) eventsRecorder.assertSingle(JoinRoomEvents.KnockRoom) } @Test - fun `clicking on closing Knock error emits the expected Event`() { + fun `clicking on closing Knock error emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock), knockAction = AsyncAction.Failure(Exception("Error")), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates) } @Test - fun `clicking on cancel knock request emit the expected Event`() { + fun `clicking on cancel knock request emit the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked), eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_join_room_cancel_knock_action) + clickOn(R.string.screen_join_room_cancel_knock_action) eventsRecorder.assertSingle(JoinRoomEvents.CancelKnock(true)) } @Test - fun `clicking on closing Cancel Knock error emits the expected Event`() { + fun `clicking on closing Cancel Knock error emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked), cancelKnockAction = AsyncAction.Failure(Exception("Error")), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates) } @Test - fun `clicking on closing Join error emits the expected Event`() { + fun `clicking on closing Join error emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock), joinAction = AsyncAction.Failure(Exception("Error")), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates) } @Test - fun `when joining room is successful, the expected callback is invoked`() { + fun `when joining room is successful, the expected callback is invoked`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( joinAction = AsyncAction.Success(Unit), eventSink = eventsRecorder, @@ -146,53 +145,55 @@ class JoinRoomViewTest { } @Test - fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() { + fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val inviteData = anInviteData() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, null)), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_accept) + clickOn(CommonStrings.action_accept) eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite(inviteData)) } @Test - fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() { + fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val inviteData = anInviteData() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, null)), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_decline) + clickOn(CommonStrings.action_decline) eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, false)) } @Test fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and can report room, the expected callback is invoked`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - val inviteData = anInviteData() - val joinRoomState = aJoinRoomState( - contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())), - canReportRoom = true, - eventSink = eventsRecorder, - ) - ensureCalledOnceWithParam(inviteData) { - rule.setJoinRoomView( - state = joinRoomState, - onDeclineInviteAndBlockUser = it, + runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder(expectEvents = false) + val inviteData = anInviteData() + val joinRoomState = aJoinRoomState( + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())), + canReportRoom = true, + eventSink = eventsRecorder, ) - rule.clickOn(R.string.screen_join_room_decline_and_block_button_title) + ensureCalledOnceWithParam(inviteData) { + setJoinRoomView( + state = joinRoomState, + onDeclineInviteAndBlockUser = it, + ) + clickOn(R.string.screen_join_room_decline_and_block_button_title) + } } } @Test - fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and cant report room, emits the expected Event`() { + fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and cant report room, emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val inviteData = anInviteData() val joinRoomState = aJoinRoomState( @@ -200,29 +201,29 @@ class JoinRoomViewTest { canReportRoom = false, eventSink = eventsRecorder, ) - rule.setJoinRoomView(state = joinRoomState) - rule.clickOn(R.string.screen_join_room_decline_and_block_button_title) + setJoinRoomView(state = joinRoomState) + clickOn(R.string.screen_join_room_decline_and_block_button_title) eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, true)) } @Test - fun `clicking on Retry when an error occurs emits the expected Event`() { + fun `clicking on Retry when an error occurs emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aFailureContentState(), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_retry) + clickOn(CommonStrings.action_retry) eventsRecorder.assertSingle(JoinRoomEvents.RetryFetchingContent) } @Test - fun `clicking on ok when user is unauthorized the expected callback`() { + fun `clicking on ok when user is unauthorized the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(), joinAction = AsyncAction.Failure(JoinRoom.Failures.UnauthorizedJoin), @@ -230,25 +231,25 @@ class JoinRoomViewTest { ), onBackClick = it ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) } } @Test - fun `clicking on forget when user is banned invokes the expected callback`() { + fun `clicking on forget when user is banned invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(null, null)), eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_join_room_forget_action) + clickOn(R.string.screen_join_room_forget_action) eventsRecorder.assertSingle(JoinRoomEvents.ForgetRoom) } } -private fun AndroidComposeTestRule.setJoinRoomView( +private fun AndroidComposeUiTest.setJoinRoomView( state: JoinRoomState, onBackClick: () -> Unit = EnsureNeverCalled(), onJoinSuccess: () -> Unit = EnsureNeverCalled(), diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt index a9fea0905e..fc1600d8c8 100644 --- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt @@ -6,13 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.knockrequests.impl.banner import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.knockrequests.impl.R import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable @@ -21,35 +24,30 @@ import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class KnockRequestsBannerViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `clicking on view on single request invoke the expected callback`() { + fun `clicking on view on single request invoke the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setKnockRequestsBannerView( + setKnockRequestsBannerView( state = aKnockRequestsBannerState( eventSink = eventsRecorder, ), onViewRequestsClick = it ) - rule.clickOn(R.string.screen_room_single_knock_request_view_button_title) + clickOn(R.string.screen_room_single_knock_request_view_button_title) } } @Test - fun `clicking on view all when multiple requests invoke the expected callback`() { + fun `clicking on view all when multiple requests invoke the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setKnockRequestsBannerView( + setKnockRequestsBannerView( state = aKnockRequestsBannerState( knockRequests = listOf( aKnockRequestPresentable(displayName = "Alice"), @@ -60,37 +58,37 @@ class KnockRequestsBannerViewTest { ), onViewRequestsClick = it ) - rule.clickOn(R.string.screen_room_multiple_knock_requests_view_all_button_title) + clickOn(R.string.screen_room_multiple_knock_requests_view_all_button_title) } } @Test - fun `clicking on accept on a single request emit the expected event`() { + fun `clicking on accept on a single request emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setKnockRequestsBannerView( + setKnockRequestsBannerView( state = aKnockRequestsBannerState( eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_accept) + clickOn(CommonStrings.action_accept) eventsRecorder.assertSingle(KnockRequestsBannerEvents.AcceptSingleRequest) } @Test - fun `clicking on dismiss emit the expected event`() { + fun `clicking on dismiss emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setKnockRequestsBannerView( + setKnockRequestsBannerView( state = aKnockRequestsBannerState( eventSink = eventsRecorder, ), ) - val close = rule.activity.getString(CommonStrings.action_close) - rule.onNodeWithContentDescription(close).performClick() + val close = activity!!.getString(CommonStrings.action_close) + onNodeWithContentDescription(close).performClick() eventsRecorder.assertSingle(KnockRequestsBannerEvents.Dismiss) } } -private fun AndroidComposeTestRule.setKnockRequestsBannerView( +private fun AndroidComposeUiTest.setKnockRequestsBannerView( state: KnockRequestsBannerState, onViewRequestsClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt index 188dcc7e56..14cac7a9b7 100644 --- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.knockrequests.impl.list import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.knockrequests.impl.R import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable @@ -23,90 +26,86 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import kotlinx.collections.immutable.persistentListOf -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class KnockRequestsListViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on back invoke the expected callback`() { + fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setKnockRequestsListView( + setKnockRequestsListView( aKnockRequestsListState( eventSink = eventsRecorder, ), onBackClick = it ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on accept emit the expected event`() { + fun `clicking on accept emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val knockRequest = aKnockRequestPresentable() - rule.setKnockRequestsListView( + setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(persistentListOf(knockRequest)), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_accept) + clickOn(CommonStrings.action_accept) eventsRecorder.assertSingle(KnockRequestsListEvents.Accept(knockRequest)) } @Test - fun `clicking on decline emit the expected event`() { + fun `clicking on decline emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val knockRequest = aKnockRequestPresentable() - rule.setKnockRequestsListView( + setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(persistentListOf(knockRequest)), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_decline) + clickOn(CommonStrings.action_decline) eventsRecorder.assertSingle(KnockRequestsListEvents.Decline(knockRequest)) } @Test - fun `clicking on decline and ban emit the expected event`() { + fun `clicking on decline and ban emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val knockRequest = aKnockRequestPresentable() - rule.setKnockRequestsListView( + setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(persistentListOf(knockRequest)), eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_knock_requests_list_decline_and_ban_action_title) + clickOn(R.string.screen_knock_requests_list_decline_and_ban_action_title) eventsRecorder.assertSingle(KnockRequestsListEvents.DeclineAndBan(knockRequest)) } @Test - fun `clicking on accept all emit the expected event`() { + fun `clicking on accept all emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) - rule.setKnockRequestsListView( + setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_knock_requests_list_accept_all_button_title) + clickOn(R.string.screen_knock_requests_list_accept_all_button_title) eventsRecorder.assertSingle(KnockRequestsListEvents.AcceptAll) } @Test - fun `retry on async view retry emit the expected event`() { + fun `retry on async view retry emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) - rule.setKnockRequestsListView( + setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), asyncAction = AsyncAction.Failure(RuntimeException("Failed to accept all")), @@ -114,15 +113,15 @@ class KnockRequestsListViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_retry) + clickOn(CommonStrings.action_retry) eventsRecorder.assertSingle(KnockRequestsListEvents.RetryCurrentAction) } @Test - fun `canceling async view emit the expected event`() { + fun `canceling async view emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) - rule.setKnockRequestsListView( + setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), asyncAction = AsyncAction.Failure(RuntimeException("Failed to accept all")), @@ -130,15 +129,15 @@ class KnockRequestsListViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(KnockRequestsListEvents.ResetCurrentAction) } @Test - fun `confirming async view emit the expected event`() { + fun `confirming async view emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) - rule.setKnockRequestsListView( + setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), asyncAction = AsyncAction.ConfirmingNoParams, @@ -146,12 +145,12 @@ class KnockRequestsListViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title) + clickOn(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title) eventsRecorder.assertSingle(KnockRequestsListEvents.ConfirmCurrentAction) } } -private fun AndroidComposeTestRule.setKnockRequestsListView( +private fun AndroidComposeUiTest.setKnockRequestsListView( state: KnockRequestsListState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeViewTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeViewTest.kt index ac0a129f49..7609acf809 100644 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeViewTest.kt +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeViewTest.kt @@ -5,11 +5,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.linknewdevice.impl.screens.desktop import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.linknewdevice.impl.R import io.element.android.tests.testutils.EnsureNeverCalled @@ -18,42 +21,37 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class DesktopNoticeViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `on back pressed - calls the expected callback`() { + fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setView( + setView( state = aDesktopNoticeState(), onBackClicked = callback, ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `on back button clicked - calls the expected callback`() { + fun `on back button clicked - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setView( + setView( state = aDesktopNoticeState(), onBackClicked = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `when can continue - calls the expected callback`() { + fun `when can continue - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setView( + setView( state = aDesktopNoticeState(canContinue = true), onReadyToScanClick = callback, ) @@ -61,16 +59,16 @@ class DesktopNoticeViewTest { } @Test - fun `on submit button clicked - emits the Continue event`() { + fun `on submit button clicked - emits the Continue event`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder() - rule.setView( + setView( state = aDesktopNoticeState(eventSink = eventRecorder), ) - rule.clickOn(R.string.screen_link_new_device_desktop_submit) + clickOn(R.string.screen_link_new_device_desktop_submit) eventRecorder.assertSingle(DesktopNoticeEvent.Continue) } - private fun AndroidComposeTestRule.setView( + private fun AndroidComposeUiTest.setView( state: DesktopNoticeState, onBackClicked: () -> Unit = EnsureNeverCalled(), onReadyToScanClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorViewTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorViewTest.kt index aa52a70149..b63d7471ac 100644 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorViewTest.kt +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorViewTest.kt @@ -5,58 +5,56 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.linknewdevice.impl.screens.error import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ErrorViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `on back pressed - calls the onCancel callback`() { + fun `on back pressed - calls the onCancel callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setErrorView( + setErrorView( onCancel = callback, ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `on try again button clicked - calls the expected callback`() { + fun `on try again button clicked - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setErrorView( + setErrorView( onRetry = callback ) - rule.clickOn(CommonStrings.action_try_again) + clickOn(CommonStrings.action_try_again) } } @Test - fun `on cancel button clicked - calls the expected callback`() { + fun `on cancel button clicked - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setErrorView( + setErrorView( onCancel = callback ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) } } - private fun AndroidComposeTestRule.setErrorView( + private fun AndroidComposeUiTest.setErrorView( onRetry: () -> Unit = EnsureNeverCalled(), onCancel: () -> Unit = EnsureNeverCalled(), errorScreenType: ErrorScreenType = ErrorScreenType.UnknownError, diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberViewTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberViewTest.kt index 20e1d898dd..25dc9efa8a 100644 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberViewTest.kt +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberViewTest.kt @@ -5,13 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.linknewdevice.impl.screens.number import androidx.activity.ComponentActivity +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsNotEnabled -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -20,65 +23,60 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class EnterNumberViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `on back pressed - calls the expected callback`() { + fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setView( + setView( state = aEnterNumberState(), onBackClicked = callback, ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `on back button clicked - calls the expected callback`() { + fun `on back button clicked - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setView( + setView( state = aEnterNumberState(), onBackClicked = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `on continue button clicked - emits the Continue event`() { + fun `on continue button clicked - emits the Continue event`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder() - rule.setView( + setView( state = aEnterNumberState( number = "12", eventSink = eventRecorder, ), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventRecorder.assertSingle(EnterNumberEvent.Continue) } @Test - fun `when the number is not complete, continue button is disabled`() { + fun `when the number is not complete, continue button is disabled`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( state = aEnterNumberState( number = "1", eventSink = eventRecorder, ), ) - val continueStr = rule.activity.getString(CommonStrings.action_continue) - rule.onNodeWithText(continueStr).assertIsNotEnabled() + val continueStr = activity!!.getString(CommonStrings.action_continue) + onNodeWithText(continueStr).assertIsNotEnabled() } - private fun AndroidComposeTestRule.setView( + private fun AndroidComposeUiTest.setView( state: EnterNumberState, onBackClicked: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeViewTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeViewTest.kt index c6c89ba818..d552c2bff6 100644 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeViewTest.kt +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeViewTest.kt @@ -5,36 +5,34 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.linknewdevice.impl.screens.qrcode import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ShowQrCodeViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `on back pressed - calls the expected callback`() { + fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setView( + setView( onBackClick = callback ) - rule.pressBackKey() + pressBackKey() } } - private fun AndroidComposeTestRule.setView( + private fun AndroidComposeUiTest.setView( onBackClick: () -> Unit = EnsureNeverCalled(), ) { setContent { diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootViewTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootViewTest.kt index e352debfb0..bceb8753b2 100644 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootViewTest.kt +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootViewTest.kt @@ -5,11 +5,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.linknewdevice.impl.screens.root import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.linknewdevice.impl.R import io.element.android.libraries.architecture.AsyncData @@ -19,74 +22,69 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class LinkNewDeviceRootViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `on back pressed - calls the onRetry callback`() { + fun `on back pressed - calls the onRetry callback`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setLinkNewDeviceRootView( + setLinkNewDeviceRootView( state = aLinkNewDeviceRootState( eventSink = eventRecorder, ), onBackClick = callback ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `link desktop button clicked - calls the expected callback`() { + fun `link desktop button clicked - calls the expected callback`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setLinkNewDeviceRootView( + setLinkNewDeviceRootView( state = aLinkNewDeviceRootState( isSupported = AsyncData.Success(true), eventSink = eventRecorder, ), onLinkDesktopDeviceClick = callback, ) - rule.clickOn(R.string.screen_link_new_device_root_desktop_computer) + clickOn(R.string.screen_link_new_device_root_desktop_computer) } } @Test - fun `link mobile button clicked - emits the expected event`() { + fun `link mobile button clicked - emits the expected event`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder() - rule.setLinkNewDeviceRootView( + setLinkNewDeviceRootView( state = aLinkNewDeviceRootState( isSupported = AsyncData.Success(true), eventSink = eventRecorder, ) ) - rule.clickOn(R.string.screen_link_new_device_root_mobile_device) + clickOn(R.string.screen_link_new_device_root_mobile_device) eventRecorder.assertSingle(LinkNewDeviceRootEvent.LinkMobileDevice) } @Test - fun `not supported - dismiss click - invokes the expected callback`() { + fun `not supported - dismiss click - invokes the expected callback`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setLinkNewDeviceRootView( + setLinkNewDeviceRootView( state = aLinkNewDeviceRootState( isSupported = AsyncData.Success(false), eventSink = eventRecorder, ), onBackClick = callback, ) - rule.clickOn(CommonStrings.action_dismiss) + clickOn(CommonStrings.action_dismiss) } } - private fun AndroidComposeTestRule.setLinkNewDeviceRootView( + private fun AndroidComposeUiTest.setLinkNewDeviceRootView( state: LinkNewDeviceRootState = aLinkNewDeviceRootState(), onBackClick: () -> Unit = EnsureNeverCalled(), onLinkDesktopDeviceClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeViewTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeViewTest.kt index fcc3afeb7d..1932718fef 100644 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeViewTest.kt +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeViewTest.kt @@ -5,11 +5,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.linknewdevice.impl.screens.scan import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.test.AN_EXCEPTION @@ -19,44 +22,39 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ScanQrCodeViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `on back pressed - calls the expected callback`() { + fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( state = aScanQrCodeState( eventSink = eventRecorder, ), onBackClick = callback ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `try again button clicked - emits the expected event`() { + fun `try again button clicked - emits the expected event`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder() - rule.setView( + setView( state = aScanQrCodeState( scanAction = AsyncAction.Failure(AN_EXCEPTION), eventSink = eventRecorder, ) ) - rule.clickOn(CommonStrings.action_try_again) + clickOn(CommonStrings.action_try_again) eventRecorder.assertSingle(ScanQrCodeEvent.TryAgain) } - private fun AndroidComposeTestRule.setView( + private fun AndroidComposeUiTest.setView( state: ScanQrCodeState = aScanQrCodeState(), onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationViewTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationViewTest.kt index 317fbf8fed..63c19ba913 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationViewTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationViewTest.kt @@ -5,15 +5,18 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.location.impl.share import androidx.activity.ComponentActivity import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.libraries.testtags.TestTags @@ -23,102 +26,98 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ShareLocationViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `test back action`() { + fun `test back action`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setShareLocationView( + setShareLocationView( state = aShareLocationState( eventSink = eventsRecorder ), navigateUp = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `test fab click`() { + fun `test fab click`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShareLocationView( + setShareLocationView( aShareLocationState( eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - rule.onNodeWithTag(TestTags.floatingActionButton.value).performClick() + onNodeWithTag(TestTags.floatingActionButton.value).performClick() eventsRecorder.assertSingle(ShareLocationEvent.StartTrackingUserLocation) } @Test - fun `when permission denied is displayed user can open the settings`() { + fun `when permission denied is displayed user can open the settings`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShareLocationView( + setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied), eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ShareLocationEvent.OpenAppSettings) } @Test - fun `when permission denied is displayed user can close the dialog`() { + fun `when permission denied is displayed user can close the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShareLocationView( + setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied), eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog) } @Test - fun `when permission rationale is displayed user can request permissions`() { + fun `when permission rationale is displayed user can request permissions`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShareLocationView( + setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale), eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ShareLocationEvent.RequestPermissions) } @Test - fun `when permission rationale is displayed user can close the dialog`() { + fun `when permission rationale is displayed user can close the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShareLocationView( + setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale), eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog) } @Test - fun `when location service disabled is displayed user can open location settings`() { + fun `when location service disabled is displayed user can open location settings`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShareLocationView( + setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled), hasLocationPermission = true, @@ -126,14 +125,14 @@ class ShareLocationViewTest { ), navigateUp = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ShareLocationEvent.OpenLocationSettings) } @Test - fun `when location service disabled is displayed user can close the dialog`() { + fun `when location service disabled is displayed user can close the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShareLocationView( + setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled), hasLocationPermission = true, @@ -141,12 +140,12 @@ class ShareLocationViewTest { ), navigateUp = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog) } } -private fun AndroidComposeTestRule.setShareLocationView( +private fun AndroidComposeUiTest.setShareLocationView( state: ShareLocationState, navigateUp: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt index fecbbdbf89..45ed894f97 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt @@ -6,16 +6,19 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.location.impl.show import androidx.activity.ComponentActivity import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.location.api.Location import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState @@ -26,115 +29,111 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ShowLocationViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `test back action`() { + fun `test back action`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setShowLocationView( + setShowLocationView( state = aShowLocationState( eventSink = eventsRecorder ), onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `test share action`() { + fun `test share action`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShowLocationView( + setShowLocationView( aShowLocationState( eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - val shareContentDescription = rule.activity.getString(CommonStrings.action_share) - rule.onNodeWithContentDescription(shareContentDescription).performClick() + val shareContentDescription = activity!!.getString(CommonStrings.action_share) + onNodeWithContentDescription(shareContentDescription).performClick() // The default aStaticLocationMode uses Location(1.23, 2.34, 4f) eventsRecorder.assertSingle(ShowLocationEvent.Share(Location(1.23, 2.34, 4f))) } @Test - fun `test fab click`() { + fun `test fab click`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShowLocationView( + setShowLocationView( aShowLocationState( eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - rule.onNodeWithTag(TestTags.floatingActionButton.value).performClick() + onNodeWithTag(TestTags.floatingActionButton.value).performClick() eventsRecorder.assertSingle(ShowLocationEvent.TrackMyLocation(true)) } @Test - fun `when permission denied is displayed user can open the settings`() { + fun `when permission denied is displayed user can open the settings`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShowLocationView( + setShowLocationView( aShowLocationState( constraintsDialogState = LocationConstraintsDialogState.PermissionDenied, eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ShowLocationEvent.OpenAppSettings) } @Test - fun `when permission denied is displayed user can close the dialog`() { + fun `when permission denied is displayed user can close the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShowLocationView( + setShowLocationView( aShowLocationState( constraintsDialogState = LocationConstraintsDialogState.PermissionDenied, eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ShowLocationEvent.DismissDialog) } @Test - fun `when permission rationale is displayed user can request permissions`() { + fun `when permission rationale is displayed user can request permissions`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShowLocationView( + setShowLocationView( aShowLocationState( constraintsDialogState = LocationConstraintsDialogState.PermissionRationale, eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ShowLocationEvent.RequestPermissions) } @Test - fun `when permission rationale is displayed user can close the dialog`() { + fun `when permission rationale is displayed user can close the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShowLocationView( + setShowLocationView( aShowLocationState( constraintsDialogState = LocationConstraintsDialogState.PermissionRationale, eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ShowLocationEvent.DismissDialog) } } -private fun AndroidComposeTestRule.setShowLocationView( +private fun AndroidComposeUiTest.setShowLocationView( state: ShowLocationState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadTest.kt index 1ecb79bd67..e6d1659778 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadTest.kt @@ -6,60 +6,57 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.lockscreen.impl.unlock.keypad import android.view.KeyEvent import androidx.activity.ComponentActivity import androidx.compose.ui.input.key.Key +import androidx.compose.ui.test.AndroidComposeUiTest import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasText import androidx.compose.ui.test.isRoot -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performKeyInput import androidx.compose.ui.test.pressKey import androidx.compose.ui.test.requestFocus +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.compose.ui.unit.dp import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class PinKeypadTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `clicking on a number emits the expected event`() { + fun `clicking on a number emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPinKeyPad(onClick = eventsRecorder) - rule.onNode(hasText("1")).performClick() + setPinKeyPad(onClick = eventsRecorder) + onNode(hasText("1")).performClick() eventsRecorder.assertSingle(PinKeypadModel.Number('1')) } @Test - fun `clicking on the delete previous character button emits the expected event`() { + fun `clicking on the delete previous character button emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPinKeyPad(onClick = eventsRecorder) - rule.onNode(hasContentDescription(rule.activity.getString(CommonStrings.a11y_delete))).performClick() + setPinKeyPad(onClick = eventsRecorder) + onNode(hasContentDescription(activity!!.getString(CommonStrings.a11y_delete))).performClick() eventsRecorder.assertSingle(PinKeypadModel.Back) } @OptIn(ExperimentalTestApi::class) @Test - fun `typing using the hardware keyboard emits the expected events`() { + fun `typing using the hardware keyboard emits the expected events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPinKeyPad(onClick = eventsRecorder) - rule.onNodeWithText("1").requestFocus() - rule.onAllNodes(isRoot())[0].performKeyInput { + setPinKeyPad(onClick = eventsRecorder) + onNodeWithText("1").requestFocus() + onAllNodes(isRoot())[0].performKeyInput { val keys = listOf( Key.A, Key.NumPad1, @@ -118,7 +115,7 @@ class PinKeypadTest { ) } - private fun AndroidComposeTestRule.setPinKeyPad( + private fun AndroidComposeUiTest.setPinKeyPad( onClick: (PinKeypadModel) -> Unit = EnsureNeverCalledWithParam(), ) { setContent { diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt index c6610b212c..61ec7cc698 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt @@ -6,13 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.login.impl.screens.chooseaccountprovider import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.libraries.architecture.AsyncData @@ -25,36 +28,31 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class ChooseAccountProviderViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setChooseAccountProviderView( + setChooseAccountProviderView( state = aChooseAccountProviderState( eventSink = eventSink, ), onBackClick = it, ) - rule.pressBack() + pressBack() } } @Config(qualifiers = "h1024dp") @Test - fun `selecting an account provider emits the the expected event`() { + fun `selecting an account provider emits the the expected event`() = runAndroidComposeUiTest { val eventSink = EventsRecorder() - rule.setChooseAccountProviderView( + setChooseAccountProviderView( state = aChooseAccountProviderState( accountProviders = listOf( ChooseAccountProviderPresenterTest.accountProvider1, @@ -64,24 +62,24 @@ class ChooseAccountProviderViewTest { eventSink = eventSink, ), ) - rule.onNodeWithText(ChooseAccountProviderPresenterTest.accountProvider1.title).performClick() + onNodeWithText(ChooseAccountProviderPresenterTest.accountProvider1.title).performClick() eventSink.assertSingle(ChooseAccountProviderEvents.SelectAccountProvider(ChooseAccountProviderPresenterTest.accountProvider1)) } @Test - fun `when error is displayed - closing the dialog emits the expected event`() { + fun `when error is displayed - closing the dialog emits the expected event`() = runAndroidComposeUiTest { val eventSink = EventsRecorder() - rule.setChooseAccountProviderView( + setChooseAccountProviderView( state = aChooseAccountProviderState( loginMode = AsyncData.Failure(AN_EXCEPTION), eventSink = eventSink, ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventSink.assertSingle(ChooseAccountProviderEvents.ClearError) } - private fun AndroidComposeTestRule.setChooseAccountProviderView( + private fun AndroidComposeUiTest.setChooseAccountProviderView( state: ChooseAccountProviderState, onBackClick: () -> Unit = EnsureNeverCalled(), onOAuthDetails: (OAuthDetails) -> Unit = EnsureNeverCalledWithParam(), diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt index 26da50da63..c0e7e5c378 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt @@ -6,20 +6,23 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.login.impl.screens.loginpassword import androidx.activity.ComponentActivity +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.matrix.test.A_PASSWORD import io.element.android.libraries.matrix.test.A_USER_NAME @@ -30,158 +33,154 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class LoginPasswordViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `clicking on back invoke back callback`() { + fun `clicking on back invoke back callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( eventSink = eventsRecorder ), onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `changing login invokes the expected event`() { + fun `changing login invokes the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( eventSink = eventsRecorder, ), ) - val userNameHint = rule.activity.getString(CommonStrings.common_username) - rule.onNodeWithText(userNameHint).performTextInput(A_USER_NAME) + val userNameHint = activity!!.getString(CommonStrings.common_username) + onNodeWithText(userNameHint).performTextInput(A_USER_NAME) eventsRecorder.assertSingle( LoginPasswordEvents.SetLogin(A_USER_NAME) ) } @Test - fun `changing login removes new lines the expected event`() { + fun `changing login removes new lines the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( eventSink = eventsRecorder, ), ) - val userNameHint = rule.activity.getString(CommonStrings.common_username) - rule.onNodeWithText(userNameHint).performTextInput("a\nb") + val userNameHint = activity!!.getString(CommonStrings.common_username) + onNodeWithText(userNameHint).performTextInput("a\nb") eventsRecorder.assertSingle( LoginPasswordEvents.SetLogin("ab") ) } @Test - fun `clearing login invokes the expected event`() { + fun `clearing login invokes the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(A_USER_NAME), eventSink = eventsRecorder, ), ) - val a11yClear = rule.activity.getString(CommonStrings.action_clear) - rule.onNodeWithContentDescription(a11yClear).performClick() + val a11yClear = activity!!.getString(CommonStrings.action_clear) + onNodeWithContentDescription(a11yClear).performClick() eventsRecorder.assertSingle( LoginPasswordEvents.SetLogin("") ) } @Test - fun `changing password invokes the expected event`() { + fun `changing password invokes the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( eventSink = eventsRecorder, ), ) - val userNameHint = rule.activity.getString(CommonStrings.common_password) - rule.onNodeWithText(userNameHint).performTextInput(A_PASSWORD) + val userNameHint = activity!!.getString(CommonStrings.common_password) + onNodeWithText(userNameHint).performTextInput(A_PASSWORD) eventsRecorder.assertSingle( LoginPasswordEvents.SetPassword(A_PASSWORD) ) } @Test - fun `reveal password makes the password visible`() { + fun `reveal password makes the password visible`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(password = A_PASSWORD), eventSink = eventsRecorder, ), ) - rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) + onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) + val resources = activity!!.resources // Show password - val a11yShowPassword = rule.activity.getString(CommonStrings.a11y_show_password) - rule.onNodeWithContentDescription(a11yShowPassword).performClick() - rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText(A_PASSWORD)) + val a11yShowPassword = resources.getString(CommonStrings.a11y_show_password) + onNodeWithContentDescription(a11yShowPassword).performClick() + onNodeWithTag(TestTags.loginPassword.value).assert(hasText(A_PASSWORD)) // Hide password - val a11yHidePassword = rule.activity.getString(CommonStrings.a11y_hide_password) - rule.onNodeWithContentDescription(a11yHidePassword).performClick() - rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) + val a11yHidePassword = resources.getString(CommonStrings.a11y_hide_password) + onNodeWithContentDescription(a11yHidePassword).performClick() + onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) } @Test - fun `when login is empty, continue button is not enabled`() { + fun `when login is empty, continue button is not enabled`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(password = A_PASSWORD), eventSink = eventsRecorder, ), ) - val continueStr = rule.activity.getString(CommonStrings.action_continue) - rule.onNodeWithText(continueStr).assertIsNotEnabled() + val continueStr = activity!!.getString(CommonStrings.action_continue) + onNodeWithText(continueStr).assertIsNotEnabled() } @Test - fun `when password is empty, continue button is not enabled`() { + fun `when password is empty, continue button is not enabled`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(login = A_USER_NAME), eventSink = eventsRecorder, ), ) - val continueStr = rule.activity.getString(CommonStrings.action_continue) - rule.onNodeWithText(continueStr).assertIsNotEnabled() + val continueStr = activity!!.getString(CommonStrings.action_continue) + onNodeWithText(continueStr).assertIsNotEnabled() } @Config(qualifiers = "h1024dp") @Test - fun `clicking on Continue sends expected event`() { + fun `clicking on Continue sends expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(login = A_USER_NAME, password = A_PASSWORD), eventSink = eventsRecorder, ), ) - val continueStr = rule.activity.getString(CommonStrings.action_continue) - rule.onNodeWithText(continueStr).assertIsEnabled() - rule.clickOn(CommonStrings.action_continue) + val continueStr = activity!!.getString(CommonStrings.action_continue) + onNodeWithText(continueStr).assertIsEnabled() + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle( LoginPasswordEvents.Submit ) } } -private fun AndroidComposeTestRule.setLoginPasswordView( +private fun AndroidComposeUiTest.setLoginPasswordView( state: LoginPasswordState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt index a8f0ccbb5a..bcb62ea707 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt @@ -6,14 +6,17 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.login.impl.screens.onboarding import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import com.google.testing.junit.testparameterinjector.KotlinTestParameters.namedTestValues import com.google.testing.junit.testparameterinjector.TestParameter import io.element.android.features.login.impl.R @@ -29,22 +32,17 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.RobolectricTestParameterInjector @RunWith(RobolectricTestParameterInjector::class) class OnboardingViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `when can create account - clicking on create account calls the expected callback`() { + fun `when can create account - clicking on create account calls the expected callback`() = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( canCreateAccount = true, showDeveloperSettings = false, @@ -52,40 +50,40 @@ class OnboardingViewTest { ), onCreateAccount = callback, ) - rule.clickOn(R.string.screen_onboarding_sign_up) + clickOn(R.string.screen_onboarding_sign_up) // Developer settings should not be shown - val developerSettingsText = rule.activity.getString(CommonStrings.common_developer_options) - rule.onNodeWithContentDescription(developerSettingsText).assertDoesNotExist() + val developerSettingsText = activity!!.getString(CommonStrings.common_developer_options) + onNodeWithContentDescription(developerSettingsText).assertDoesNotExist() } } @Test - fun `when can go back - clicking on back calls the expected callback`() { + fun `when can go back - clicking on back calls the expected callback`() = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( isAddingAccount = true, eventSink = eventSink, ), onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `when can login with QR code - clicking on sign in with QR code calls the expected callback`() { + fun `when can login with QR code - clicking on sign in with QR code calls the expected callback`() = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( canLoginWithQrCode = true, eventSink = eventSink, ), onSignInWithQrCode = callback, ) - rule.clickOn(R.string.screen_onboarding_sign_in_with_qr_code) + clickOn(R.string.screen_onboarding_sign_in_with_qr_code) } } @@ -95,10 +93,10 @@ class OnboardingViewTest { "can search account provider" to false, "cannot search account provider" to true, ) - ) { + ) = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( canLoginWithQrCode = true, mustChooseAccountProvider = mustChooseAccountProvider, @@ -106,7 +104,7 @@ class OnboardingViewTest { ), onSignIn = callback, ) - rule.clickOn(R.string.screen_onboarding_sign_in_manually) + clickOn(R.string.screen_onboarding_sign_in_manually) } } @@ -116,10 +114,10 @@ class OnboardingViewTest { "can search account provider" to false, "cannot search account provider" to true, ) - ) { + ) = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( canLoginWithQrCode = false, canCreateAccount = false, @@ -128,89 +126,89 @@ class OnboardingViewTest { ), onSignIn = callback, ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) } } @Test - fun `when sign in to pre defined account provider - clicking on button emits the expected event`() { + fun `when sign in to pre defined account provider - clicking on button emits the expected event`() = runAndroidComposeUiTest { val eventSink = EventsRecorder() - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( defaultAccountProvider = "element.io", eventSink = eventSink, ), ) - val buttonText = rule.activity.getString(R.string.screen_onboarding_sign_in_to, "element.io") - rule.onNodeWithText(buttonText).performClick() + val buttonText = activity!!.getString(R.string.screen_onboarding_sign_in_to, "element.io") + onNodeWithText(buttonText).performClick() eventSink.assertSingle(OnBoardingEvents.OnSignIn("element.io")) } @Test - fun `when error is displayed - closing the dialog emits the expected event`() { + fun `when error is displayed - closing the dialog emits the expected event`() = runAndroidComposeUiTest { val eventSink = EventsRecorder() - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( defaultAccountProvider = "element.io", loginMode = AsyncData.Failure(AN_EXCEPTION), eventSink = eventSink, ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventSink.assertSingle(OnBoardingEvents.ClearError) } @Test - fun `clicking on report a problem calls the sign in callback`() { + fun `clicking on report a problem calls the sign in callback`() = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( canReportBug = true, eventSink = eventSink, ), onReportProblem = callback, ) - val text = rule.activity.getString(CommonStrings.common_report_a_problem) - rule.onNodeWithText(text).assertExists() - rule.clickOn(CommonStrings.common_report_a_problem) + val text = activity!!.getString(CommonStrings.common_report_a_problem) + onNodeWithText(text).assertExists() + clickOn(CommonStrings.common_report_a_problem) } } @Test - fun `clicking on settings calls the developer settings callback`() { + fun `clicking on settings calls the developer settings callback`() = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( showDeveloperSettings = true, eventSink = eventSink, ), onDeveloperSettingsClick = callback, ) - val text = rule.activity.getString(CommonStrings.common_developer_options) - rule.onNodeWithContentDescription(text).performClick() + val text = activity!!.getString(CommonStrings.common_developer_options) + onNodeWithContentDescription(text).performClick() } } @Test - fun `cannot report a problem when the feature is disabled`() { + fun `cannot report a problem when the feature is disabled`() = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( canReportBug = false, eventSink = eventSink, ), ) - val text = rule.activity.getString(CommonStrings.common_report_a_problem) - rule.onNodeWithText(text).assertDoesNotExist() + val text = activity!!.getString(CommonStrings.common_report_a_problem) + onNodeWithText(text).assertDoesNotExist() } @Test - fun `when success PasswordLogin - the expected callback is invoked and the event is received`() { + fun `when success PasswordLogin - the expected callback is invoked and the event is received`() = runAndroidComposeUiTest { val eventSink = EventsRecorder() ensureCalledOnce { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( loginMode = AsyncData.Success(LoginMode.PasswordLogin), eventSink = eventSink, @@ -222,11 +220,11 @@ class OnboardingViewTest { } @Test - fun `when success Oidc - the expected callback is invoked and the event is received`() { + fun `when success Oidc - the expected callback is invoked and the event is received`() = runAndroidComposeUiTest { val eventSink = EventsRecorder() val oAuthDetails = OAuthDetails("aUrl") ensureCalledOnceWithParam(oAuthDetails) { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails)), eventSink = eventSink, @@ -238,11 +236,11 @@ class OnboardingViewTest { } @Test - fun `when success AccountCreation - the expected callback is invoked and the event is received`() { + fun `when success AccountCreation - the expected callback is invoked and the event is received`() = runAndroidComposeUiTest { val eventSink = EventsRecorder() val oAuthDetails = OAuthDetails("aUrl") ensureCalledOnceWithParam(oAuthDetails.url) { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl")), eventSink = eventSink, @@ -253,7 +251,7 @@ class OnboardingViewTest { eventSink.assertSingle(OnBoardingEvents.ClearError) } - private fun AndroidComposeTestRule.setOnboardingView( + private fun AndroidComposeUiTest.setOnboardingView( state: OnBoardingState, onBackClick: () -> Unit = EnsureNeverCalled(), onDeveloperSettingsClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationViewTest.kt index a0469a684e..79566625c5 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationViewTest.kt @@ -6,49 +6,47 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.login.impl.screens.qrcode.confirmation import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class QrCodeConfirmationViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `on back pressed - calls the expected callback`() { + fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeConfirmationView( + setQrCodeConfirmationView( step = QrCodeConfirmationStep.DisplayCheckCode("12"), onCancel = callback ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `on Cancel button clicked - calls the expected callback`() { + fun `on Cancel button clicked - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeConfirmationView( + setQrCodeConfirmationView( step = QrCodeConfirmationStep.DisplayVerificationCode("123456"), onCancel = callback ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) } } - private fun AndroidComposeTestRule.setQrCodeConfirmationView( + private fun AndroidComposeUiTest.setQrCodeConfirmationView( step: QrCodeConfirmationStep, onCancel: () -> Unit ) { diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorViewTest.kt index de0f689220..2ae68c3485 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.login.impl.screens.qrcode.error import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType import io.element.android.libraries.ui.strings.CommonStrings @@ -18,47 +21,42 @@ import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class QrCodeErrorViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `on back pressed - calls the onCancel callback`() { + fun `on back pressed - calls the onCancel callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeErrorView( + setQrCodeErrorView( onCancel = callback, ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `on try again button clicked - calls the expected callback`() { + fun `on try again button clicked - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeErrorView( + setQrCodeErrorView( onRetry = callback, ) - rule.clickOn(CommonStrings.action_try_again) + clickOn(CommonStrings.action_try_again) } } @Test - fun `on cancel button clicked - calls the expected callback`() { + fun `on cancel button clicked - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeErrorView( + setQrCodeErrorView( onCancel = callback, ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) } } - private fun AndroidComposeTestRule.setQrCodeErrorView( + private fun AndroidComposeUiTest.setQrCodeErrorView( onRetry: () -> Unit = EnsureNeverCalled(), onCancel: () -> Unit = EnsureNeverCalled(), errorScreenType: QrCodeErrorScreenType = QrCodeErrorScreenType.UnknownError, diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroViewTest.kt index cec67e5011..c6812d3759 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.login.impl.screens.qrcode.intro import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.login.impl.R import io.element.android.tests.testutils.EnsureNeverCalled @@ -19,42 +22,37 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class QrCodeIntroViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `on back pressed - calls the expected callback`() { + fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeIntroView( + setQrCodeIntroView( state = aQrCodeIntroState(), onBackClicked = callback ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `on back button clicked - calls the expected callback`() { + fun `on back button clicked - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeIntroView( + setQrCodeIntroView( state = aQrCodeIntroState(), onBackClicked = callback ) - rule.pressBack() + pressBack() } } @Test - fun `when can continue - calls the expected callback`() { + fun `when can continue - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeIntroView( + setQrCodeIntroView( state = aQrCodeIntroState(canContinue = true), onContinue = callback ) @@ -62,16 +60,16 @@ class QrCodeIntroViewTest { } @Test - fun `on submit button clicked - emits the Continue event`() { + fun `on submit button clicked - emits the Continue event`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder() - rule.setQrCodeIntroView( + setQrCodeIntroView( state = aQrCodeIntroState(eventSink = eventRecorder), ) - rule.clickOn(R.string.screen_qr_code_login_initial_state_button_title) + clickOn(R.string.screen_qr_code_login_initial_state_button_title) eventRecorder.assertSingle(QrCodeIntroEvents.Continue) } - private fun AndroidComposeTestRule.setQrCodeIntroView( + private fun AndroidComposeUiTest.setQrCodeIntroView( state: QrCodeIntroState, onBackClicked: () -> Unit = EnsureNeverCalled(), onContinue: () -> Unit = EnsureNeverCalled(), diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanViewTest.kt index b8becd545f..bde960ef1a 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanViewTest.kt @@ -6,12 +6,15 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.login.impl.screens.qrcode.scan import androidx.activity.ComponentActivity import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import io.element.android.libraries.architecture.AsyncAction @@ -24,16 +27,11 @@ import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBackKey import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class QrCodeScanViewTest { - @get:Rule - val rule = createAndroidComposeRule() - private var provider: ProcessCameraProvider? = null @Before @@ -48,28 +46,28 @@ class QrCodeScanViewTest { } @Test - fun `on back pressed - calls the expected callback`() { + fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeScanView( + setQrCodeScanView( state = aQrCodeScanState(), onBackClick = callback ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `on QR code data ready - calls the expected callback`() { + fun `on QR code data ready - calls the expected callback`() = runAndroidComposeUiTest { val data = FakeMatrixQrCodeLoginData() ensureCalledOnceWithParam(data) { callback -> - rule.setQrCodeScanView( + setQrCodeScanView( state = aQrCodeScanState(authenticationAction = AsyncAction.Success(data)), onQrCodeDataReady = callback ) } } - private fun AndroidComposeTestRule.setQrCodeScanView( + private fun AndroidComposeUiTest.setQrCodeScanView( state: QrCodeScanState, onBackClick: () -> Unit = EnsureNeverCalled(), onQrCodeDataReady: (MatrixQrCodeLoginData) -> Unit = EnsureNeverCalledWithParam(), diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt index 84ca038d7b..a42fd891d4 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.logout.impl import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.testtags.TestTags @@ -21,97 +24,93 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressTag -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class LogoutViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on logout sends a LogoutEvents`() { + fun `clicking on logout sends a LogoutEvents`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLogoutView( + setLogoutView( aLogoutState( eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_signout) + clickOn(CommonStrings.action_signout) eventsRecorder.assertSingle(LogoutEvents.Logout(false)) } @Test - fun `confirming logout sends a LogoutEvents`() { + fun `confirming logout sends a LogoutEvents`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLogoutView( + setLogoutView( aLogoutState( logoutAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.pressTag(TestTags.dialogPositive.value) + pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(LogoutEvents.Logout(false)) } @Test - fun `clicking on back invoke back callback`() { + fun `clicking on back invoke back callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setLogoutView( + setLogoutView( aLogoutState( eventSink = eventsRecorder ), onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on confirm after error sends a LogoutEvents`() { + fun `clicking on confirm after error sends a LogoutEvents`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLogoutView( + setLogoutView( aLogoutState( logoutAction = AsyncAction.Failure(Exception("Failed to logout")), eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_signout_anyway) + clickOn(CommonStrings.action_signout_anyway) eventsRecorder.assertSingle(LogoutEvents.Logout(true)) } @Test - fun `clicking on cancel after error sends a LogoutEvents`() { + fun `clicking on cancel after error sends a LogoutEvents`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLogoutView( + setLogoutView( aLogoutState( logoutAction = AsyncAction.Failure(Exception("Failed to logout")), eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(LogoutEvents.CloseDialogs) } @Test - fun `last session setting button invoke onChangeRecoveryKeyClicked`() { + fun `last session setting button invoke onChangeRecoveryKeyClicked`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setLogoutView( + setLogoutView( aLogoutState( isLastDevice = true, eventSink = eventsRecorder ), onChangeRecoveryKeyClick = callback, ) - rule.clickOn(CommonStrings.common_settings) + clickOn(CommonStrings.common_settings) } } } -private fun AndroidComposeTestRule.setLogoutView( +private fun AndroidComposeUiTest.setLogoutView( state: LogoutState, onChangeRecoveryKeyClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutViewTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutViewTest.kt index 8eae534740..99860259c4 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutViewTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.logout.impl.direct import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.logout.api.direct.DirectLogoutState @@ -21,83 +24,79 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBackKey import org.junit.Ignore -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class DefaultDirectLogoutViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on confirm logout sends expected Event`() { + fun `clicking on confirm logout sends expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDefaultDirectLogoutView( + setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder, ) ) - rule.clickOn(CommonStrings.action_signout) + clickOn(CommonStrings.action_signout) eventsRecorder.assertSingle(DirectLogoutEvents.Logout(false)) } @Test - fun `clicking on cancel logout sends expected Event`() { + fun `clicking on cancel logout sends expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDefaultDirectLogoutView( + setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder, ) ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) } @Ignore("Pressing back key should dismiss the dialog, and so generate the expected event, but it's not the case.") @Test - fun `clicking on back invoke back callback`() { + fun `clicking on back invoke back callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDefaultDirectLogoutView( + setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder, ) ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) } @Test - fun `clicking on confirm after error sends expected Event`() { + fun `clicking on confirm after error sends expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDefaultDirectLogoutView( + setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.Failure(Exception("Error")), eventSink = eventsRecorder, ) ) - rule.clickOn(CommonStrings.action_signout_anyway) + clickOn(CommonStrings.action_signout_anyway) eventsRecorder.assertSingle(DirectLogoutEvents.Logout(true)) } @Test - fun `clicking on cancel after error sends expected Event`() { + fun `clicking on cancel after error sends expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDefaultDirectLogoutView( + setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.Failure(Exception("Error")), eventSink = eventsRecorder, ) ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) } } -private fun AndroidComposeTestRule.setDefaultDirectLogoutView( +private fun AndroidComposeUiTest.setDefaultDirectLogoutView( state: DirectLogoutState, ) { setContent { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index 62b9eac68d..70ef70325e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -6,13 +6,15 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl import androidx.activity.ComponentActivity import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.longClick import androidx.compose.ui.test.onAllNodesWithContentDescription import androidx.compose.ui.test.onAllNodesWithTag @@ -25,6 +27,7 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeRight +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.compose.ui.text.AnnotatedString import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.emojibasebindings.Emoji @@ -78,82 +81,78 @@ import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.setSafeContent import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config import kotlin.time.Duration.Companion.milliseconds @RunWith(AndroidJUnit4::class) class MessagesViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on back invoke expected callback`() { + fun `clicking on back invoke expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - rule.setMessagesView( + setMessagesView( state = state, onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on room name invoke expected callback`() { + fun `clicking on room name invoke expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - rule.setMessagesView( + setMessagesView( state = state, onRoomDetailsClick = callback, ) - rule.onNodeWithText(state.roomName.orEmpty(), useUnmergedTree = true).performClick() + onNodeWithText(state.roomName.orEmpty(), useUnmergedTree = true).performClick() } } @Test - fun `clicking on join call invoke expected callback`() { + fun `clicking on join call invoke expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( eventSink = eventsRecorder ) ensureCalledOnceWithParam(false) { callback -> - rule.setMessagesView( + setMessagesView( state = state, onJoinCallClick = callback, ) - val joinCallContentDescription = rule.activity.getString(CommonStrings.a11y_start_call) - rule.onNodeWithContentDescription(joinCallContentDescription).performClick() + val joinCallContentDescription = activity!!.getString(CommonStrings.a11y_start_call) + onNodeWithContentDescription(joinCallContentDescription).performClick() } } @Test - fun `clicking on join voice call invoke expected callback`() { + fun `clicking on join voice call invoke expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( eventSink = eventsRecorder, roomCallState = aStandByCallState(isDM = true) ) ensureCalledOnceWithParam(true) { callback -> - rule.setMessagesView( + setMessagesView( state = state, onJoinCallClick = callback, ) - val joinVoiceCallContentDescription = rule.activity.getString(CommonStrings.a11y_start_voice_call) - rule.onNodeWithContentDescription(joinVoiceCallContentDescription).performClick() + val joinVoiceCallContentDescription = activity!!.getString(CommonStrings.a11y_start_voice_call) + onNodeWithContentDescription(joinVoiceCallContentDescription).performClick() } } @Test - fun `clicking on an Event invoke expected callback`() { + fun `clicking on an Event invoke expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( timelineState = aTimelineState( @@ -167,12 +166,12 @@ class MessagesViewTest { expectedParam2 = timelineItem, result = true, ) - rule.setMessagesView( + setMessagesView( state = state, onEventClick = callback, ) // Cannot perform click on "Text", it's not detected. Use tag instead - rule.onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performClick() + onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performClick() callback.assertSuccess() } @@ -202,7 +201,7 @@ class MessagesViewTest { userHasPermissionToRedactOther: Boolean = false, userHasPermissionToSendReaction: Boolean = false, userCanPinEvent: Boolean = false, - ) { + ) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( actionListState = anActionListState( @@ -220,11 +219,11 @@ class MessagesViewTest { ), ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event - rule.setMessagesView( + setMessagesView( state = state, ) // Cannot perform click on "Text", it's not detected. Use tag instead - rule.onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performTouchInput { longClick() } + onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performTouchInput { longClick() } eventsRecorder.assertSingle( ActionListEvent.ComputeForMessage( event = timelineItem, @@ -235,7 +234,7 @@ class MessagesViewTest { @Test @Config(qualifiers = "h1024dp") - fun `clicking on a read receipt list emits the expected Event`() { + fun `clicking on a read receipt list emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState( @@ -255,10 +254,10 @@ class MessagesViewTest { ), ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event - rule.setMessagesView( + setMessagesView( state = state, ) - rule.onNodeWithTag(TestTags.messageReadReceipts.value, useUnmergedTree = true).performClick() + onNodeWithTag(TestTags.messageReadReceipts.value, useUnmergedTree = true).performClick() eventsRecorder.assertSingle(ReadReceiptBottomSheetEvent.EventSelected(timelineItem)) } @@ -272,7 +271,7 @@ class MessagesViewTest { swipeTest(userHasPermissionToSendMessage = false) } - private fun swipeTest(userHasPermissionToSendMessage: Boolean) { + private fun swipeTest(userHasPermissionToSendMessage: Boolean) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val canBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = true) val cannotBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = false) @@ -285,10 +284,10 @@ class MessagesViewTest { ), eventSink = eventsRecorder, ) - rule.setMessagesView( + setMessagesView( state = state, ) - rule.onAllNodesWithTag(TestTags.messageBubble.value).apply { + onAllNodesWithTag(TestTags.messageBubble.value).apply { onFirst().performTouchInput { swipeRight(endX = 200f) } onLast().performTouchInput { swipeRight(endX = 200f) } } @@ -300,7 +299,7 @@ class MessagesViewTest { } @Test - fun `clicking on send location invoke expected callback`() { + fun `clicking on send location invoke expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( composerState = aMessageComposerState( @@ -309,16 +308,16 @@ class MessagesViewTest { eventSink = eventsRecorder ) ensureCalledOnce { callback -> - rule.setMessagesView( + setMessagesView( state = state, onSendLocationClick = callback, ) - rule.clickOn(R.string.screen_room_attachment_source_location) + clickOn(R.string.screen_room_attachment_source_location) } } @Test - fun `clicking on create poll invoke expected callback`() { + fun `clicking on create poll invoke expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( composerState = aMessageComposerState( @@ -327,25 +326,25 @@ class MessagesViewTest { eventSink = eventsRecorder ) ensureCalledOnce { callback -> - rule.setMessagesView( + setMessagesView( state = state, onCreatePollClick = callback, ) // Then click on the poll action - rule.clickOn(R.string.screen_room_attachment_source_poll) + clickOn(R.string.screen_room_attachment_source_poll) } } @Test @Config(qualifiers = "h1024dp") - fun `clicking on the avatar of the sender of an Event emits the expected event`() { + fun `clicking on the avatar of the sender of an Event emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( eventSink = eventsRecorder ) val timelineEvent = state.timelineState.timelineItems.filterIsInstance().first() - rule.setMessagesView(state = state) - rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() + setMessagesView(state = state) + onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() eventsRecorder.assertSingle( MessagesEvent.OnUserClicked( MatrixUser( @@ -359,12 +358,12 @@ class MessagesViewTest { @Test @Config(qualifiers = "h1024dp") - fun `clicking on the display name of the sender of an Event emits expected event`() { + fun `clicking on the display name of the sender of an Event emits expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState(eventSink = eventsRecorder) val timelineEvent = state.timelineState.timelineItems.filterIsInstance().first() - rule.setMessagesView(state = state) - rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() + setMessagesView(state = state) + onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() eventsRecorder.assertSingle( MessagesEvent.OnUserClicked( MatrixUser( @@ -377,7 +376,7 @@ class MessagesViewTest { } @Test - fun `selecting a action on a message emits the expected Event`() { + fun `selecting a action on a message emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( eventSink = eventsRecorder @@ -395,17 +394,17 @@ class MessagesViewTest { ) ), ) - rule.setMessagesView( + setMessagesView( state = stateWithMessageAction, ) - rule.clickOn(CommonStrings.action_edit) + clickOn(CommonStrings.action_edit) // Give time for the close animation to complete - rule.mainClock.advanceTimeBy(milliseconds = 1_000) + mainClock.advanceTimeBy(milliseconds = 1_000) eventsRecorder.assertSingle(MessagesEvent.HandleAction(TimelineItemAction.Edit, timelineItem)) } @Test - fun `clicking on a reaction emits the expected Event`() { + fun `clicking on a reaction emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState( @@ -414,10 +413,10 @@ class MessagesViewTest { eventSink = eventsRecorder, ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event - rule.setMessagesView( + setMessagesView( state = state, ) - rule.onAllNodesWithText( + onAllNodesWithText( text = "👍️", useUnmergedTree = true, ).onFirst().performClick() @@ -425,7 +424,7 @@ class MessagesViewTest { } @Test - fun `long clicking on a reaction emits the expected Event`() { + fun `long clicking on a reaction emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState( @@ -437,10 +436,10 @@ class MessagesViewTest { ), ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event - rule.setMessagesView( + setMessagesView( state = state, ) - rule.onAllNodesWithText( + onAllNodesWithText( text = "👍️", useUnmergedTree = true, ).onFirst().performTouchInput { longClick() } @@ -448,7 +447,7 @@ class MessagesViewTest { } @Test - fun `clicking on more reaction emits the expected Event`() { + fun `clicking on more reaction emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState( @@ -459,16 +458,16 @@ class MessagesViewTest { ), ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event - rule.setMessagesView( + setMessagesView( state = state, ) - val moreReactionContentDescription = rule.activity.getString(R.string.screen_room_timeline_add_reaction) - rule.onAllNodesWithContentDescription(moreReactionContentDescription).onFirst().performClick() + val moreReactionContentDescription = activity!!.getString(R.string.screen_room_timeline_add_reaction) + onAllNodesWithContentDescription(moreReactionContentDescription).onFirst().performClick() eventsRecorder.assertSingle(CustomReactionEvent.ShowCustomReactionSheet(timelineItem)) } @Test - fun `clicking on more reaction from action list emits the expected Event`() { + fun `clicking on more reaction from action list emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState( @@ -491,18 +490,18 @@ class MessagesViewTest { eventSink = eventsRecorder ), ) - rule.setMessagesView( + setMessagesView( state = stateWithActionListState, ) - val moreReactionContentDescription = rule.activity.getString(CommonStrings.a11y_react_with_other_emojis) - rule.onNodeWithContentDescription(moreReactionContentDescription).performClick() + val moreReactionContentDescription = activity!!.getString(CommonStrings.a11y_react_with_other_emojis) + onNodeWithContentDescription(moreReactionContentDescription).performClick() // Give time for the close animation to complete - rule.mainClock.advanceTimeBy(milliseconds = 1_000) + mainClock.advanceTimeBy(milliseconds = 1_000) eventsRecorder.assertSingle(CustomReactionEvent.ShowCustomReactionSheet(timelineItem)) } @Test - fun `clicking on verified user send failure from action list emits the expected Event`() { + fun `clicking on verified user send failure from action list emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState() val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event @@ -519,21 +518,21 @@ class MessagesViewTest { ), timelineState = aTimelineState(eventSink = eventsRecorder) ) - rule.setMessagesView( + setMessagesView( state = stateWithActionListState, ) // Clear initial 'LoadMore' event emitted when setting the state eventsRecorder.clear() - val verifiedUserSendFailure = rule.activity.getString(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, "Alice") - rule.onNodeWithText(verifiedUserSendFailure).performClick() + val verifiedUserSendFailure = activity!!.getString(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, "Alice") + onNodeWithText(verifiedUserSendFailure).performClick() // Give time for the close animation to complete - rule.mainClock.advanceTimeBy(milliseconds = 1_000) + mainClock.advanceTimeBy(milliseconds = 1_000) eventsRecorder.assertSingle(TimelineEvent.ComputeVerifiedUserSendFailure(timelineItem)) } @Test - fun `clicking on a custom emoji emits the expected Events`() { + fun `clicking on a custom emoji emits the expected Events`() = runAndroidComposeUiTest { val aUnicode = "🙈" val customReactionStateEventsRecorder = EventsRecorder() val eventsRecorder = EventsRecorder() @@ -563,18 +562,18 @@ class MessagesViewTest { eventSink = customReactionStateEventsRecorder ), ) - rule.setMessagesView( + setMessagesView( state = stateWithCustomReactionState, ) - rule.onNodeWithText(aUnicode, useUnmergedTree = true).performClick() + onNodeWithText(aUnicode, useUnmergedTree = true).performClick() // Give time for the close animation to complete - rule.mainClock.advanceTimeBy(milliseconds = 1_000) + mainClock.advanceTimeBy(milliseconds = 1_000) customReactionStateEventsRecorder.assertSingle(CustomReactionEvent.DismissCustomReactionSheet) eventsRecorder.assertSingle(MessagesEvent.ToggleReaction(aUnicode, timelineItem.eventOrTransactionId)) } @Test - fun `clicking on pinned messages banner emits the expected Event`() { + fun `clicking on pinned messages banner emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState(eventSink = eventsRecorder), @@ -587,16 +586,16 @@ class MessagesViewTest { ), ), ) - rule.setMessagesView(state = state) + setMessagesView(state = state) // Clear initial 'LoadMore' event emitted when setting the state eventsRecorder.clear() - rule.onNodeWithText("This is a pinned message").performClick() + onNodeWithText("This is a pinned message").performClick() eventsRecorder.assertSingle(TimelineEvent.FocusOnEvent(AN_EVENT_ID, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds)) } @Test - fun `clicking on successor room button emits expected event`() { + fun `clicking on successor room button emits expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val successorRoomId = RoomId("!successor:server.org") val state = aMessagesState( @@ -606,18 +605,18 @@ class MessagesViewTest { ), timelineState = aTimelineState(eventSink = eventsRecorder) ) - rule.setMessagesView(state = state) + setMessagesView(state = state) // Clear initial 'LoadMore' event emitted when setting the state eventsRecorder.clear() - val text = rule.activity.getString(R.string.screen_room_timeline_tombstoned_room_action) + val text = activity!!.getString(R.string.screen_room_timeline_tombstoned_room_action) // The bottomsheet subcompose seems to make the node to appear twice - rule.onAllNodesWithText(text).onFirst().performClick() + onAllNodesWithText(text).onFirst().performClick() eventsRecorder.assertSingle(TimelineEvent.NavigateToPredecessorOrSuccessorRoom(successorRoomId)) } @Test - fun `clicking on threads list button calls the expected function`() { + fun `clicking on threads list button calls the expected function`() = runAndroidComposeUiTest { val state = aMessagesState( threads = MessagesState.Threads( hasThreads = true, @@ -625,28 +624,28 @@ class MessagesViewTest { ) ) val onThreadsListClicked = lambdaRecorder {} - rule.setMessagesView( + setMessagesView( state = state, onThreadsListClicked = onThreadsListClicked, ) - rule.onNodeWithContentDescription("Threads").performClick() + onNodeWithContentDescription("Threads").performClick() onThreadsListClicked.assertions().isCalledOnce() } @Test - fun `no banner shown when there is no successor room`() { + fun `no banner shown when there is no successor room`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( successorRoom = null, eventSink = eventsRecorder ) - rule.setMessagesView(state = state) - rule.assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_message) - rule.assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_action) + setMessagesView(state = state) + assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_message) + assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_action) } } -private fun AndroidComposeTestRule.setMessagesView( +private fun AndroidComposeUiTest.setMessagesView( state: MessagesState, onBackClick: () -> Unit = EnsureNeverCalled(), onRoomDetailsClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateViewTest.kt index 24779ba78a..0ee342513a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateViewTest.kt @@ -6,12 +6,15 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.crypto.identity import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.designsystem.components.avatar.anAvatarData import io.element.android.libraries.matrix.api.core.UserId @@ -21,19 +24,15 @@ import io.element.android.libraries.matrix.ui.room.RoomMemberIdentityStateChange import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class IdentityChangeStateViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `show and resolve pin violation`() { + fun `show and resolve pin violation`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIdentityChangeStateView( + setIdentityChangeStateView( state = anIdentityChangeState( listOf( RoomMemberIdentityStateChange( @@ -45,18 +44,18 @@ class IdentityChangeStateViewTest { ), ) - rule.onNodeWithText("identity was reset", substring = true).assertExists("should display pin violation warning") - rule.onNodeWithText("@alice:localhost", substring = true).assertExists("should display user mxid") - rule.onNodeWithText("Alice", substring = true).assertExists("should display user displayname") + onNodeWithText("identity was reset", substring = true).assertExists("should display pin violation warning") + onNodeWithText("@alice:localhost", substring = true).assertExists("should display user mxid") + onNodeWithText("Alice", substring = true).assertExists("should display user displayname") - rule.clickOn(res = CommonStrings.action_dismiss) + clickOn(res = CommonStrings.action_dismiss) eventsRecorder.assertSingle(IdentityChangeEvent.PinIdentity(UserId("@alice:localhost"))) } @Test - fun `show and resolve verification violation`() { + fun `show and resolve verification violation`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIdentityChangeStateView( + setIdentityChangeStateView( state = anIdentityChangeState( listOf( RoomMemberIdentityStateChange( @@ -68,17 +67,17 @@ class IdentityChangeStateViewTest { ), ) - rule.onNodeWithText("identity was reset", substring = true).assertExists("should display verification violation warning") - rule.onNodeWithText("@alice:localhost", substring = true).assertExists("should display user mxid") - rule.onNodeWithText("Alice", substring = true).assertExists("should display user displayname") + onNodeWithText("identity was reset", substring = true).assertExists("should display verification violation warning") + onNodeWithText("@alice:localhost", substring = true).assertExists("should display user mxid") + onNodeWithText("Alice", substring = true).assertExists("should display user displayname") - rule.clickOn(res = CommonStrings.crypto_identity_change_withdraw_verification_action) + clickOn(res = CommonStrings.crypto_identity_change_withdraw_verification_action) eventsRecorder.assertSingle(IdentityChangeEvent.WithdrawVerification(UserId("@alice:localhost"))) } @Test - fun `Should not show any banner if no violations`() { - rule.setIdentityChangeStateView( + fun `Should not show any banner if no violations`() = runAndroidComposeUiTest { + setIdentityChangeStateView( state = anIdentityChangeState( listOf( RoomMemberIdentityStateChange( @@ -93,10 +92,10 @@ class IdentityChangeStateViewTest { ), ) - rule.onNodeWithText("identity was reset", substring = true).assertDoesNotExist() + onNodeWithText("identity was reset", substring = true).assertDoesNotExist() } - private fun AndroidComposeTestRule.setIdentityChangeStateView( + private fun AndroidComposeUiTest.setIdentityChangeStateView( state: IdentityChangeState, ) { setContent { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt index 02767fbeb9..07a0fd5f94 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt @@ -6,54 +6,53 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.crypto.sendfailure.resolve import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.setSafeContent -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ResolveVerifiedUserSendFailureViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on resolve and resend emit the expected event`() { + fun `clicking on resolve and resend emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setResolveVerifiedUserSendFailureView( + setResolveVerifiedUserSendFailureView( state = aResolveVerifiedUserSendFailureState( verifiedUserSendFailure = aChangedIdentitySendFailure(), eventSink = eventsRecorder, ), ) - rule.clickOn(res = CommonStrings.screen_resolve_send_failure_changed_identity_primary_button_title) + clickOn(res = CommonStrings.screen_resolve_send_failure_changed_identity_primary_button_title) eventsRecorder.assertSingle(ResolveVerifiedUserSendFailureEvent.ResolveAndResend) } @Test - fun `clicking on retry emit the expected event`() { + fun `clicking on retry emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setResolveVerifiedUserSendFailureView( + setResolveVerifiedUserSendFailureView( state = aResolveVerifiedUserSendFailureState( verifiedUserSendFailure = aChangedIdentitySendFailure(), eventSink = eventsRecorder, ), ) - rule.clickOn(res = CommonStrings.action_retry) + clickOn(res = CommonStrings.action_retry) eventsRecorder.assertSingle(ResolveVerifiedUserSendFailureEvent.Retry) } - private fun AndroidComposeTestRule.setResolveVerifiedUserSendFailureView( + private fun AndroidComposeUiTest.setResolveVerifiedUserSendFailureView( state: ResolveVerifiedUserSendFailureState, ) { setSafeContent { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/LinkViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/LinkViewTest.kt index e198ea9043..b656430466 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/LinkViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/LinkViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.link import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.ui.strings.CommonStrings @@ -19,51 +22,46 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.wysiwyg.link.Link -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class LinkViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `clicking on cancel emits the expected event`() { + fun `clicking on cancel emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLinkView( + setLinkView( aLinkState( linkClick = ConfirmingLinkClick(aLink), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle( LinkEvent.Cancel ) } @Test - fun `clicking on continue emits the expected event`() { + fun `clicking on continue emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLinkView( + setLinkView( aLinkState( linkClick = ConfirmingLinkClick(aLink), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle( LinkEvent.Confirm ) } @Test - fun `success state invokes the callback and emits the expected event`() { + fun `success state invokes the callback and emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() ensureCalledOnceWithParam(aLink) { callback -> - rule.setLinkView( + setLinkView( aLinkState( linkClick = AsyncAction.Success(aLink), eventSink = eventsRecorder, @@ -77,7 +75,7 @@ class LinkViewTest { } } -private fun AndroidComposeTestRule.setLinkView( +private fun AndroidComposeUiTest.setLinkView( state: LinkState, onLinkValid: (Link) -> Unit = EnsureNeverCalledWithParam(), ) { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerViewTest.kt index 2c33e348c0..546731ff87 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerViewTest.kt @@ -6,13 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.pinned.banner import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.strings.CommonStrings @@ -22,49 +25,45 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PinnedMessagesBannerViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on the banner invoke expected callback`() { + fun `clicking on the banner invoke expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aLoadedPinnedMessagesBannerState( eventSink = eventsRecorder ) val pinnedEventId = state.currentPinnedMessage.eventId ensureCalledOnceWithParam(pinnedEventId) { callback -> - rule.setPinnedMessagesBannerView( + setPinnedMessagesBannerView( state = state, onClick = callback ) - rule.onRoot().performClick() + onRoot().performClick() eventsRecorder.assertSingle(PinnedMessagesBannerEvent.MoveToNextPinned) } } @Test - fun `clicking on view all emit the expected event`() { + fun `clicking on view all emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = true) val state = aLoadedPinnedMessagesBannerState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - rule.setPinnedMessagesBannerView( + setPinnedMessagesBannerView( state = state, onViewAllClick = callback ) - rule.clickOn(CommonStrings.screen_room_pinned_banner_view_all_button_title) + clickOn(CommonStrings.screen_room_pinned_banner_view_all_button_title) } } } -private fun AndroidComposeTestRule.setPinnedMessagesBannerView( +private fun AndroidComposeUiTest.setPinnedMessagesBannerView( state: PinnedMessagesBannerState, onClick: (EventId) -> Unit = EnsureNeverCalledWithParam(), onViewAllClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt index 41671b71c1..9c10abb631 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt @@ -6,16 +6,19 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.pinned.list import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.longClick import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.messages.impl.actionlist.ActionListEvent import io.element.android.features.messages.impl.actionlist.anActionListState @@ -31,33 +34,28 @@ import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.setSafeContent import io.element.android.wysiwyg.link.Link -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PinnedMessagesListViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `clicking on back calls the expected callback`() { + fun `clicking on back calls the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aLoadedPinnedMessagesListState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - rule.setPinnedMessagesListView( + setPinnedMessagesListView( state = state, onBackClick = callback ) - rule.pressBack() + pressBack() } } @Test - fun `click on an event calls the expected callback`() { + fun `click on an event calls the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val content = aTimelineItemFileContent() val state = aLoadedPinnedMessagesListState( @@ -67,16 +65,16 @@ class PinnedMessagesListViewTest { val event = state.timelineItems.first() as TimelineItem.Event ensureCalledOnceWithParam(event) { callback -> - rule.setPinnedMessagesListView( + setPinnedMessagesListView( state = state, onEventClick = callback ) - rule.onAllNodesWithText(content.filename).onFirst().performClick() + onAllNodesWithText(content.filename).onFirst().performClick() } } @Test - fun `long click on an event emits the expected event`() { + fun `long click on an event emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = true) val content = aTimelineItemFileContent() val state = aLoadedPinnedMessagesListState( @@ -84,10 +82,10 @@ class PinnedMessagesListViewTest { actionListState = anActionListState(eventSink = eventsRecorder) ) - rule.setPinnedMessagesListView( + setPinnedMessagesListView( state = state, ) - rule.onAllNodesWithText(content.filename).onFirst() + onAllNodesWithText(content.filename).onFirst() .performTouchInput { longClick() } @@ -96,7 +94,7 @@ class PinnedMessagesListViewTest { } } -private fun AndroidComposeTestRule.setPinnedMessagesListView( +private fun AndroidComposeUiTest.setPinnedMessagesListView( state: PinnedMessagesListState, onBackClick: () -> Unit = EnsureNeverCalled(), onEventClick: (event: TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt index 315d9c459c..9e98f0fa49 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.timeline import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runComposeUiTest import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.utils.FakeMentionSpanFormatter import io.element.android.libraries.core.extensions.runCatchingExceptions @@ -18,15 +21,12 @@ import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class DefaultHtmlConverterProviderTest { - @get:Rule val composeTestRule = createComposeRule() - private val provider = DefaultHtmlConverterProvider( mentionSpanProvider = MentionSpanProvider( permalinkParser = FakePermalinkParser(), @@ -43,8 +43,8 @@ class DefaultHtmlConverterProviderTest { } @Test - fun `calling provide after calling Update first should return an HtmlConverter`() { - composeTestRule.setContent { + fun `calling provide after calling Update first should return an HtmlConverter`() = runComposeUiTest { + setContent { CompositionLocalProvider(LocalInspectionMode provides true) { provider.Update() } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index 3a0b0e1224..2138d4ced2 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -6,15 +6,18 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.timeline import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToIndex +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.messages.impl.timeline.components.MessageShieldData import io.element.android.features.messages.impl.timeline.components.aCriticalShield @@ -39,19 +42,15 @@ import io.element.android.wysiwyg.link.Link import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import org.junit.Ignore -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class TimelineViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `reaching the end of the timeline with more events to load emits a LoadMore event`() { + fun `reaching the end of the timeline with more events to load emits a LoadMore event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTimelineView( + setTimelineView( state = aTimelineState( timelineItems = persistentListOf( TimelineItem.Virtual( @@ -66,9 +65,9 @@ class TimelineViewTest { } @Test - fun `reaching the end of the timeline does not send a LoadMore event`() { + fun `reaching the end of the timeline does not send a LoadMore event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTimelineView( + setTimelineView( state = aTimelineState( timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), eventSink = eventsRecorder, @@ -78,9 +77,9 @@ class TimelineViewTest { } @Test - fun `scroll to bottom on live timeline does not emit the Event`() { + fun `scroll to bottom on live timeline does not emit the Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTimelineView( + setTimelineView( state = aTimelineState( timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), isLive = true, @@ -92,14 +91,14 @@ class TimelineViewTest { eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) eventsRecorder.clear() - val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom) - rule.onNodeWithContentDescription(contentDescription).performClick() + val contentDescription = activity!!.getString(CommonStrings.a11y_jump_to_bottom) + onNodeWithContentDescription(contentDescription).performClick() } @Test - fun `scroll to bottom on detached timeline emits the expected Event`() { + fun `scroll to bottom on detached timeline emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTimelineView( + setTimelineView( state = aTimelineState( timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), isLive = false, @@ -110,15 +109,15 @@ class TimelineViewTest { eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) eventsRecorder.clear() - val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom) - rule.onNodeWithContentDescription(contentDescription).performClick() + val contentDescription = activity!!.getString(CommonStrings.a11y_jump_to_bottom) + onNodeWithContentDescription(contentDescription).performClick() eventsRecorder.assertSingle(TimelineEvent.JumpToLive) } @Test - fun `an empty timeline triggers a prefetch`() { + fun `an empty timeline triggers a prefetch`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTimelineView( + setTimelineView( state = aTimelineState( timelineItems = persistentListOf(), eventSink = eventsRecorder, @@ -129,9 +128,9 @@ class TimelineViewTest { } @Test - fun `show shield dialog`() { + fun `show shield dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTimelineView( + setTimelineView( state = aTimelineState( timelineItems = persistentListOf( aTimelineItemEvent( @@ -143,8 +142,8 @@ class TimelineViewTest { eventSink = eventsRecorder, ), ) - val contentDescription = rule.activity.getString(CommonStrings.a11y_encryption_details) - rule.onNodeWithContentDescription(contentDescription).performClick() + val contentDescription = activity!!.getString(CommonStrings.a11y_encryption_details) + onNodeWithContentDescription(contentDescription).performClick() eventsRecorder.assertList( listOf( TimelineEvent.OnScrollFinished(0), @@ -154,9 +153,9 @@ class TimelineViewTest { } @Test - fun `hide shield dialog`() { + fun `hide shield dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTimelineView( + setTimelineView( state = aTimelineState( timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), isLive = false, @@ -167,16 +166,16 @@ class TimelineViewTest { eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) eventsRecorder.clear() - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(TimelineEvent.HideShieldDialog) } @Ignore( "performScrollToIndex in compose tests no longer sets LazyListState.isScrollInProgress to true, so the LoadMore event is not emitted." + - "This needs to be reworked to use a different approach to check the LoadMore event was emitted." + "This needs to be reworked to use a different approach to check the LoadMore event was emitted." ) @Test - fun `scrolling near to the start of the loaded items triggers a pre-fetch`() { + fun `scrolling near to the start of the loaded items triggers a pre-fetch`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val items = List(200) { aTimelineItemEvent( @@ -185,7 +184,7 @@ class TimelineViewTest { ) }.toImmutableList() - rule.setTimelineView( + setTimelineView( state = aTimelineState( timelineItems = items, eventSink = eventsRecorder, @@ -194,9 +193,9 @@ class TimelineViewTest { ), ) - rule.onNodeWithTag("timeline").performScrollToIndex(180) + onNodeWithTag("timeline").performScrollToIndex(180) - rule.mainClock.advanceTimeBy(1000) + mainClock.advanceTimeBy(1000) eventsRecorder.assertList( listOf( @@ -207,7 +206,7 @@ class TimelineViewTest { } } -private fun AndroidComposeTestRule.setTimelineView( +private fun AndroidComposeUiTest.setTimelineView( state: TimelineState, timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(), onUserDataClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollViewTest.kt index 64b5216d2e..40671e4bf8 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollViewTest.kt @@ -6,12 +6,15 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.timeline.components.event import androidx.activity.ComponentActivity +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.messages.impl.timeline.TimelineEvent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent @@ -20,14 +23,11 @@ import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressTag -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class TimelineItemPollViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test fun `answering a poll with first answer should emit a PollAnswerSelected event`() { testAnswer(answerIndex = 0) @@ -38,17 +38,17 @@ class TimelineItemPollViewTest { testAnswer(answerIndex = 1) } - private fun testAnswer(answerIndex: Int) { + private fun testAnswer(answerIndex: Int) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val content = aTimelineItemPollContent() - rule.setContent { + setContent { TimelineItemPollView( content = content, eventSink = eventsRecorder ) } val answer = content.answerItems[answerIndex].answer - rule.onNode( + onNode( matcher = hasText(answer.text), useUnmergedTree = true, ).performClick() @@ -56,38 +56,38 @@ class TimelineItemPollViewTest { } @Test - fun `editing a poll should emit a PollEditClicked event`() { + fun `editing a poll should emit a PollEditClicked event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val content = aTimelineItemPollContent( isMine = true, isEditable = true, ) - rule.setContent { + setContent { TimelineItemPollView( content = content, eventSink = eventsRecorder ) } - rule.clickOn(CommonStrings.action_edit_poll) + clickOn(CommonStrings.action_edit_poll) eventsRecorder.assertSingle(TimelineEvent.EditPoll(content.eventId!!)) } @Test - fun `closing a poll should emit a PollEndClicked event`() { + fun `closing a poll should emit a PollEndClicked event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val content = aTimelineItemPollContent( isMine = true, ) - rule.setContent { + setContent { TimelineItemPollView( content = content, eventSink = eventsRecorder ) } - rule.clickOn(CommonStrings.action_end_poll) + clickOn(CommonStrings.action_end_poll) // A confirmation dialog should be shown eventsRecorder.assertEmpty() - rule.pressTag(TestTags.dialogPositive.value) + pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(TimelineEvent.EndPoll(content.eventId!!)) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineTextViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineTextViewTest.kt index 154225aa7a..7b8597f05a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineTextViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineTextViewTest.kt @@ -6,14 +6,17 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.timeline.components.event import android.text.SpannableString import android.text.SpannedString import androidx.activity.ComponentActivity import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.core.text.buildSpannedString import androidx.core.text.inSpans import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -38,45 +41,40 @@ import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.wysiwyg.view.spans.CustomMentionSpan import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.test.runTest -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class TimelineTextViewTest { - @get:Rule val rule = createAndroidComposeRule() - private val mentionSpanTheme = MentionSpanTheme(currentUserId = A_USER_ID) private val formatLambda = lambdaRecorder { mentionType -> mentionType.toString() } private val mentionSpanFormatter = FakeMentionSpanFormatter(formatLambda) @Test - fun `getTextWithResolvedMentions - does nothing for a non spannable CharSequence`() = runTest { + fun `getTextWithResolvedMentions - does nothing for a non spannable CharSequence`() = runAndroidComposeUiTest { val charSequence = "Hello @alice:example.com" val mentionSpanUpdater = aMentionSpanUpdater() - val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) + val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) assertThat(result.getMentionSpans()).isEmpty() assert(formatLambda).isNeverCalled() } @Test - fun `getTextWithResolvedMentions - does nothing if there are no mentions`() = runTest { + fun `getTextWithResolvedMentions - does nothing if there are no mentions`() = runAndroidComposeUiTest { val charSequence = SpannableString("Hello @alice:example.com") val mentionSpanUpdater = aMentionSpanUpdater() - val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) + val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) assertThat(result.getMentionSpans()).isEmpty() assert(formatLambda).isNeverCalled() } @Test - fun `getTextWithResolvedMentions - just returns the body if there is no formattedBody`() = runTest { + fun `getTextWithResolvedMentions - just returns the body if there is no formattedBody`() = runAndroidComposeUiTest { val charSequence = "Hello @alice:example.com" val mentionSpanUpdater = aMentionSpanUpdater() - val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(body = charSequence, formattedBody = null)) + val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(body = charSequence, formattedBody = null)) assertThat(result.getMentionSpans()).isEmpty() assertThat(result.toString()).isEqualTo(charSequence) @@ -84,7 +82,7 @@ class TimelineTextViewTest { } @Test - fun `getTextWithResolvedMentions - with Room mention format correctly`() = runTest { + fun `getTextWithResolvedMentions - with Room mention format correctly`() = runAndroidComposeUiTest { val mentionType = MentionType.Room(roomIdOrAlias = A_ROOM_ID_2.toRoomIdOrAlias()) val charSequence = buildSpannedString { append("Hello ") @@ -93,7 +91,7 @@ class TimelineTextViewTest { } } val mentionSpanUpdater = aMentionSpanUpdater() - val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) + val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) val expectedDisplayText = mentionType.toString() assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText) @@ -102,7 +100,7 @@ class TimelineTextViewTest { } @Test - fun `getTextWithResolvedMentions - replaces MentionSpan's text`() = runTest { + fun `getTextWithResolvedMentions - replaces MentionSpan's text`() = runAndroidComposeUiTest { val mentionType = MentionType.User(userId = A_USER_ID) val charSequence = buildSpannedString { append("Hello ") @@ -111,7 +109,7 @@ class TimelineTextViewTest { } } val mentionSpanUpdater = aMentionSpanUpdater() - val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) + val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) val expectedDisplayText = mentionType.toString() assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText) @@ -119,7 +117,7 @@ class TimelineTextViewTest { } @Test - fun `getTextWithResolvedMentions - replaces MentionSpan's text inside CustomMentionSpan`() = runTest { + fun `getTextWithResolvedMentions - replaces MentionSpan's text inside CustomMentionSpan`() = runAndroidComposeUiTest { val mentionType = MentionType.User(userId = A_USER_ID) val charSequence = buildSpannedString { append("Hello ") @@ -129,12 +127,12 @@ class TimelineTextViewTest { } val mentionSpanUpdater = aMentionSpanUpdater() val expectedDisplayText = mentionType.toString() - val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) + val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText) assert(formatLambda).isCalledOnce() } - private suspend fun AndroidComposeTestRule.getText( + private suspend fun AndroidComposeUiTest.getText( mentionSpanUpdater: MentionSpanUpdater, content: TimelineItemTextBasedContent, ): CharSequence { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedViewTest.kt index af3acee6a2..8050278fb2 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedViewTest.kt @@ -6,56 +6,55 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.timeline.protection import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.lambda.lambdaError -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ProtectedViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `when hideContent is false, the content is rendered`() { - rule.setProtectedView( + fun `when hideContent is false, the content is rendered`() = runAndroidComposeUiTest { + setProtectedView( hideContent = false, content = { Text("Hello") } ) - rule.onNodeWithText("Hello").assertExists() + onNodeWithText("Hello").assertExists() } @Test - fun `when hideContent is true, the content is not rendered, and user can reveal it`() { + fun `when hideContent is true, the content is not rendered, and user can reveal it`() = runAndroidComposeUiTest { ensureCalledOnce { - rule.setProtectedView( + setProtectedView( hideContent = true, onShowClick = it, content = { Text("Hello") } ) - rule.onNodeWithText("Hello").assertDoesNotExist() - rule.clickOn(CommonStrings.action_show) + onNodeWithText("Hello").assertDoesNotExist() + clickOn(CommonStrings.action_show) } } } -private fun AndroidComposeTestRule.setProtectedView( +private fun AndroidComposeUiTest.setProtectedView( hideContent: Boolean = false, onShowClick: () -> Unit = { lambdaError() }, content: @Composable () -> Unit = {}, diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt index 1ff25a0a81..a6b97c554c 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt @@ -6,13 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.poll.impl.history import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.poll.api.pollcontent.aPollContentState import io.element.android.features.poll.impl.R @@ -26,34 +29,29 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class PollHistoryViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setPollHistoryViewView( + setPollHistoryViewView( aPollHistoryState( eventSink = eventsRecorder ), goBack = it ) - rule.pressBack() + pressBack() } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on edit poll invokes the expected callback`() { + fun `clicking on edit poll invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val eventId = EventId("\$anEventId") val state = aPollHistoryState( @@ -69,17 +67,17 @@ class PollHistoryViewTest { eventSink = eventsRecorder ) ensureCalledOnceWithParam(eventId) { - rule.setPollHistoryViewView( + setPollHistoryViewView( state = state, onEditPoll = it ) - rule.clickOn(CommonStrings.action_edit_poll) + clickOn(CommonStrings.action_edit_poll) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on poll end emits the expected Event`() { + fun `clicking on poll end emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val eventId = EventId("\$anEventId") val state = aPollHistoryState( @@ -95,16 +93,16 @@ class PollHistoryViewTest { ), eventSink = eventsRecorder ) - rule.setPollHistoryViewView( + setPollHistoryViewView( state = state, ) - rule.clickOn(CommonStrings.action_end_poll) + clickOn(CommonStrings.action_end_poll) // Cancel the dialog - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) // Do it again, and confirm the dialog - rule.clickOn(CommonStrings.action_end_poll) + clickOn(CommonStrings.action_end_poll) eventsRecorder.assertEmpty() - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle( PollHistoryEvents.EndPoll(eventId) ) @@ -112,7 +110,7 @@ class PollHistoryViewTest { @Config(qualifiers = "h1024dp") @Test - fun `clicking on poll answer emits the expected Event`() { + fun `clicking on poll answer emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val eventId = EventId("\$anEventId") val state = aPollHistoryState( @@ -129,10 +127,10 @@ class PollHistoryViewTest { eventSink = eventsRecorder ) val answer = state.pollHistoryItems.ongoing.first().state.answerItems.first().answer - rule.setPollHistoryViewView( + setPollHistoryViewView( state = state, ) - rule.onNodeWithText( + onNodeWithText( text = answer.text, useUnmergedTree = true, ).performClick() @@ -142,14 +140,14 @@ class PollHistoryViewTest { } @Test - fun `clicking on past tab emits the expected Event`() { + fun `clicking on past tab emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPollHistoryViewView( + setPollHistoryViewView( aPollHistoryState( eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_polls_history_filter_past) + clickOn(R.string.screen_polls_history_filter_past) eventsRecorder.assertSingle( PollHistoryEvents.SelectFilter(filter = PollHistoryFilter.PAST) ) @@ -157,22 +155,22 @@ class PollHistoryViewTest { @Config(qualifiers = "h1024dp") @Test - fun `clicking on load more emits the expected Event`() { + fun `clicking on load more emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPollHistoryViewView( + setPollHistoryViewView( aPollHistoryState( hasMoreToLoad = true, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_load_more) + clickOn(CommonStrings.action_load_more) eventsRecorder.assertSingle( PollHistoryEvents.LoadMore ) } } -private fun AndroidComposeTestRule.setPollHistoryViewView( +private fun AndroidComposeUiTest.setPollHistoryViewView( state: PollHistoryState, onEditPoll: (EventId) -> Unit = EnsureNeverCalledWithParam(), goBack: () -> Unit = EnsureNeverCalled(), diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutViewTest.kt index 258e9855de..e7ce526843 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.preferences.impl.about import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -19,51 +22,47 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class AboutViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on back invokes back callback`() { + fun `clicking on back invokes back callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setAboutView( + setAboutView( anAboutState(), onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on an item invokes the expected callback`() { + fun `clicking on an item invokes the expected callback`() = runAndroidComposeUiTest { val state = anAboutState() ensureCalledOnceWithParam(state.elementLegals.first()) { callback -> - rule.setAboutView( + setAboutView( state, onElementLegalClick = callback, ) - rule.clickOn(state.elementLegals.first().titleRes) + clickOn(state.elementLegals.first().titleRes) } } @Test - fun `clicking on the open source licenses invokes the expected callback`() { + fun `clicking on the open source licenses invokes the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setAboutView( + setAboutView( anAboutState(), onOpenSourceLicensesClick = callback, ) - rule.clickOn(CommonStrings.common_open_source_licenses) + clickOn(CommonStrings.common_open_source_licenses) } } } -private fun AndroidComposeTestRule.setAboutView( +private fun AndroidComposeUiTest.setAboutView( state: AboutState, onElementLegalClick: (ElementLegal) -> Unit = EnsureNeverCalledWithParam(), onOpenSourceLicensesClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt index e46e350415..b6fe5c3d0b 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt @@ -6,13 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.preferences.impl.advanced import androidx.activity.ComponentActivity import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.Interaction @@ -30,104 +33,99 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import kotlinx.collections.immutable.toImmutableList -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class AdvancedSettingsViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder ), onBackClick = it ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on other theme emits the expected event`() { + fun `clicking on other theme emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.common_appearance) - rule.clickOn(CommonStrings.common_dark) + clickOn(CommonStrings.common_appearance) + clickOn(CommonStrings.common_dark) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTheme(ThemeOption.Dark)) } @Test - fun `black theme is shown when available`() { - rule.setAdvancedSettingsView( + fun `black theme is shown when available`() = runAndroidComposeUiTest { + setAdvancedSettingsView( state = aAdvancedSettingsState( availableThemeOptions = ThemeOption.entries.toImmutableList(), ), ) - rule.clickOn(CommonStrings.common_appearance) - rule.run { - val text = activity.getString(CommonStrings.common_black) + clickOn(CommonStrings.common_appearance) + run { + val text = activity!!.getString(CommonStrings.common_black) onNodeWithText(text).assertExists() } } @Test - fun `black theme is hidden when unavailable`() { - rule.setAdvancedSettingsView( + fun `black theme is hidden when unavailable`() = runAndroidComposeUiTest { + setAdvancedSettingsView( state = aAdvancedSettingsState( availableThemeOptions = ThemeOption.entries.filterNot { it == ThemeOption.Black }.toImmutableList(), ), ) - rule.clickOn(CommonStrings.common_appearance) - rule.assertNoNodeWithText(CommonStrings.common_black) + clickOn(CommonStrings.common_appearance) + assertNoNodeWithText(CommonStrings.common_black) } @Test - fun `clicking on View source emits the expected event`() { + fun `clicking on View source emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_view_source) + clickOn(CommonStrings.action_view_source) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetDeveloperModeEnabled(true)) } @Test - fun `clicking on Share presence emits the expected event`() { + fun `clicking on Share presence emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_advanced_settings_share_presence) + clickOn(R.string.screen_advanced_settings_share_presence) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetSharePresenceEnabled(true)) } @Test - fun `clicking on media to enable compression emits the expected event`() { + fun `clicking on media to enable compression emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val analyticsService = FakeAnalyticsService() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, ), analyticsService = analyticsService ) - rule.clickOn(R.string.screen_advanced_settings_media_compression_description) + clickOn(R.string.screen_advanced_settings_media_compression_description) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(true)) assertThat(analyticsService.capturedEvents).isEqualTo( listOf( @@ -139,17 +137,17 @@ class AdvancedSettingsViewTest { } @Test - fun `clicking on media to disable compression emits the expected event`() { + fun `clicking on media to disable compression emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val analyticsService = FakeAnalyticsService() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( mediaOptimizationState = MediaOptimizationState.AllMedia(isEnabled = true), eventSink = eventsRecorder, ), analyticsService = analyticsService ) - rule.clickOn(R.string.screen_advanced_settings_media_compression_description) + clickOn(R.string.screen_advanced_settings_media_compression_description) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(false)) assertThat(analyticsService.capturedEvents).isEqualTo( listOf( @@ -162,65 +160,65 @@ class AdvancedSettingsViewTest { @Test @Config(qualifiers = "h1080dp") - fun `clicking on hide invite avatars emits the expected event`() { + fun `clicking on hide invite avatars emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, hideInviteAvatars = false ), ) - rule.clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title) + clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetHideInviteAvatars(true)) } @Test @Config(qualifiers = "h1080dp") - fun `clicking on timeline media preview always hide emits the expected event`() { + fun `clicking on timeline media preview always hide emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, timelineMediaPreviewValue = MediaPreviewValue.On ), ) - rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) + clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off)) } @Test @Config(qualifiers = "h1080dp") - fun `clicking on timeline media preview private rooms emits the expected event`() { + fun `clicking on timeline media preview private rooms emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, timelineMediaPreviewValue = MediaPreviewValue.On ), ) - rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) + clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private)) } @Test @Config(qualifiers = "h1080dp") - fun `clicking on timeline media preview always show emits the expected event`() { + fun `clicking on timeline media preview always show emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, timelineMediaPreviewValue = MediaPreviewValue.Off ), ) - rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_always_show) + clickOn(R.string.screen_advanced_settings_show_media_timeline_always_show) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.On)) } @Test @Config(qualifiers = "h1080dp") - fun `hide invite avatars toggle is disabled when action is loading`() { + fun `hide invite avatars toggle is disabled when action is loading`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, hideInviteAvatars = false, @@ -228,14 +226,14 @@ class AdvancedSettingsViewTest { ), ) // The toggle should be disabled, so clicking should not emit any events - rule.clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title) + clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title) } @Test @Config(qualifiers = "h1080dp") - fun `timeline media preview options are disabled when action is loading`() { + fun `timeline media preview options are disabled when action is loading`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, timelineMediaPreviewValue = MediaPreviewValue.On, @@ -243,12 +241,12 @@ class AdvancedSettingsViewTest { ), ) // The options should be disabled, so clicking should not emit any events - rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) - rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) + clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) + clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) } } -private fun AndroidComposeTestRule.setAdvancedSettingsView( +private fun AndroidComposeUiTest.setAdvancedSettingsView( state: AdvancedSettingsState, analyticsService: AnalyticsService = FakeAnalyticsService(), onBackClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUserViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUserViewTest.kt index b3549762ab..993d14caab 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUserViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUserViewTest.kt @@ -6,13 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.preferences.impl.blockedusers import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.preferences.impl.R import io.element.android.libraries.architecture.AsyncAction @@ -23,72 +26,67 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class BlockedUserViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `clicking on back invokes back callback`() { + fun `clicking on back invokes back callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setBlockedUsersView( + setBlockedUsersView( aBlockedUsersState( eventSink = eventsRecorder ), onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on a user emits the expected Event`() { + fun `clicking on a user emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val userList = aMatrixUserList() - rule.setBlockedUsersView( + setBlockedUsersView( aBlockedUsersState( blockedUsers = userList, eventSink = eventsRecorder ), ) - rule.onNodeWithText(userList.first().displayName.orEmpty()).performClick() + onNodeWithText(userList.first().displayName.orEmpty()).performClick() eventsRecorder.assertSingle(BlockedUsersEvents.Unblock(userList.first().userId)) } @Test - fun `clicking on cancel sends a BlockedUsersEvents`() { + fun `clicking on cancel sends a BlockedUsersEvents`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setBlockedUsersView( + setBlockedUsersView( aBlockedUsersState( unblockUserAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(BlockedUsersEvents.Cancel) } @Test - fun `clicking on confirm sends a BlockedUsersEvents`() { + fun `clicking on confirm sends a BlockedUsersEvents`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setBlockedUsersView( + setBlockedUsersView( aBlockedUsersState( unblockUserAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_blocked_users_unblock_alert_action) + clickOn(R.string.screen_blocked_users_unblock_alert_action) eventsRecorder.assertSingle(BlockedUsersEvents.ConfirmUnblock) } } -private fun AndroidComposeTestRule.setBlockedUsersView( +private fun AndroidComposeUiTest.setBlockedUsersView( state: BlockedUsersState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt index d4d02d7de9..61d7278a8a 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt @@ -6,13 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.preferences.impl.developer import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.preferences.impl.R import io.element.android.tests.testutils.EnsureNeverCalled @@ -20,76 +23,71 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class DeveloperSettingsViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setDeveloperSettingsView( + setDeveloperSettingsView( state = aDeveloperSettingsState( eventSink = eventsRecorder ), onBackClick = it ) - rule.pressBack() + pressBack() } } @Config(qualifiers = "h2000dp") @Test - fun `clicking on push history notification invokes the expected callback`() { + fun `clicking on push history notification invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setDeveloperSettingsView( + setDeveloperSettingsView( state = aDeveloperSettingsState( eventSink = eventsRecorder ), onPushHistoryClick = it ) - rule.clickOn(R.string.troubleshoot_notifications_entry_point_push_history_title) + clickOn(R.string.troubleshoot_notifications_entry_point_push_history_title) } } @Config(qualifiers = "h2000dp") @Test - fun `clicking on open showkase invokes the expected callback`() { + fun `clicking on open showkase invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setDeveloperSettingsView( + setDeveloperSettingsView( state = aDeveloperSettingsState( eventSink = eventsRecorder ), onOpenShowkase = it ) - rule.onNodeWithText("Open Showkase browser").performClick() + onNodeWithText("Open Showkase browser").performClick() } } @Config(qualifiers = "h2200dp") @Test - fun `clicking on clear cache emits the expected event`() { + fun `clicking on clear cache emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDeveloperSettingsView( + setDeveloperSettingsView( state = aDeveloperSettingsState( eventSink = eventsRecorder ), ) - rule.onNodeWithText("Clear cache").performClick() + onNodeWithText("Clear cache").performClick() eventsRecorder.assertSingle(DeveloperSettingsEvents.ClearCache) } } -private fun AndroidComposeTestRule.setDeveloperSettingsView( +private fun AndroidComposeUiTest.setDeveloperSettingsView( state: DeveloperSettingsState, onOpenShowkase: () -> Unit = EnsureNeverCalled(), onPushHistoryClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPageTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPageTest.kt index 123f31ae8e..17218c6ab5 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPageTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPageTest.kt @@ -5,19 +5,22 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.preferences.impl.developer.appsettings import androidx.activity.ComponentActivity +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.filterToOne import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.isEditable import androidx.compose.ui.test.isFocusable -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem @@ -27,78 +30,73 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class AppDeveloperSettingsPageTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setAppDeveloperSettingsView( + setAppDeveloperSettingsView( state = anAppDeveloperSettingsState( eventSink = eventsRecorder ), onBackClick = it ) - rule.pressBack() + pressBack() } } @Config(qualifiers = "h1500dp") @Test - fun `clicking on element call url open the dialogs and submit emits the expected event`() { + fun `clicking on element call url open the dialogs and submit emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAppDeveloperSettingsView( + setAppDeveloperSettingsView( state = anAppDeveloperSettingsState( eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_advanced_settings_element_call_base_url) - val textInputNode = rule.onAllNodes(isEditable().and(isFocusable())).filterToOne(hasAnyAncestor(isDialog())) + clickOn(R.string.screen_advanced_settings_element_call_base_url) + val textInputNode = onAllNodes(isEditable().and(isFocusable())).filterToOne(hasAnyAncestor(isDialog())) textInputNode.performTextInput("https://call.element.dev") - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(AppDeveloperSettingsEvent.SetCustomElementCallBaseUrl("https://call.element.dev")) } @Config(qualifiers = "h2000dp") @Test - fun `clicking on open showkase invokes the expected callback`() { + fun `clicking on open showkase invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setAppDeveloperSettingsView( + setAppDeveloperSettingsView( state = anAppDeveloperSettingsState( eventSink = eventsRecorder ), onOpenShowkase = it ) - rule.onNodeWithText("Open Showkase browser").performClick() + onNodeWithText("Open Showkase browser").performClick() } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on log level emits the expected event`() { + fun `clicking on log level emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAppDeveloperSettingsView( + setAppDeveloperSettingsView( state = anAppDeveloperSettingsState( eventSink = eventsRecorder ), ) - rule.onNodeWithText("Tracing log level").performClick() - rule.onNodeWithText("Debug").performClick() + onNodeWithText("Tracing log level").performClick() + onNodeWithText("Debug").performClick() eventsRecorder.assertSingle(AppDeveloperSettingsEvent.SetTracingLogLevel(LogLevelItem.DEBUG)) } } -private fun AndroidComposeTestRule.setAppDeveloperSettingsView( +private fun AndroidComposeUiTest.setAppDeveloperSettingsView( state: AppDeveloperSettingsState, onOpenShowkase: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt index ea140abbd7..66ed0339a3 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt @@ -6,13 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.preferences.impl.notifications import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.preferences.impl.R import io.element.android.libraries.architecture.AsyncAction @@ -25,76 +28,71 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class NotificationSettingsViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() ensureCalledOnce { - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder ), onBackClick = it ) - rule.pressBack() + pressBack() } eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on troubleshoot notification invokes the expected callback`() { + fun `clicking on troubleshoot notification invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() ensureCalledOnce { - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder ), onTroubleshootNotificationsClick = it ) - rule.clickOn(R.string.troubleshoot_notifications_entry_point_title) + clickOn(R.string.troubleshoot_notifications_entry_point_title) } eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on group chats invokes the expected callback`() { + fun `clicking on group chats invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() ensureCalledOnceWithParam(false) { - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder ), onOpenEditDefault = it ) - rule.clickOn(R.string.screen_notification_settings_group_chats) + clickOn(R.string.screen_notification_settings_group_chats) } eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on direct chats invokes the expected callback`() { + fun `clicking on direct chats invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() ensureCalledOnceWithParam(true) { - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder ), onOpenEditDefault = it ) - rule.clickOn(R.string.screen_notification_settings_direct_chats) + clickOn(R.string.screen_notification_settings_direct_chats) } eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) } @@ -111,15 +109,15 @@ class NotificationSettingsViewTest { testNotificationToggle(false) } - private fun testNotificationToggle(initialState: Boolean) { + private fun testNotificationToggle(initialState: Boolean) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( appNotificationEnabled = initialState, eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_notification_settings_enable_notifications) + clickOn(R.string.screen_notification_settings_enable_notifications) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -140,15 +138,15 @@ class NotificationSettingsViewTest { testAtRoomToggle(false) } - private fun testAtRoomToggle(initialState: Boolean) { + private fun testAtRoomToggle(initialState: Boolean) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( atRoomNotificationsEnabled = initialState, eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_notification_settings_room_mention_label) + clickOn(R.string.screen_notification_settings_room_mention_label) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -169,15 +167,15 @@ class NotificationSettingsViewTest { testInvitationToggle(false) } - private fun testInvitationToggle(initialState: Boolean) { + private fun testInvitationToggle(initialState: Boolean) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( inviteForMeNotificationsEnabled = initialState, eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_notification_settings_invite_for_me_label) + clickOn(R.string.screen_notification_settings_invite_for_me_label) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -188,15 +186,15 @@ class NotificationSettingsViewTest { @Config(qualifiers = "h1024dp") @Test - fun `with an error configuration, clicking on continue emits the expected events`() { + fun `with an error configuration, clicking on continue emits the expected events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( changeNotificationSettingAction = AsyncAction.Failure(AN_EXCEPTION), eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -207,15 +205,15 @@ class NotificationSettingsViewTest { @Config(qualifiers = "h1024dp") @Test - fun `with invalid configuration, clicking on continue emits the expected events`() { + fun `with invalid configuration, clicking on continue emits the expected events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aInvalidNotificationSettingsState( fixFailed = false, eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -226,15 +224,15 @@ class NotificationSettingsViewTest { @Config(qualifiers = "h1024dp") @Test - fun `with invalid configuration and error, clicking on OK emits the expected events`() { + fun `with invalid configuration and error, clicking on OK emits the expected events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aInvalidNotificationSettingsState( fixFailed = true, eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -245,14 +243,14 @@ class NotificationSettingsViewTest { @Config(qualifiers = "h1024dp") @Test - fun `clicking on Push notification provider emits the expected event`() { + fun `clicking on Push notification provider emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_advanced_settings_push_provider_android) + clickOn(R.string.screen_advanced_settings_push_provider_android) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -262,16 +260,16 @@ class NotificationSettingsViewTest { } @Test - fun `clicking on a push provider emits the expected event`() { + fun `clicking on a push provider emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder, showChangePushProviderDialog = true, availablePushDistributors = listOf(aDistributor("P1"), aDistributor("P2")) ), ) - rule.onNodeWithText("P2").performClick() + onNodeWithText("P2").performClick() eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -281,7 +279,7 @@ class NotificationSettingsViewTest { } } -private fun AndroidComposeTestRule.setNotificationSettingsView( +private fun AndroidComposeUiTest.setNotificationSettingsView( state: NotificationSettingsState, onOpenEditDefault: (isOneToOne: Boolean) -> Unit = EnsureNeverCalledWithParam(), onTroubleshootNotificationsClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt index da91bdbf86..88ebbf64a1 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt @@ -5,13 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.preferences.impl.root import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.preferences.impl.R import io.element.android.libraries.matrix.api.user.MatrixUser @@ -25,49 +28,45 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PreferencesRootViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on back invokes back callback`() { + fun `clicking on back invokes back callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( eventSink = eventsRecorder ), onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `click on User profile invokes the expected callback`() { + fun `click on User profile invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val user = aMatrixUser() ensureCalledOnceWithParam(user) { callback -> - rule.setView( + setView( aPreferencesRootState( myUser = user, eventSink = eventsRecorder, ), onOpenUserProfile = callback, ) - rule.onNodeWithText("Alice").performClick() + onNodeWithText("Alice").performClick() } } @Test - fun `clicking on other session sends a SwitchToSession`() { + fun `clicking on other session sends a SwitchToSession`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setView( + setView( aPreferencesRootState( isMultiAccountEnabled = true, otherSessions = listOf( @@ -79,366 +78,366 @@ class PreferencesRootViewTest { eventSink = eventsRecorder, ), ) - rule.onNodeWithText("Bob").performClick() + onNodeWithText("Bob").performClick() eventsRecorder.assertSingle(PreferencesRootEvent.SwitchToSession(A_USER_ID_2)) } @Test - fun `click on Add account invokes the expected callback`() { + fun `click on Add account invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( isMultiAccountEnabled = true, eventSink = eventsRecorder, ), onAddAccountClick = callback, ) - rule.clickOn(CommonStrings.common_add_another_account) + clickOn(CommonStrings.common_add_another_account) } } @Test - fun `when multi account is not enabled, item is not shown`() { + fun `when multi account is not enabled, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( isMultiAccountEnabled = false, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.common_add_another_account)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.common_add_another_account)).assertDoesNotExist() } @Test - fun `click on Encryption invokes the expected callback`() { + fun `click on Encryption invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( showSecureBackup = true, eventSink = eventsRecorder, ), onSecureBackupClick = callback, ) - rule.clickOn(CommonStrings.common_encryption) + clickOn(CommonStrings.common_encryption) } } @Test - fun `when showSecureBackup is false, item is not shown`() { + fun `when showSecureBackup is false, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( showSecureBackup = false, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.common_encryption)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.common_encryption)).assertDoesNotExist() } @Test - fun `click on Manage account invokes the expected callback`() { + fun `click on Manage account invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam("aUrl") { callback -> - rule.setView( + setView( aPreferencesRootState( accountManagementUrl = "aUrl", eventSink = eventsRecorder, ), onManageAccountClick = callback, ) - rule.clickOn(CommonStrings.action_manage_account_and_devices) + clickOn(CommonStrings.action_manage_account_and_devices) } } @Test - fun `when accountManagementUrl is null, item is not shown`() { + fun `when accountManagementUrl is null, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( accountManagementUrl = null, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.action_manage_account_and_devices)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.action_manage_account_and_devices)).assertDoesNotExist() } @Test - fun `click on Link new devices invokes the expected callback`() { + fun `click on Link new devices invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( showLinkNewDevice = true, eventSink = eventsRecorder, ), onLinkNewDeviceClick = callback, ) - rule.clickOn(CommonStrings.common_link_new_device) + clickOn(CommonStrings.common_link_new_device) } } @Test - fun `when showLinkNewDevice is false, item is not shown`() { + fun `when showLinkNewDevice is false, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( showLinkNewDevice = false, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.common_link_new_device)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.common_link_new_device)).assertDoesNotExist() } @Test - fun `click on Analytics invokes the expected callback`() { + fun `click on Analytics invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( showAnalyticsSettings = true, eventSink = eventsRecorder, ), onOpenAnalytics = callback, ) - rule.clickOn(CommonStrings.common_analytics) + clickOn(CommonStrings.common_analytics) } } @Test - fun `when showAnalyticsSettings is false, item is not shown`() { + fun `when showAnalyticsSettings is false, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( showAnalyticsSettings = false, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.common_analytics)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.common_analytics)).assertDoesNotExist() } @Test - fun `click on Report a problem invokes the expected callback`() { + fun `click on Report a problem invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( canReportBug = true, eventSink = eventsRecorder, ), onOpenRageShake = callback, ) - rule.clickOn(CommonStrings.common_report_a_problem) + clickOn(CommonStrings.common_report_a_problem) } } @Test - fun `when canReportBug is false, item is not shown`() { + fun `when canReportBug is false, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( canReportBug = false, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.common_report_a_problem)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.common_report_a_problem)).assertDoesNotExist() } @Test - fun `click on Screen lock invokes the expected callback`() { + fun `click on Screen lock invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( eventSink = eventsRecorder, ), onOpenLockScreenSettings = callback, ) - rule.clickOn(CommonStrings.common_screen_lock) + clickOn(CommonStrings.common_screen_lock) } } @Test - fun `click on About invokes the expected callback`() { + fun `click on About invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( eventSink = eventsRecorder, ), onOpenAbout = callback, ) - rule.clickOn(CommonStrings.common_about) + clickOn(CommonStrings.common_about) } } @Test - fun `click on Developer settings invokes the expected callback`() { + fun `click on Developer settings invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( showDeveloperSettings = true, eventSink = eventsRecorder, ), onOpenDeveloperSettings = callback, ) - rule.clickOn(CommonStrings.common_developer_options) + clickOn(CommonStrings.common_developer_options) } } @Test - fun `when showDeveloperSettings is false, item is not shown`() { + fun `when showDeveloperSettings is false, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( showDeveloperSettings = false, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.common_developer_options)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.common_developer_options)).assertDoesNotExist() } @Test - fun `click on Advanced settings invokes the expected callback`() { + fun `click on Advanced settings invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( eventSink = eventsRecorder, ), onOpenAdvancedSettings = callback, ) - rule.clickOn(CommonStrings.common_advanced_settings) + clickOn(CommonStrings.common_advanced_settings) } } @Test - fun `click on Labs invokes the expected callback`() { + fun `click on Labs invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( showLabsItem = true, eventSink = eventsRecorder, ), onOpenLabs = callback, ) - rule.clickOn(R.string.screen_labs_title) + clickOn(R.string.screen_labs_title) } } @Test - fun `when showLabsItem is false, item is not shown`() { + fun `when showLabsItem is false, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( showLabsItem = false, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(R.string.screen_labs_title)).assertDoesNotExist() + onNodeWithText(activity!!.getString(R.string.screen_labs_title)).assertDoesNotExist() } @Test - fun `click on Notification invokes the expected callback`() { + fun `click on Notification invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( eventSink = eventsRecorder, ), onOpenNotificationSettings = callback, ) - rule.clickOn(R.string.screen_notification_settings_title) + clickOn(R.string.screen_notification_settings_title) } } @Test - fun `click on Blocked users invokes the expected callback`() { + fun `click on Blocked users invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( nbOfBlockedUsers = 1, eventSink = eventsRecorder, ), onOpenBlockedUsers = callback, ) - rule.clickOn(CommonStrings.common_blocked_users) + clickOn(CommonStrings.common_blocked_users) } } @Test - fun `when nbOfBlockedUsers is 0, item is not shown`() { + fun `when nbOfBlockedUsers is 0, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( nbOfBlockedUsers = 0, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.common_blocked_users)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.common_blocked_users)).assertDoesNotExist() } @Test - fun `click on Remove this device invokes the expected callback`() { + fun `click on Remove this device invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( eventSink = eventsRecorder, ), onSignOutClick = callback, ) - rule.clickOn(CommonStrings.action_signout) + clickOn(CommonStrings.action_signout) } } @Test - fun `click on Deactivate invokes the expected callback`() { + fun `click on Deactivate invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( canDeactivateAccount = true, eventSink = eventsRecorder, ), onDeactivateClick = callback, ) - rule.clickOn(CommonStrings.action_delete_account) + clickOn(CommonStrings.action_delete_account) } } @Test - fun `when canDeactivateAccount is false, item is not shown`() { + fun `when canDeactivateAccount is false, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( canDeactivateAccount = false, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.action_delete_account)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.action_delete_account)).assertDoesNotExist() } @Test - fun `clicking on version sends a PreferencesRootEvents`() { + fun `clicking on version sends a PreferencesRootEvents`() = runAndroidComposeUiTest { val version = "VERSION" val eventsRecorder = EventsRecorder() - rule.setView( + setView( aPreferencesRootState( version = version, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(version).performClick() + onNodeWithText(version).performClick() eventsRecorder.assertSingle(PreferencesRootEvent.OnVersionInfoClick) } } -private fun AndroidComposeTestRule.setView( +private fun AndroidComposeUiTest.setView( state: PreferencesRootState, onBackClick: () -> Unit = EnsureNeverCalled(), onAddAccountClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileViewTest.kt index 728e05ee7e..20db955955 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileViewTest.kt @@ -6,14 +6,17 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.preferences.impl.user.editprofile import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.ui.media.AvatarAction @@ -23,96 +26,93 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class EditUserProfileViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on back emits the expected event`() { + fun `clicking on back emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setEditUserProfileView( + setEditUserProfileView( aEditUserProfileState( eventSink = eventsRecorder, ), ) - rule.pressBack() + pressBack() eventsRecorder.assertSingle(EditUserProfileEvent.Exit) } @Test - fun `clicking on save from the exit confirmation dialog emits the expected event`() { + fun `clicking on save from the exit confirmation dialog emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setEditUserProfileView( + setEditUserProfileView( aEditUserProfileState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_save, inDialog = true) + clickOn(CommonStrings.action_save, inDialog = true) eventsRecorder.assertSingle(EditUserProfileEvent.Save) } @Test - fun `clicking on discard exit emits the expected event`() { + fun `clicking on discard exit emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setEditUserProfileView( + setEditUserProfileView( aEditUserProfileState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_discard) + clickOn(CommonStrings.action_discard) eventsRecorder.assertSingle(EditUserProfileEvent.Exit) } @Test - fun `clicking on save emits the expected event`() { + fun `clicking on save emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setEditUserProfileView( + setEditUserProfileView( aEditUserProfileState( saveButtonEnabled = true, saveAction = AsyncAction.Uninitialized, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) eventsRecorder.assertSingle(EditUserProfileEvent.Save) } @Test - fun `clicking on avatar opens the bottom sheet dialog`() { + fun `clicking on avatar opens the bottom sheet dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val actions = listOf( AvatarAction.TakePhoto, AvatarAction.ChoosePhoto, AvatarAction.Remove, ) - rule.setEditUserProfileView( + setEditUserProfileView( aEditUserProfileState( saveAction = AsyncAction.Uninitialized, avatarActions = actions, eventSink = eventsRecorder, ), ) - val contentDescription = rule.activity.getString(CommonStrings.a11y_avatar) - rule.onNodeWithContentDescription(contentDescription).performClick() + val resources = activity!!.resources + val contentDescription = resources.getString(CommonStrings.a11y_avatar) + onNodeWithContentDescription(contentDescription).performClick() // Assert that the actions are displayed actions.forEach { action -> - val text = rule.activity.getString(action.titleResId) - rule.onNodeWithText(text).assertExists() + val text = resources.getString(action.titleResId) + onNodeWithText(text).assertExists() } } @Test - fun `success invokes the expected callback`() { + fun `success invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setEditUserProfileView( + setEditUserProfileView( aEditUserProfileState( saveAction = AsyncAction.Success(Unit), eventSink = eventsRecorder, @@ -123,7 +123,7 @@ class EditUserProfileViewTest { } } -private fun AndroidComposeTestRule.setEditUserProfileView( +private fun AndroidComposeUiTest.setEditUserProfileView( state: EditUserProfileState, onEditProfileSuccess: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt index 59d9507571..a18c82b275 100644 --- a/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt +++ b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt @@ -6,13 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.reportroom.impl import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -20,76 +23,72 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ReportRoomViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on back invoke the expected callback`() { + fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setReportRoomView( + setReportRoomView( aReportRoomState( eventSink = eventsRecorder, ), onBackClick = it ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on report when enabled emits the expected event`() { + fun `clicking on report when enabled emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setReportRoomView( + setReportRoomView( aReportRoomState( reason = "Spam", eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_report) + clickOn(CommonStrings.action_report) eventsRecorder.assertSingle(ReportRoomEvents.Report) } @Test - fun `clicking on decline when disabled does not emit event`() { + fun `clicking on decline when disabled does not emit event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setReportRoomView( + setReportRoomView( aReportRoomState(eventSink = eventsRecorder), ) - rule.clickOn(CommonStrings.action_report) + clickOn(CommonStrings.action_report) } @Test - fun `clicking on leave room option emits the expected event`() { + fun `clicking on leave room option emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setReportRoomView( + setReportRoomView( aReportRoomState(eventSink = eventsRecorder), ) - rule.clickOn(CommonStrings.action_leave_room) + clickOn(CommonStrings.action_leave_room) eventsRecorder.assertSingle(ReportRoomEvents.ToggleLeaveRoom) } @Test - fun `typing text in the reason field emits the expected Event`() { + fun `typing text in the reason field emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setReportRoomView( + setReportRoomView( aReportRoomState( eventSink = eventsRecorder, reason = "" ), ) - rule.onNodeWithText("").performTextInput("Spam!") + onNodeWithText("").performTextInput("Spam!") eventsRecorder.assertSingle(ReportRoomEvents.UpdateReason("Spam!")) } } -private fun AndroidComposeTestRule.setReportRoomView( +private fun AndroidComposeUiTest.setReportRoomView( state: ReportRoomState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsViewTest.kt b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsViewTest.kt index f28c9c150f..668c6bb221 100644 --- a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsViewTest.kt +++ b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.rolesandpermissions.impl.permissions import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.rolesandpermissions.impl.R import io.element.android.libraries.architecture.AsyncAction @@ -23,84 +26,80 @@ import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ChangeRoomPermissionsViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `click on back icon invokes Exit`() { + fun `click on back icon invokes Exit`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( eventSink = recorder ) ) - rule.pressBack() + pressBack() recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) } @Test - fun `click on back key invokes Exit`() { + fun `click on back key invokes Exit`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( eventSink = recorder ) ) - rule.pressBackKey() + pressBackKey() recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) } @Test - fun `when confirming exit with pending changes, using the back key actually exits`() { + fun `when confirming exit with pending changes, using the back key actually exits`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, eventSink = recorder, ), ) - rule.pressBackKey() + pressBackKey() recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) } @Test - fun `when confirming exit with pending changes, clicking on 'discard' button in the dialog actually exits`() { + fun `when confirming exit with pending changes, clicking on 'discard' button in the dialog actually exits`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, saveAction = AsyncAction.ConfirmingCancellation, eventSink = recorder, ), ) - rule.clickOn(CommonStrings.action_discard) + clickOn(CommonStrings.action_discard) recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) } @Test - fun `when confirming exit with pending changes, clicking on 'save' button in the dialog saves the changes`() { + fun `when confirming exit with pending changes, clicking on 'save' button in the dialog saves the changes`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, saveAction = AsyncAction.ConfirmingCancellation, eventSink = recorder, ), ) - rule.clickOn(CommonStrings.action_save, inDialog = true) + clickOn(CommonStrings.action_save, inDialog = true) recorder.assertSingle(ChangeRoomPermissionsEvent.Save) } @Test - fun `click on a role item triggers ChangeRole event`() { + fun `click on a role item triggers ChangeRole event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( itemsBySection = persistentMapOf( // Makes sure there is only one item to click on @@ -109,70 +108,70 @@ class ChangeRoomPermissionsViewTest { eventSink = recorder, ) ) - rule.clickOn(R.string.screen_room_change_permissions_room_name) - rule.clickOn(R.string.screen_room_change_permissions_everyone) + clickOn(R.string.screen_room_change_permissions_room_name) + clickOn(R.string.screen_room_change_permissions_everyone) recorder.assertSingle( ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, SelectableRole.Everyone), ) } @Test - fun `click on the Save menu item triggers Save event`() { + fun `click on the Save menu item triggers Save event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, eventSink = recorder, ), ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) recorder.assertSingle(ChangeRoomPermissionsEvent.Save) } @Test - fun `a successful save exits the screen`() { + fun `a successful save exits the screen`() = runAndroidComposeUiTest { ensureCalledOnceWithParam(true) { callback -> - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, saveAction = AsyncAction.Success(true), ), onComplete = callback, ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) } } @Test - fun `a cancellation exits the screen`() { + fun `a cancellation exits the screen`() = runAndroidComposeUiTest { ensureCalledOnceWithParam(false) { callback -> - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, saveAction = AsyncAction.Success(false), ), onComplete = callback, ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) } } @Test - fun `click on the Ok option in save error dialog triggers ResetPendingAction event`() { + fun `click on the Ok option in save error dialog triggers ResetPendingAction event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, saveAction = AsyncAction.Failure(IllegalStateException("Failed to set room power levels")), eventSink = recorder, ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) recorder.assertSingle(ChangeRoomPermissionsEvent.ResetPendingActions) } } -private fun AndroidComposeTestRule.setChangeRoomPermissionsRule( +private fun AndroidComposeUiTest.setChangeRoomPermissionsRule( state: ChangeRoomPermissionsState = aChangeRoomPermissionsState(), onComplete: (Boolean) -> Unit = EnsureNeverCalledWithParam(), ) { diff --git a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesViewTest.kt b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesViewTest.kt index 09bef49cbd..62d0608f26 100644 --- a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesViewTest.kt +++ b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesViewTest.kt @@ -6,15 +6,18 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.rolesandpermissions.impl.roles import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import io.element.android.libraries.architecture.AsyncAction @@ -30,20 +33,16 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey import kotlinx.collections.immutable.toImmutableList -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class ChangeRolesViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `passing a 'User' role throws an exception`() { + fun `passing a 'User' role throws an exception`() = runAndroidComposeUiTest { val exception = runCatchingExceptions { - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( role = RoomMember.Role.User, eventSink = EnsureNeverCalledWithParam(), @@ -54,106 +53,106 @@ class ChangeRolesViewTest { } @Test - fun `back key - with search active toggles the search`() { + fun `back key - with search active toggles the search`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( isSearchActive = true, eventSink = eventsRecorder, ), ) - rule.pressBackKey() + pressBackKey() // Advance time to let the event be processed, as the search toggle might have some delay (e.g. for the animation) - rule.mainClock.advanceTimeBy(1) + mainClock.advanceTimeBy(1) eventsRecorder.assertSingle(ChangeRolesEvent.ToggleSearchActive) } @Test - fun `back key - with search inactive exits the screen`() { + fun `back key - with search inactive exits the screen`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( isSearchActive = false, eventSink = eventsRecorder, ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(ChangeRolesEvent.Exit) } @Test - fun `back button - exits the screen`() { + fun `back button - exits the screen`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( isSearchActive = false, eventSink = eventsRecorder, ), ) - rule.pressBack() + pressBack() eventsRecorder.assertSingle(ChangeRolesEvent.Exit) } @Test - fun `save button - with changes, it saves them`() { + fun `save button - with changes, it saves them`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( hasPendingChanges = true, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) eventsRecorder.assertSingle(ChangeRolesEvent.Save) } @Test - fun `save button - with no changes, does nothing`() { + fun `save button - with no changes, does nothing`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( hasPendingChanges = false, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) eventsRecorder.assertEmpty() } @Test - fun `exit confirmation dialog - discard exits the screen`() { + fun `exit confirmation dialog - discard exits the screen`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( isSearchActive = true, savingState = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_discard) + clickOn(CommonStrings.action_discard) eventsRecorder.assertSingle(ChangeRolesEvent.Exit) } @Test - fun `exit confirmation dialog - save emits the save event`() { + fun `exit confirmation dialog - save emits the save event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( isSearchActive = true, savingState = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) eventsRecorder.assertSingle(ChangeRolesEvent.Save) } @Test - fun `save admins confirmation dialog - submit saves the changes`() { + fun `save admins confirmation dialog - submit saves the changes`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( role = RoomMember.Role.Admin, isSearchActive = true, @@ -161,14 +160,14 @@ class ChangeRolesViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(ChangeRolesEvent.Save) } @Test - fun `save owners confirmation dialog - continue saves the changes`() { + fun `save owners confirmation dialog - continue saves the changes`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( role = RoomMember.Role.Owner(isCreator = false), isSearchActive = true, @@ -176,14 +175,14 @@ class ChangeRolesViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ChangeRolesEvent.Save) } @Test - fun `save admins confirmation dialog - cancel removes the dialog`() { + fun `save admins confirmation dialog - cancel removes the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( role = RoomMember.Role.Admin, isSearchActive = true, @@ -191,14 +190,14 @@ class ChangeRolesViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog) } @Test - fun `save owners confirmation dialog - cancel removes the dialog`() { + fun `save owners confirmation dialog - cancel removes the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( role = RoomMember.Role.Owner(isCreator = false), isSearchActive = true, @@ -206,39 +205,39 @@ class ChangeRolesViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog) } @Test - fun `error dialog - dismissing removes the dialog`() { + fun `error dialog - dismissing removes the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( isSearchActive = true, savingState = AsyncAction.Failure(IllegalStateException("boom")), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog) } @Test - fun `testing removing user from selected list emits the expected event`() { + fun `testing removing user from selected list emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val selectedUsers = aMatrixUserList().take(2) val userToDeselect = selectedUsers[1] assertThat(userToDeselect.displayName).isEqualTo("Bob") - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesStateWithSelectedUsers().copy( selectedUsers = selectedUsers.toImmutableList(), eventSink = eventsRecorder, ), ) // Unselect the user from the row list - val contentDescription = rule.activity.getString(CommonStrings.action_remove) - rule.onNodeWithContentDescription( + val contentDescription = activity!!.getString(CommonStrings.action_remove) + onNodeWithContentDescription( label = contentDescription, useUnmergedTree = true, ).performClick() @@ -247,7 +246,7 @@ class ChangeRolesViewTest { @Test @Config(qualifiers = "h1000dp") - fun `testing adding user to the selected list emits the expected event`() { + fun `testing adding user to the selected list emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val selectedUsers = aMatrixUserList().take(2) val state = aChangeRolesStateWithSelectedUsers().copy( @@ -256,16 +255,16 @@ class ChangeRolesViewTest { ) val userToSelect = (state.searchResults as SearchBarResultState.Results).results.members.first().toMatrixUser() assertThat(userToSelect.displayName).isEqualTo("Carol") - rule.setChangeRolesContent( + setChangeRolesContent( state = state, ) // Select the user from the user list - rule.onNodeWithText("Carol").performClick() + onNodeWithText("Carol").performClick() eventsRecorder.assertSingle(ChangeRolesEvent.UserSelectionToggled(userToSelect)) } @Test - fun `testing removing user to the selected list emits the expected event`() { + fun `testing removing user to the selected list emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val selectedUsers = aMatrixUserList().take(2) val state = aChangeRolesStateWithSelectedUsers().copy( @@ -274,18 +273,18 @@ class ChangeRolesViewTest { ) val userToSelect = (state.searchResults as SearchBarResultState.Results).results.moderators.first().toMatrixUser() assertThat(userToSelect.displayName).isEqualTo("Bob") - rule.setChangeRolesContent( + setChangeRolesContent( state = state, ) // Unselect the user from the user list - rule.onAllNodesWithText( + onAllNodesWithText( text = "Bob", useUnmergedTree = true, )[1].performClick() eventsRecorder.assertSingle(ChangeRolesEvent.UserSelectionToggled(userToSelect)) } - private fun AndroidComposeTestRule.setChangeRolesContent( + private fun AndroidComposeUiTest.setChangeRolesContent( state: ChangeRolesState, ) { setContent { diff --git a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsViewTest.kt b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsViewTest.kt index e08ae205b7..d8908c405d 100644 --- a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsViewTest.kt +++ b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.rolesandpermissions.impl.root import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.rolesandpermissions.impl.R import io.element.android.libraries.architecture.AsyncAction @@ -23,159 +26,154 @@ import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledTimes import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.setSafeContent -import kotlinx.coroutines.test.runTest -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class RolesAndPermissionsViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `click on back invokes expected callback`() { + fun `click on back invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( goBack = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `tapping on Admins opens admin list`() { + fun `tapping on Admins opens admin list`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( aRolesAndPermissionsState( roomSupportsOwners = false, eventSink = EventsRecorder(expectEvents = false) ), openAdminList = callback, ) - rule.clickOn(R.string.screen_room_roles_and_permissions_admins) + clickOn(R.string.screen_room_roles_and_permissions_admins) } } @Test - fun `tapping on Admins and Owners opens admin list`() { + fun `tapping on Admins and Owners opens admin list`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( aRolesAndPermissionsState( roomSupportsOwners = true, eventSink = EventsRecorder(expectEvents = false) ), openAdminList = callback, ) - rule.clickOn(R.string.screen_room_roles_and_permissions_admins_and_owners) + clickOn(R.string.screen_room_roles_and_permissions_admins_and_owners) } } @Test - fun `tapping on Moderators opens moderators list`() { + fun `tapping on Moderators opens moderators list`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( openModeratorList = callback, ) - rule.clickOn(R.string.screen_room_roles_and_permissions_moderators) + clickOn(R.string.screen_room_roles_and_permissions_moderators) } } @Test @Config(qualifiers = "h640dp") - fun `tapping permission item open the change permissions screen`() { + fun `tapping permission item open the change permissions screen`() = runAndroidComposeUiTest { ensureCalledTimes(1) { callback -> - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( openEditPermissions = callback, ) - rule.clickOn(R.string.screen_room_roles_and_permissions_permissions_header) + clickOn(R.string.screen_room_roles_and_permissions_permissions_header) } } @Test @Config(qualifiers = "h640dp") - fun `tapping on reset permissions triggers ResetPermissions event`() { + fun `tapping on reset permissions triggers ResetPermissions event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( state = aRolesAndPermissionsState( eventSink = recorder, ), ) - rule.clickOn(R.string.screen_room_roles_and_permissions_reset) + clickOn(R.string.screen_room_roles_and_permissions_reset) recorder.assertSingle(RolesAndPermissionsEvents.ResetPermissions) } @Test - fun `tapping on Reset in the reset permissions confirmation dialog triggers ResetPermissions event`() { + fun `tapping on Reset in the reset permissions confirmation dialog triggers ResetPermissions event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( state = aRolesAndPermissionsState( resetPermissionsAction = AsyncAction.ConfirmingNoParams, eventSink = recorder, ), ) - rule.clickOn(CommonStrings.action_reset) + clickOn(CommonStrings.action_reset) recorder.assertSingle(RolesAndPermissionsEvents.ResetPermissions) } @Test - fun `tapping on Cancel in the reset permissions confirmation dialog triggers CancelPendingAction event`() { + fun `tapping on Cancel in the reset permissions confirmation dialog triggers CancelPendingAction event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( state = aRolesAndPermissionsState( resetPermissionsAction = AsyncAction.ConfirmingNoParams, eventSink = recorder, ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) recorder.assertSingle(RolesAndPermissionsEvents.CancelPendingAction) } @Test - fun `tapping on 'Demote to moderator' in the demote self bottom sheet triggers the right event`() { + fun `tapping on 'Demote to moderator' in the demote self bottom sheet triggers the right event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( state = aRolesAndPermissionsState( changeOwnRoleAction = AsyncAction.ConfirmingNoParams, eventSink = recorder, ), ) - rule.clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator) - rule.mainClock.advanceTimeBy(1_000L) + clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator) + mainClock.advanceTimeBy(1_000L) recorder.assertSingle(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.Moderator)) } @Test - fun `tapping on 'Demote to member' in the demote self bottom sheet triggers the right event`() = runTest { + fun `tapping on 'Demote to member' in the demote self bottom sheet triggers the right event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( state = aRolesAndPermissionsState( changeOwnRoleAction = AsyncAction.ConfirmingNoParams, eventSink = recorder, ), ) - rule.clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_member) - rule.mainClock.advanceTimeBy(1_000L) + clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_member) + mainClock.advanceTimeBy(1_000L) recorder.assertSingle(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.User)) } @Test - fun `tapping on 'Cancel' in the demote self bottom sheet triggers the right event`() { + fun `tapping on 'Cancel' in the demote self bottom sheet triggers the right event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( state = aRolesAndPermissionsState( changeOwnRoleAction = AsyncAction.ConfirmingNoParams, eventSink = recorder, ), ) - rule.clickOn(CommonStrings.action_cancel) - rule.mainClock.advanceTimeBy(1_000L) + clickOn(CommonStrings.action_cancel) + mainClock.advanceTimeBy(1_000L) recorder.assertSingle(RolesAndPermissionsEvents.CancelPendingAction) } } -private fun AndroidComposeTestRule.setRolesAndPermissionsView( +private fun AndroidComposeUiTest.setRolesAndPermissionsView( state: RolesAndPermissionsState = aRolesAndPermissionsState( roomSupportsOwners = false, eventSink = EventsRecorder(expectEvents = false), diff --git a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperViewTest.kt b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperViewTest.kt index 4b37f993f9..5f871183d6 100644 --- a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperViewTest.kt +++ b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.roomaliasresolver.impl import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias @@ -22,48 +25,44 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RoomAliasHelperViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setRoomAliasResolverView( + setRoomAliasResolverView( aRoomAliasResolverState( eventSink = eventsRecorder, ), onBackClick = it ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on Retry emits the expected Event`() { + fun `clicking on Retry emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomAliasResolverView( + setRoomAliasResolverView( aRoomAliasResolverState( resolveState = AsyncData.Failure(Exception("Error")), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_retry) + clickOn(CommonStrings.action_retry) eventsRecorder.assertSingle(RoomAliasResolverEvents.Retry) } @Test - fun `success state invokes the expected Callback`() { + fun `success state invokes the expected Callback`() = runAndroidComposeUiTest { val result = aResolvedRoomAlias() val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(result) { - rule.setRoomAliasResolverView( + setRoomAliasResolverView( aRoomAliasResolverState( resolveState = AsyncData.Success(result), eventSink = eventsRecorder, @@ -74,7 +73,7 @@ class RoomAliasHelperViewTest { } } -private fun AndroidComposeTestRule.setRoomAliasResolverView( +private fun AndroidComposeUiTest.setRoomAliasResolverView( state: RoomAliasResolverState, onBackClick: () -> Unit = EnsureNeverCalled(), onAliasResolved: (ResolvedRoomAlias) -> Unit = EnsureNeverCalledWithParam(), diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt index 588a10a218..50139e0149 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt @@ -6,14 +6,17 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.roomdetails.impl import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.userprofile.shared.aUserProfileState @@ -32,98 +35,94 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class RoomDetailsViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `click on back invokes expected callback`() { + fun `click on back invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( goBack = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `click on share invokes expected callback`() { + fun `click on share invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( onShareRoom = callback, ) - rule.clickOn(CommonStrings.action_share) + clickOn(CommonStrings.action_share) } } @Config(qualifiers = "h1024dp") @Test - fun `click on room members invokes expected callback`() { + fun `click on room members invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( openRoomMemberList = callback, ) - rule.clickOn(CommonStrings.common_people) + clickOn(CommonStrings.common_people) } } @Config(qualifiers = "h1024dp") @Test - fun `click on polls invokes expected callback`() { + fun `click on polls invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( openPollHistory = callback, ) - rule.clickOn(R.string.screen_polls_history_title) + clickOn(R.string.screen_polls_history_title) } } @Config(qualifiers = "h1024dp") @Test - fun `click on media gallery invokes expected callback`() { + fun `click on media gallery invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( openMediaGallery = callback, ) - rule.clickOn(R.string.screen_room_details_media_gallery_title) + clickOn(R.string.screen_room_details_media_gallery_title) } } @Config(qualifiers = "h1024dp") @Test - fun `click on notification invokes expected callback`() { + fun `click on notification invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( openRoomNotificationSettings = callback, ) - rule.clickOn(R.string.screen_room_details_notification_title) + clickOn(R.string.screen_room_details_notification_title) } } @Test - fun `click on invite invokes expected callback`() { + fun `click on invite invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canInvite = true, ), invitePeople = callback, ) - rule.clickOn(CommonStrings.action_invite) + clickOn(CommonStrings.action_invite) } } @Test - fun `click on call invokes expected callback`() { + fun `click on call invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnceWithParam(CallIntent.AUDIO) { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canInvite = true, @@ -134,103 +133,103 @@ class RoomDetailsViewTest { ), onJoinCallClick = callback, ) - rule.clickOn(CommonStrings.action_call) + clickOn(CommonStrings.action_call) } } @Test - fun `click on video call invokes expected callback`() { + fun `click on video call invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnceWithParam(CallIntent.VIDEO) { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canInvite = true, ), onJoinCallClick = callback, ) - rule.clickOn(CommonStrings.common_video) + clickOn(CommonStrings.common_video) } } @Config(qualifiers = "h1024dp") @Test - fun `click on pinned messages invokes expected callback`() { + fun `click on pinned messages invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canInvite = true, ), onPinnedMessagesClick = callback, ) - rule.clickOn(R.string.screen_room_details_pinned_events_row_title) + clickOn(R.string.screen_room_details_pinned_events_row_title) } } @Config(qualifiers = "h1024dp") @Test - fun `click on security and privacy invokes expected callback`() { + fun `click on security and privacy invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canShowSecurityAndPrivacy = true, ), onSecurityAndPrivacyClick = callback, ) - rule.clickOn(R.string.screen_room_details_security_and_privacy_title) + clickOn(R.string.screen_room_details_security_and_privacy_title) } } @Config(qualifiers = "h1024dp") @Test - fun `click on add topic emit expected event`() { + fun `click on add topic emit expected event`() = runAndroidComposeUiTest { ensureCalledOnceWithParam(RoomDetailsAction.AddTopic) { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), roomTopic = RoomTopicState.CanAddTopic, ), onActionClick = callback, ) - rule.clickOn(R.string.screen_room_details_add_topic_title) + clickOn(R.string.screen_room_details_add_topic_title) } } @Test - fun `click on menu edit emit expected event`() { + fun `click on menu edit emit expected event`() = runAndroidComposeUiTest { ensureCalledOnceWithParam(RoomDetailsAction.Edit) { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canEdit = true, ), onActionClick = callback, ) - val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu) - rule.onNodeWithContentDescription(menuContentDescription).performClick() - rule.clickOn(CommonStrings.action_edit) + val menuContentDescription = activity!!.getString(CommonStrings.a11y_user_menu) + onNodeWithContentDescription(menuContentDescription).performClick() + clickOn(CommonStrings.action_edit) } } @Test - fun `click on avatar test`() { + fun `click on avatar test`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aRoomDetailsState( eventSink = eventsRecorder, roomAvatarUrl = "an_avatar_url", ) val callback = EnsureCalledOnceWithTwoParams(state.roomName, "an_avatar_url") - rule.setRoomDetailView( + setRoomDetailView( state = state, openAvatarPreview = callback, ) - rule.onNodeWithTag(TestTags.roomDetailAvatar.value).performClick() + onNodeWithTag(TestTags.roomDetailAvatar.value).performClick() callback.assertSuccess() } @Test - fun `click on avatar test on DM`() { + fun `click on avatar test on DM`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aRoomDetailsState( roomType = RoomDetailsType.Dm( @@ -241,114 +240,114 @@ class RoomDetailsViewTest { eventSink = eventsRecorder, ) val callback = EnsureCalledOnceWithTwoParams("Daniel", "an_avatar_url") - rule.setRoomDetailView( + setRoomDetailView( state = state, openAvatarPreview = callback, ) - rule.onNodeWithTag(TestTags.memberDetailAvatar.value).performClick() + onNodeWithTag(TestTags.memberDetailAvatar.value).performClick() callback.assertSuccess() } @Test - fun `click on mute emit expected event`() { + fun `click on mute emit expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomDetailsState( eventSink = eventsRecorder, roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.ALL_MESSAGES), ) - rule.setRoomDetailView( + setRoomDetailView( state = state, ) - rule.clickOn(CommonStrings.common_mute) + clickOn(CommonStrings.common_mute) eventsRecorder.assertSingle(RoomDetailsEvent.MuteNotification) } @Test - fun `click on unmute emit expected event`() { + fun `click on unmute emit expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomDetailsState( eventSink = eventsRecorder, roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.MUTE), ) - rule.setRoomDetailView( + setRoomDetailView( state = state, ) - rule.clickOn(CommonStrings.common_unmute) + clickOn(CommonStrings.common_unmute) eventsRecorder.assertSingle(RoomDetailsEvent.UnmuteNotification) } @Config(qualifiers = "h1024dp") @Test - fun `click on favorite emit expected Event`() { + fun `click on favorite emit expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.common_favourite) + clickOn(CommonStrings.common_favourite) eventsRecorder.assertSingle(RoomDetailsEvent.SetFavorite(true)) } @Config(qualifiers = "h1500dp") @Test - fun `click on leave emit expected Event`() { + fun `click on leave emit expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_room_details_leave_room_title) + clickOn(R.string.screen_room_details_leave_room_title) eventsRecorder.assertSingle(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) } @Config(qualifiers = "h1500dp") @Test - fun `click on report room invokes expected callback`() { + fun `click on report room invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), ), onReportRoomClick = callback, ) - rule.clickOn(CommonStrings.action_report_room) + clickOn(CommonStrings.action_report_room) } } @Config(qualifiers = "h1024dp") @Test - fun `click on knock requests invokes expected callback`() { + fun `click on knock requests invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canShowKnockRequests = true, ), onKnockRequestsClick = callback, ) - rule.clickOn(R.string.screen_room_details_requests_to_join_title) + clickOn(R.string.screen_room_details_requests_to_join_title) } } @Config(qualifiers = "h1024dp") @Test - fun `click on profile invokes the expected callback`() { + fun `click on profile invokes the expected callback`() = runAndroidComposeUiTest { ensureCalledOnceWithParam(A_USER_ID) { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), roomMemberDetailsState = aUserProfileState(userId = A_USER_ID), ), onProfileClick = callback, ) - rule.clickOn(R.string.screen_room_details_profile_row_title) + clickOn(R.string.screen_room_details_profile_row_title) } } } -private fun AndroidComposeTestRule.setRoomDetailView( +private fun AndroidComposeUiTest.setRoomDetailView( state: RoomDetailsState = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), ), diff --git a/features/roomdetailsedit/impl/src/test/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditViewTest.kt b/features/roomdetailsedit/impl/src/test/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditViewTest.kt index 71fb143074..686794d641 100644 --- a/features/roomdetailsedit/impl/src/test/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditViewTest.kt +++ b/features/roomdetailsedit/impl/src/test/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditViewTest.kt @@ -5,18 +5,21 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.roomdetailsedit.impl import androidx.activity.ComponentActivity import androidx.annotation.StringRes +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assert import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.isEditable -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.ui.media.AvatarAction @@ -28,58 +31,54 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import org.junit.Ignore -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RoomDetailsEditViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on back emits the expected Event`() { + fun `clicking on back emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder ), ) - rule.pressBack() + pressBack() eventsRecorder.assertSingle(RoomDetailsEditEvent.OnBackPress) } @Test - fun `clicking on discard when confirming exit emits the expected Event`() { + fun `clicking on discard when confirming exit emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_discard) + clickOn(CommonStrings.action_discard) eventsRecorder.assertSingle(RoomDetailsEditEvent.OnBackPress) } @Test - fun `clicking on save when confirming exit emits the expected Event`() { + fun `clicking on save when confirming exit emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_save, inDialog = true) + clickOn(CommonStrings.action_save, inDialog = true) eventsRecorder.assertSingle(RoomDetailsEditEvent.Save) } @Test - fun `when edition is successful, the expected callback is invoked`() { + fun `when edition is successful, the expected callback is invoked`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, saveAction = AsyncAction.Success(Unit) @@ -90,55 +89,55 @@ class RoomDetailsEditViewTest { } @Test - fun `when name is changed, the expected Event is emitted`() { + fun `when name is changed, the expected Event is emitted`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, roomRawName = "Marketing", ), ) - rule.onNodeWithText("Marketing").performTextInput("A") + onNodeWithText("Marketing").performTextInput("A") eventsRecorder.assertSingle(RoomDetailsEditEvent.UpdateRoomName("AMarketing")) } @Test - fun `when user cannot change name, nothing happen`() { + fun `when user cannot change name, nothing happen`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, roomRawName = "Marketing", canChangeName = false, ), ) - rule.onNodeWithText("Marketing").assert(!isEditable()) + onNodeWithText("Marketing").assert(!isEditable()) } @Test - fun `when topic is changed, the expected Event is emitted`() { + fun `when topic is changed, the expected Event is emitted`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, roomTopic = "My Topic", ), ) - rule.onNodeWithText("My Topic").performTextInput("A") + onNodeWithText("My Topic").performTextInput("A") eventsRecorder.assertSingle(RoomDetailsEditEvent.UpdateRoomTopic("AMy Topic")) } @Test - fun `when user cannot change topic, nothing happen`() { + fun `when user cannot change topic, nothing happen`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, roomTopic = "My Topic", canChangeTopic = false, ), ) - rule.onNodeWithText("My Topic").assert(!isEditable()) + onNodeWithText("My Topic").assert(!isEditable()) } @Ignore("This test is failing because the bottom sheet does not open") @@ -171,73 +170,73 @@ class RoomDetailsEditViewTest { private fun testAvatarChange( @StringRes stringActionRes: Int, expectedEvent: RoomDetailsEditEvent.HandleAvatarAction, - ) { + ) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, ), ) // Open the bottom sheet - rule.onNode(hasTestTag(TestTags.editAvatar.value)).performClick() - rule.onNodeWithText(rule.activity.getString(stringActionRes)).assertExists() - rule.clickOn(stringActionRes) + onNode(hasTestTag(TestTags.editAvatar.value)).performClick() + onNodeWithText(activity!!.getString(stringActionRes)).assertExists() + clickOn(stringActionRes) eventsRecorder.assertSingle(expectedEvent) } @Test - fun `when user cannot change avatar, nothing happen`() { + fun `when user cannot change avatar, nothing happen`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, canChangeAvatar = false, ), ) - rule.onNode(hasTestTag(TestTags.editAvatar.value)).performClick() - rule.onNodeWithText(rule.activity.getString(CommonStrings.action_take_photo)).assertDoesNotExist() + onNode(hasTestTag(TestTags.editAvatar.value)).performClick() + onNodeWithText(activity!!.getString(CommonStrings.action_take_photo)).assertDoesNotExist() } @Test - fun `when save is clicked, the expected Event is emitted`() { + fun `when save is clicked, the expected Event is emitted`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, saveButtonEnabled = true, ), ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) eventsRecorder.assertSingle(RoomDetailsEditEvent.Save) } @Test - fun `when save is clicked, but nothing need to be saved, nothing happens`() { + fun `when save is clicked, but nothing need to be saved, nothing happens`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, saveButtonEnabled = false, ), ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) } @Test - fun `when error is shown, closing the dialog emit the expected Event`() { + fun `when error is shown, closing the dialog emit the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, saveAction = AsyncAction.Failure(RuntimeException("Whelp")), ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(RoomDetailsEditEvent.CloseDialog) } } -private fun AndroidComposeTestRule.setRoomDetailsEditView( +private fun AndroidComposeUiTest.setRoomDetailsEditView( state: RoomDetailsEditState, onDone: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt index a50ad6a22c..f9d60f87da 100644 --- a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt +++ b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt @@ -6,15 +6,18 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.roomdirectory.impl.root import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.testtags.TestTags @@ -22,31 +25,27 @@ import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.ensureCalledOnceWithParam -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RoomDirectoryViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `typing text in search field emits the expected Event`() { + fun `typing text in search field emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDirectoryView( + setRoomDirectoryView( state = aRoomDirectoryState( eventSink = eventsRecorder, ) ) - rule.onNodeWithTag(TestTags.searchTextField.value).performTextInput( + onNodeWithTag(TestTags.searchTextField.value).performTextInput( text = "Test" ) eventsRecorder.assertSingle(RoomDirectoryEvents.Search("Test")) } @Test - fun `clicking on room item then onResultClick lambda is called once`() { + fun `clicking on room item then onResultClick lambda is called once`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomDirectoryState( roomDescriptions = aRoomDescriptionList(), @@ -54,27 +53,27 @@ class RoomDirectoryViewTest { ) val clickedRoom = state.roomDescriptions.first() ensureCalledOnceWithParam(clickedRoom) { callback -> - rule.setRoomDirectoryView( + setRoomDirectoryView( state = state, onResultClick = callback, ) - rule.onNodeWithText(clickedRoom.computedName).performClick() + onNodeWithText(clickedRoom.computedName).performClick() } } @Test - fun `composing load more indicator emits expected Event`() { + fun `composing load more indicator emits expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomDirectoryState( displayLoadMoreIndicator = true, eventSink = eventsRecorder, ) - rule.setRoomDirectoryView(state = state) + setRoomDirectoryView(state = state) eventsRecorder.assertSingle(RoomDirectoryEvents.LoadMore) } } -private fun AndroidComposeTestRule.setRoomDirectoryView( +private fun AndroidComposeUiTest.setRoomDirectoryView( state: RoomDirectoryState, onBackClick: () -> Unit = EnsureNeverCalled(), onResultClick: (RoomDescription) -> Unit = EnsureNeverCalledWithParam(), diff --git a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt index 6508b28053..646481715a 100644 --- a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt +++ b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.roommembermoderation.impl import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.ModerationActionState @@ -24,21 +27,17 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnceWithTwoParams import io.element.android.tests.testutils.pressTag import io.element.android.tests.testutils.setSafeContent -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RoomMemberModerationViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on display profile action calls onSelectAction`() { + fun `clicking on display profile action calls onSelectAction`() = runAndroidComposeUiTest { val user = anAlice() val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithTwoParams(ModerationAction.DisplayProfile, user) { callback -> - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = user, actions = listOf( @@ -48,16 +47,16 @@ class RoomMemberModerationViewTest { ), onSelectAction = callback ) - rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_member_user_info) + clickOn(R.string.screen_bottom_sheet_manage_room_member_member_user_info) } } @Test - fun `clicking on kick user action calls onSelectAction`() { + fun `clicking on kick user action calls onSelectAction`() = runAndroidComposeUiTest { val user = anAlice() val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithTwoParams(ModerationAction.KickUser, user) { callback -> - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = user, actions = listOf( @@ -67,18 +66,18 @@ class RoomMemberModerationViewTest { ), onSelectAction = callback ) - rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) + clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) // Gives time for bottomsheet to hide - rule.mainClock.advanceTimeBy(1_000) + mainClock.advanceTimeBy(1_000) } } @Test - fun `clicking on ban user action calls onSelectAction`() { + fun `clicking on ban user action calls onSelectAction`() = runAndroidComposeUiTest { val user = anAlice() val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithTwoParams(ModerationAction.BanUser, user) { callback -> - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = user, actions = listOf( @@ -88,18 +87,18 @@ class RoomMemberModerationViewTest { ), onSelectAction = callback ) - rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_ban) + clickOn(R.string.screen_bottom_sheet_manage_room_member_ban) // Gives time for bottomsheet to hide - rule.mainClock.advanceTimeBy(1_000) + mainClock.advanceTimeBy(1_000) } } @Test - fun `clicking on unban user action calls onSelectAction`() { + fun `clicking on unban user action calls onSelectAction`() = runAndroidComposeUiTest { val user = anAlice() val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithTwoParams(ModerationAction.UnbanUser, user) { callback -> - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = user, actions = listOf( @@ -109,100 +108,100 @@ class RoomMemberModerationViewTest { ), onSelectAction = callback ) - rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_unban) + clickOn(R.string.screen_bottom_sheet_manage_room_member_unban) // Gives time for bottomsheet to hide - rule.mainClock.advanceTimeBy(1_000) + mainClock.advanceTimeBy(1_000) } } @Test - fun `clicking submit on kick confirmation dialog sends DoKickUser event`() { + fun `clicking submit on kick confirmation dialog sends DoKickUser event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), kickUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.pressTag(TestTags.dialogPositive.value) + pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoKickUser(reason = "")) } @Test - fun `clicking dismiss on kick confirmation dialog sends Reset event`() { + fun `clicking dismiss on kick confirmation dialog sends Reset event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), kickUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.pressTag(TestTags.dialogNegative.value) + pressTag(TestTags.dialogNegative.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) } @Test - fun `clicking submit on ban confirmation dialog sends DoBanUser event`() { + fun `clicking submit on ban confirmation dialog sends DoBanUser event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), banUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.pressTag(TestTags.dialogPositive.value) + pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoBanUser(reason = "")) } @Test - fun `clicking dismiss on ban confirmation dialog sends Reset event`() { + fun `clicking dismiss on ban confirmation dialog sends Reset event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), banUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.pressTag(TestTags.dialogNegative.value) + pressTag(TestTags.dialogNegative.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) } @Test - fun `clicking confirm on unban confirmation dialog sends DoUnbanUser event`() { + fun `clicking confirm on unban confirmation dialog sends DoUnbanUser event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), unbanUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.pressTag(TestTags.dialogPositive.value) + pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoUnbanUser("")) } @Test - fun `clicking dismiss on unban confirmation dialog sends Reset event`() { + fun `clicking dismiss on unban confirmation dialog sends Reset event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), unbanUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.pressTag(TestTags.dialogNegative.value) + pressTag(TestTags.dialogNegative.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) } @Test - fun `disabled actions are not clickable`() { + fun `disabled actions are not clickable`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), actions = listOf( @@ -211,11 +210,11 @@ class RoomMemberModerationViewTest { eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) + clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) } } -private fun AndroidComposeTestRule.setRoomMemberModerationView( +private fun AndroidComposeUiTest.setRoomMemberModerationView( state: InternalRoomMemberModerationState, onSelectAction: (ModerationAction, MatrixUser) -> Unit = EnsureNeverCalledWithTwoParams(), ) { diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyViewTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyViewTest.kt index d9324fdb91..f9729f74f0 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyViewTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyViewTest.kt @@ -6,16 +6,19 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.securebackup.impl.enter import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performImeAction import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.securebackup.impl.setup.views.aFormattedRecoveryKey import io.element.android.libraries.architecture.AsyncAction @@ -26,58 +29,54 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class SecureBackupEnterRecoveryKeyViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `back key pressed - calls onBackClick`() { + fun `back key pressed - calls onBackClick`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setSecureBackupEnterRecoveryKeyView( + setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(), onBackClick = callback, ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `back button clicked - calls onBackClick`() { + fun `back button clicked - calls onBackClick`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setSecureBackupEnterRecoveryKeyView( + setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(), onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test @Config(qualifiers = "h1024dp") - fun `tapping on Continue when key is valid - calls expected action`() { + fun `tapping on Continue when key is valid - calls expected action`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setSecureBackupEnterRecoveryKeyView( + setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.Submit) } @Test - fun `entering a char emits the expected event`() { + fun `entering a char emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val keyValue = aFormattedRecoveryKey() - rule.setSecureBackupEnterRecoveryKeyView( + setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder), ) - rule.onNodeWithText(keyValue).performTextInput("X") + onNodeWithText(keyValue).performTextInput("X") recorder.assertSingle( SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange("X$keyValue") ) @@ -85,43 +84,43 @@ class SecureBackupEnterRecoveryKeyViewTest { @Test @Config(qualifiers = "h1024dp") - fun `toggling the visibility of the textfield changes it`() { + fun `toggling the visibility of the textfield changes it`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val keyValue = aFormattedRecoveryKey() - rule.setSecureBackupEnterRecoveryKeyView(aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder)) + setSecureBackupEnterRecoveryKeyView(aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder)) // Initially, the text field should be visible - rule.onNodeWithText(keyValue).assertExists() + onNodeWithText(keyValue).assertExists() - rule.onNodeWithContentDescription(rule.activity.getString(CommonStrings.a11y_hide_password)).performClick() + onNodeWithContentDescription(activity!!.getString(CommonStrings.a11y_hide_password)).performClick() - rule.waitForIdle() + waitForIdle() recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.ChangeRecoveryKeyFieldContentsVisibility(false)) } @Test - fun `validating from keyboard emits the expected event`() { + fun `validating from keyboard emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val keyValue = aFormattedRecoveryKey() - rule.setSecureBackupEnterRecoveryKeyView( + setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder), ) - rule.onNodeWithText(keyValue).performImeAction() + onNodeWithText(keyValue).performImeAction() recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.Submit) } @Test - fun `when submit action succeeds - calls onDone`() { + fun `when submit action succeeds - calls onDone`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setSecureBackupEnterRecoveryKeyView( + setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(submitAction = AsyncAction.Success(Unit)), onDone = callback, ) } } - private fun AndroidComposeTestRule.setSecureBackupEnterRecoveryKeyView( + private fun AndroidComposeUiTest.setSecureBackupEnterRecoveryKeyView( state: SecureBackupEnterRecoveryKeyState, onDone: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt index 6cfd061103..ce5972f66b 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt @@ -6,13 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.securebackup.impl.reset.password import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.ui.strings.CommonStrings @@ -22,64 +25,59 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ResetIdentityPasswordViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `pressing the back HW button invokes the expected callback`() { + fun `pressing the back HW button invokes the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { - rule.setResetPasswordView( + setResetPasswordView( ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}), onBack = it, ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `clicking on the back navigation button invokes the expected callback`() { + fun `clicking on the back navigation button invokes the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { - rule.setResetPasswordView( + setResetPasswordView( ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}), onBack = it, ) - rule.pressBack() + pressBack() } } @Test - fun `clicking 'Reset identity' confirms the reset`() { + fun `clicking 'Reset identity' confirms the reset`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setResetPasswordView( + setResetPasswordView( ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = eventsRecorder), ) - rule.onNodeWithText("Password").performTextInput("A password") + onNodeWithText("Password").performTextInput("A password") - rule.clickOn(CommonStrings.action_reset_identity) + clickOn(CommonStrings.action_reset_identity) eventsRecorder.assertSingle(ResetIdentityPasswordEvent.Reset("A password")) } @Test - fun `modifying the password dismisses the error state`() { + fun `modifying the password dismisses the error state`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setResetPasswordView( + setResetPasswordView( ResetIdentityPasswordState(resetAction = AsyncAction.Failure(IllegalStateException("A failure")), eventSink = eventsRecorder), ) - rule.onNodeWithText("Password").performTextInput("A password") + onNodeWithText("Password").performTextInput("A password") eventsRecorder.assertSingle(ResetIdentityPasswordEvent.DismissError) } } -private fun AndroidComposeTestRule.setResetPasswordView( +private fun AndroidComposeUiTest.setResetPasswordView( state: ResetIdentityPasswordState, onBack: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt index a913a9af27..0126d0d879 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.securebackup.impl.reset.root import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.securebackup.impl.R import io.element.android.libraries.ui.strings.CommonStrings @@ -20,76 +23,71 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class ResetIdentityRootViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `pressing the back HW button invokes the expected callback`() { + fun `pressing the back HW button invokes the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { - rule.setResetRootView( + setResetRootView( ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}), onBack = it, ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `clicking on the back navigation button invokes the expected callback`() { + fun `clicking on the back navigation button invokes the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { - rule.setResetRootView( + setResetRootView( ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}), onBack = it, ) - rule.pressBack() + pressBack() } } @Test @Config(qualifiers = "h720dp") - fun `clicking Continue displays the confirmation dialog`() { + fun `clicking Continue displays the confirmation dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setResetRootView( + setResetRootView( ResetIdentityRootState(displayConfirmationDialog = false, eventSink = eventsRecorder), ) - rule.clickOn(R.string.screen_encryption_reset_action_continue_reset) + clickOn(R.string.screen_encryption_reset_action_continue_reset) eventsRecorder.assertSingle(ResetIdentityRootEvent.Continue) } @Test - fun `clicking 'Yes, reset now' confirms the reset`() { + fun `clicking 'Yes, reset now' confirms the reset`() = runAndroidComposeUiTest { ensureCalledOnce { - rule.setResetRootView( + setResetRootView( ResetIdentityRootState(displayConfirmationDialog = true, eventSink = {}), onContinue = it, ) - rule.clickOn(R.string.screen_reset_encryption_confirmation_alert_action) + clickOn(R.string.screen_reset_encryption_confirmation_alert_action) } } @Test - fun `clicking Cancel dismisses the dialog`() { + fun `clicking Cancel dismisses the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setResetRootView( + setResetRootView( ResetIdentityRootState(displayConfirmationDialog = true, eventSink = eventsRecorder), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ResetIdentityRootEvent.DismissDialog) } } -private fun AndroidComposeTestRule.setResetRootView( +private fun AndroidComposeUiTest.setResetRootView( state: ResetIdentityRootState, onBack: () -> Unit = EnsureNeverCalled(), onContinue: () -> Unit = EnsureNeverCalled(), diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressViewTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressViewTest.kt index 17d6f3a88d..2c0a6c9acb 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressViewTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressViewTest.kt @@ -6,13 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.securityandprivacy.impl.editroomaddress import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity @@ -23,86 +26,82 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class EditRoomAddressViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `click on back invokes expected callback`() { + fun `click on back invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setEditRoomAddressView(onBackClick = callback) - rule.pressBack() + setEditRoomAddressView(onBackClick = callback) + pressBack() } } @Test - fun `click on disabled save doesn't emit event`() { + fun `click on disabled save doesn't emit event`() = runAndroidComposeUiTest { val recorder = EventsRecorder(expectEvents = false) val state = anEditRoomAddressState(eventSink = recorder) - rule.setEditRoomAddressView(state) - rule.clickOn(CommonStrings.action_save) + setEditRoomAddressView(state) + clickOn(CommonStrings.action_save) recorder.assertEmpty() } @Test - fun `click on enabled save emits the expected event`() { + fun `click on enabled save emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = anEditRoomAddressState( roomAddress = "room", roomAddressValidity = RoomAddressValidity.Valid, eventSink = recorder ) - rule.setEditRoomAddressView(state) - rule.clickOn(CommonStrings.action_save) + setEditRoomAddressView(state) + clickOn(CommonStrings.action_save) recorder.assertSingle(EditRoomAddressEvents.Save) } @Test - fun `text changes on text field emits the expected event`() { + fun `text changes on text field emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = anEditRoomAddressState( roomAddress = "", eventSink = recorder ) - rule.setEditRoomAddressView(state) + setEditRoomAddressView(state) - rule.onNodeWithTag(TestTags.roomAddressField.value).performTextInput("alias") + onNodeWithTag(TestTags.roomAddressField.value).performTextInput("alias") recorder.assertSingle(EditRoomAddressEvents.RoomAddressChanged("alias")) } @Test - fun `click on dismiss error emits the expected event`() { + fun `click on dismiss error emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = anEditRoomAddressState( roomAddress = "", saveAction = AsyncAction.Failure(IllegalStateException()), eventSink = recorder ) - rule.setEditRoomAddressView(state) - rule.clickOn(CommonStrings.action_cancel) + setEditRoomAddressView(state) + clickOn(CommonStrings.action_cancel) recorder.assertSingle(EditRoomAddressEvents.DismissError) } @Test - fun `click on retry error emits the expected event`() { + fun `click on retry error emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = anEditRoomAddressState( roomAddress = "", saveAction = AsyncAction.Failure(IllegalStateException()), eventSink = recorder ) - rule.setEditRoomAddressView(state) - rule.clickOn(CommonStrings.action_retry) + setEditRoomAddressView(state) + clickOn(CommonStrings.action_retry) recorder.assertSingle(EditRoomAddressEvents.Save) } } -private fun AndroidComposeTestRule.setEditRoomAddressView( +private fun AndroidComposeUiTest.setEditRoomAddressView( state: EditRoomAddressState = anEditRoomAddressState( eventSink = EventsRecorder(expectEvents = false), ), diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt index c732df6df0..de6da41823 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt @@ -6,13 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.securityandprivacy.impl.manageauthorizedspaces import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.spaces.SpaceRoom @@ -24,26 +27,22 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBack import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ManageAuthorizedSpacesViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking back emits Cancel event`() { + fun `clicking back emits Cancel event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aManageAuthorizedSpacesState(eventSink = recorder) - rule.setManageAuthorizedSpacesView(state) - rule.pressBack() + setManageAuthorizedSpacesView(state) + pressBack() recorder.assertSingle(ManageAuthorizedSpacesEvent.Cancel) } @Test - fun `clicking space checkbox emits ToggleSpace event`() { + fun `clicking space checkbox emits ToggleSpace event`() = runAndroidComposeUiTest { val roomId = A_ROOM_ID val space = aSpaceRoom(roomId = roomId, displayName = "Test Space") val recorder = EventsRecorder() @@ -51,37 +50,37 @@ class ManageAuthorizedSpacesViewTest { selectableSpaces = listOf(space), eventSink = recorder ) - rule.setManageAuthorizedSpacesView(state) - rule.onNodeWithText("Test Space").performClick() + setManageAuthorizedSpacesView(state) + onNodeWithText("Test Space").performClick() recorder.assertSingle(ManageAuthorizedSpacesEvent.ToggleSpace(roomId)) } @Test - fun `clicking done button emits Done event`() { + fun `clicking done button emits Done event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aManageAuthorizedSpacesState( selectedIds = listOf(A_ROOM_ID), eventSink = recorder ) - rule.setManageAuthorizedSpacesView(state) - rule.clickOn(CommonStrings.action_done) + setManageAuthorizedSpacesView(state) + clickOn(CommonStrings.action_done) recorder.assertSingle(ManageAuthorizedSpacesEvent.Done) } @Test - fun `done button is disabled when no spaces selected`() { + fun `done button is disabled when no spaces selected`() = runAndroidComposeUiTest { val recorder = EventsRecorder(expectEvents = false) val state = aManageAuthorizedSpacesState( selectedIds = emptyList(), eventSink = recorder ) - rule.setManageAuthorizedSpacesView(state) - rule.clickOn(CommonStrings.action_done) + setManageAuthorizedSpacesView(state) + clickOn(CommonStrings.action_done) recorder.assertEmpty() } } -private fun AndroidComposeTestRule.setManageAuthorizedSpacesView( +private fun AndroidComposeUiTest.setManageAuthorizedSpacesView( state: ManageAuthorizedSpacesState = aManageAuthorizedSpacesState( eventSink = EventsRecorder(expectEvents = false) ), diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyViewTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyViewTest.kt index a1f46b2938..c46accbc91 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyViewTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyViewTest.kt @@ -5,13 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.securityandprivacy.impl.root import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.securityandprivacy.impl.R import io.element.android.libraries.architecture.AsyncAction @@ -23,73 +26,69 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBack import kotlinx.collections.immutable.persistentListOf -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class SecurityAndPrivacyViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `click on back invokes emits the expected event`() { + fun `click on back invokes emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, ) - rule.setSecurityAndPrivacyView(state) - rule.pressBack() + setSecurityAndPrivacyView(state) + pressBack() recorder.assertSingle(SecurityAndPrivacyEvent.Exit) } @Test - fun `discard cancellation emits the expected event`() { + fun `discard cancellation emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = recorder, ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(CommonStrings.action_discard) + setSecurityAndPrivacyView(state) + clickOn(CommonStrings.action_discard) recorder.assertSingle(SecurityAndPrivacyEvent.Exit) } @Test - fun `save cancellation confirmation emits the expected event`() { + fun `save cancellation confirmation emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = recorder, ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(CommonStrings.action_save, inDialog = true) + setSecurityAndPrivacyView(state) + clickOn(CommonStrings.action_save, inDialog = true) recorder.assertSingle(SecurityAndPrivacyEvent.Save) } @Test - fun `click on room access item emits the expected event`() { + fun `click on room access item emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(R.string.screen_security_and_privacy_room_access_invite_only_option_title) + setSecurityAndPrivacyView(state) + clickOn(R.string.screen_security_and_privacy_room_access_invite_only_option_title) recorder.assertSingle(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.InviteOnly)) } @Test - fun `click on disabled save doesn't emit event`() { + fun `click on disabled save doesn't emit event`() = runAndroidComposeUiTest { val recorder = EventsRecorder(expectEvents = false) val state = aSecurityAndPrivacyState(eventSink = recorder) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(CommonStrings.action_save) + setSecurityAndPrivacyView(state) + clickOn(CommonStrings.action_save) recorder.assertEmpty() } @Test - fun `click on enabled save emits the expected event`() { + fun `click on enabled save emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, @@ -97,14 +96,14 @@ class SecurityAndPrivacyViewTest { roomAccess = SecurityAndPrivacyRoomAccess.Anyone, ) ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(CommonStrings.action_save) + setSecurityAndPrivacyView(state) + clickOn(CommonStrings.action_save) recorder.assertSingle(SecurityAndPrivacyEvent.Save) } @Test @Config(qualifiers = "h640dp") - fun `click on room address item emits the expected event`() { + fun `click on room address item emits the expected event`() = runAndroidComposeUiTest { val address = "@alias:matrix.org" val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( @@ -114,14 +113,14 @@ class SecurityAndPrivacyViewTest { roomAccess = SecurityAndPrivacyRoomAccess.Anyone, ), ) - rule.setSecurityAndPrivacyView(state) - rule.onNodeWithText(address).performClick() + setSecurityAndPrivacyView(state) + onNodeWithText(address).performClick() recorder.assertSingle(SecurityAndPrivacyEvent.EditRoomAddress) } @Test @Config(qualifiers = "h1024dp") - fun `click on room visibility item emits the expected event`() { + fun `click on room visibility item emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, @@ -130,14 +129,14 @@ class SecurityAndPrivacyViewTest { isVisibleInRoomDirectory = AsyncData.Success(false), ), ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(R.string.screen_security_and_privacy_room_directory_visibility_toggle_title) + setSecurityAndPrivacyView(state) + clickOn(R.string.screen_security_and_privacy_room_directory_visibility_toggle_title) recorder.assertSingle(SecurityAndPrivacyEvent.ToggleRoomVisibility) } @Test @Config(qualifiers = "h1024dp") - fun `click on history visibility item emits the expected event`() { + fun `click on history visibility item emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, @@ -145,65 +144,65 @@ class SecurityAndPrivacyViewTest { historyVisibility = SecurityAndPrivacyHistoryVisibility.Invited, ), ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(R.string.screen_security_and_privacy_room_history_since_invite_option_title) + setSecurityAndPrivacyView(state) + clickOn(R.string.screen_security_and_privacy_room_history_since_invite_option_title) recorder.assertSingle(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Invited)) } @Test @Config(qualifiers = "h1024dp") - fun `click on encryption item emits the expected event`() { + fun `click on encryption item emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, savedSettings = aSecurityAndPrivacySettings(isEncrypted = false), ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(R.string.screen_security_and_privacy_encryption_toggle_title) + setSecurityAndPrivacyView(state) + clickOn(R.string.screen_security_and_privacy_encryption_toggle_title) recorder.assertSingle(SecurityAndPrivacyEvent.ToggleEncryptionState) } @Test - fun `click on encryption confirm emits the expected event`() { + fun `click on encryption confirm emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, showEncryptionConfirmation = true, ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(R.string.screen_security_and_privacy_enable_encryption_alert_confirm_button_title) + setSecurityAndPrivacyView(state) + clickOn(R.string.screen_security_and_privacy_enable_encryption_alert_confirm_button_title) recorder.assertSingle(SecurityAndPrivacyEvent.ConfirmEnableEncryption) } @Test @Config(qualifiers = "h1024dp") - fun `click on space member access emits the expected event`() { + fun `click on space member access emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, spaceSelectionMode = SpaceSelectionMode.Single(A_ROOM_ID, null), ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(R.string.screen_security_and_privacy_room_access_space_members_option_title) + setSecurityAndPrivacyView(state) + clickOn(R.string.screen_security_and_privacy_room_access_space_members_option_title) recorder.assertSingle(SecurityAndPrivacyEvent.SelectSpaceMemberAccess) } @Test @Config(qualifiers = "h1024dp") - fun `click on ask to join with space members emits the expected event`() { + fun `click on ask to join with space members emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, spaceSelectionMode = SpaceSelectionMode.Single(A_ROOM_ID, null), ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(R.string.screen_security_and_privacy_ask_to_join_option_title) + setSecurityAndPrivacyView(state) + clickOn(R.string.screen_security_and_privacy_ask_to_join_option_title) recorder.assertSingle(SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess) } @Test @Config(qualifiers = "h1024dp") - fun `manage spaces footer is shown when space member access is selected`() { + fun `manage spaces footer is shown when space member access is selected`() = runAndroidComposeUiTest { val recorder = EventsRecorder(expectEvents = false) val state = aSecurityAndPrivacyState( eventSink = recorder, @@ -212,15 +211,16 @@ class SecurityAndPrivacyViewTest { roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember(persistentListOf(A_ROOM_ID)), ), ) - rule.setSecurityAndPrivacyView(state) + setSecurityAndPrivacyView(state) // The footer text uses AnnotatedString with a link. Verify the footer text is displayed. - val actionFooterText = rule.activity.getString(R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action) - val footerText = rule.activity.getString(R.string.screen_security_and_privacy_room_access_footer, actionFooterText) - rule.onNodeWithText(footerText).assertExists() + val resources = activity!!.resources + val actionFooterText = resources.getString(R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action) + val footerText = resources.getString(R.string.screen_security_and_privacy_room_access_footer, actionFooterText) + onNodeWithText(footerText).assertExists() } } -private fun AndroidComposeTestRule.setSecurityAndPrivacyView( +private fun AndroidComposeUiTest.setSecurityAndPrivacyView( state: SecurityAndPrivacyState = aSecurityAndPrivacyState( eventSink = EventsRecorder(expectEvents = false), ), diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt index d75fecd05a..6fc10f1e82 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt @@ -5,13 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.space.impl.addroom import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.theme.components.SearchBarResultState @@ -22,77 +25,73 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import kotlinx.collections.immutable.toImmutableList -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class AddRoomToSpaceViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking back when search inactive emits Dismiss and invokes onBackClick`() { + fun `clicking back when search inactive emits Dismiss and invokes onBackClick`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() ensureCalledOnce { - rule.setAddRoomToSpaceView( + setAddRoomToSpaceView( anAddRoomToSpaceState( isSearchActive = false, eventSink = eventsRecorder, ), onBackClick = it, ) - rule.pressBack() + pressBack() } eventsRecorder.assertSingle(AddRoomToSpaceEvent.Dismiss) } @Test - fun `clicking back when search active emits CloseSearch event`() { + fun `clicking back when search active emits CloseSearch event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAddRoomToSpaceView( + setAddRoomToSpaceView( anAddRoomToSpaceState( isSearchActive = true, eventSink = eventsRecorder, ), ) - rule.pressBack() + pressBack() eventsRecorder.assertSingle(AddRoomToSpaceEvent.OnSearchActiveChanged(false)) } @Test - fun `clicking save emits Save event`() { + fun `clicking save emits Save event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAddRoomToSpaceView( + setAddRoomToSpaceView( anAddRoomToSpaceState( selectedRooms = aSelectRoomInfoList().take(1).toImmutableList(), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) eventsRecorder.assertSingle(AddRoomToSpaceEvent.Save) } @Config(qualifiers = "h1024dp") @Test - fun `clicking room in suggestions emits ToggleRoom event`() { + fun `clicking room in suggestions emits ToggleRoom event`() = runAndroidComposeUiTest { val suggestions = aSelectRoomInfoList() val eventsRecorder = EventsRecorder() - rule.setAddRoomToSpaceView( + setAddRoomToSpaceView( anAddRoomToSpaceState( suggestions = suggestions, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(suggestions.first().name!!).performClick() + onNodeWithText(suggestions.first().name!!).performClick() eventsRecorder.assertSingle(AddRoomToSpaceEvent.ToggleRoom(suggestions.first())) } @Test - fun `onRoomsAdded called when saveAction is Success`() { + fun `onRoomsAdded called when saveAction is Success`() = runAndroidComposeUiTest { ensureCalledOnce { - rule.setAddRoomToSpaceView( + setAddRoomToSpaceView( anAddRoomToSpaceState( saveAction = AsyncAction.Success(Unit), ), @@ -103,10 +102,10 @@ class AddRoomToSpaceViewTest { @Config(qualifiers = "h1024dp") @Test - fun `displaying search results sends UpdateSearchVisibleRange event`() { + fun `displaying search results sends UpdateSearchVisibleRange event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val rooms = aSelectRoomInfoList() - rule.setAddRoomToSpaceView( + setAddRoomToSpaceView( anAddRoomToSpaceState( isSearchActive = true, searchResults = SearchBarResultState.Results(rooms), @@ -117,7 +116,7 @@ class AddRoomToSpaceViewTest { } } -private fun AndroidComposeTestRule.setAddRoomToSpaceView( +private fun AndroidComposeUiTest.setAddRoomToSpaceView( state: AddRoomToSpaceState, onBackClick: () -> Unit = EnsureNeverCalled(), onRoomsAdded: () -> Unit = EnsureNeverCalled(), diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt index 87343b6e34..6632c7f4f8 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt @@ -6,14 +6,17 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.space.impl.root import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.room.CurrentUserMembership @@ -33,37 +36,33 @@ import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class SpaceViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setSpaceView( + setSpaceView( aSpaceState( hasMoreToLoad = false, eventSink = eventsRecorder, ), onBackClick = it, ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on a room name invokes the expected callback`() { + fun `clicking on a room name invokes the expected callback`() = runAndroidComposeUiTest { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME) val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(aSpaceRoom) { - rule.setSpaceView( + setSpaceView( aSpaceState( children = listOf(aSpaceRoom), hasMoreToLoad = false, @@ -71,91 +70,91 @@ class SpaceViewTest { ), onRoomClick = it, ) - rule.onNodeWithText(A_ROOM_NAME).performClick() + onNodeWithText(A_ROOM_NAME).performClick() } } @Test - fun `clicking on Join room emits the expected Event`() { + fun `clicking on Join room emits the expected Event`() = runAndroidComposeUiTest { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = null) val eventsRecorder = EventsRecorder() - rule.setSpaceView( + setSpaceView( aSpaceState( children = listOf(aSpaceRoom), hasMoreToLoad = false, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_join) + clickOn(CommonStrings.action_join) eventsRecorder.assertSingle(SpaceEvents.Join(aSpaceRoom)) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on accept invite emits the expected Event`() { + fun `clicking on accept invite emits the expected Event`() = runAndroidComposeUiTest { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED) val eventsRecorder = EventsRecorder() - rule.setSpaceView( + setSpaceView( aSpaceState( hasMoreToLoad = false, children = listOf(aSpaceRoom), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_accept) + clickOn(CommonStrings.action_accept) eventsRecorder.assertSingle(SpaceEvents.AcceptInvite(aSpaceRoom)) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on decline invite emits the expected Event`() { + fun `clicking on decline invite emits the expected Event`() = runAndroidComposeUiTest { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED) val eventsRecorder = EventsRecorder() - rule.setSpaceView( + setSpaceView( aSpaceState( hasMoreToLoad = false, children = listOf(aSpaceRoom), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_decline) + clickOn(CommonStrings.action_decline) eventsRecorder.assertSingle(SpaceEvents.DeclineInvite(aSpaceRoom)) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on topic emits the expected Event`() { + fun `clicking on topic emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setSpaceView( + setSpaceView( aSpaceState( spaceInfo = aRoomInfo(topic = A_ROOM_TOPIC), hasMoreToLoad = false, eventSink = eventsRecorder, ) ) - rule.onNodeWithText(A_ROOM_TOPIC).performClick() + onNodeWithText(A_ROOM_TOPIC).performClick() eventsRecorder.assertSingle(SpaceEvents.ShowTopicViewer(A_ROOM_TOPIC)) } @Test - fun `clicking back in manage mode emits ExitManageMode event`() { + fun `clicking back in manage mode emits ExitManageMode event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setSpaceView( + setSpaceView( aSpaceState( hasMoreToLoad = false, isManageMode = true, eventSink = eventsRecorder, ) ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(SpaceEvents.ExitManageMode) } @Test - fun `clicking on room in manage mode emits ToggleRoomSelection event`() { + fun `clicking on room in manage mode emits ToggleRoomSelection event`() = runAndroidComposeUiTest { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME) val eventsRecorder = EventsRecorder() - rule.setSpaceView( + setSpaceView( aSpaceState( children = listOf(aSpaceRoom), hasMoreToLoad = false, @@ -163,14 +162,14 @@ class SpaceViewTest { eventSink = eventsRecorder, ) ) - rule.onNodeWithText(A_ROOM_NAME).performClick() + onNodeWithText(A_ROOM_NAME).performClick() eventsRecorder.assertSingle(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) } @Test - fun `clicking remove button emits RemoveSelectedRooms event`() { + fun `clicking remove button emits RemoveSelectedRooms event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setSpaceView( + setSpaceView( aSpaceState( children = listOf(aSpaceRoom(roomId = A_ROOM_ID)), hasMoreToLoad = false, @@ -179,15 +178,15 @@ class SpaceViewTest { eventSink = eventsRecorder, ) ) - rule.clickOn(CommonStrings.action_remove) + clickOn(CommonStrings.action_remove) eventsRecorder.assertSingle(SpaceEvents.RemoveSelectedRooms) } @Config(qualifiers = "h1024dp") @Test - fun `clicking confirm in removal dialog emits ConfirmRoomRemoval event`() { + fun `clicking confirm in removal dialog emits ConfirmRoomRemoval event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setSpaceView( + setSpaceView( aSpaceState( children = listOf(aSpaceRoom(roomId = A_ROOM_ID)), hasMoreToLoad = false, @@ -198,14 +197,14 @@ class SpaceViewTest { ) ) // Click on the Remove button in the confirmation dialog - rule.clickOn(CommonStrings.action_remove, inDialog = true) + clickOn(CommonStrings.action_remove, inDialog = true) eventsRecorder.assertSingle(SpaceEvents.ConfirmRoomRemoval) } @Test - fun `clicking create room button calls the expected callback`() { + fun `clicking create room button calls the expected callback`() = runAndroidComposeUiTest { val onCreateRoomClick = lambdaRecorder { } - rule.setSpaceView( + setSpaceView( aSpaceState( children = emptyList(), hasMoreToLoad = false, @@ -214,14 +213,14 @@ class SpaceViewTest { ), onCreateRoomClick = onCreateRoomClick, ) - rule.clickOn(CommonStrings.action_create_room) + clickOn(CommonStrings.action_create_room) onCreateRoomClick.assertions().isCalledOnce() } @Test - fun `clicking add existing room button calls the expected callback`() { + fun `clicking add existing room button calls the expected callback`() = runAndroidComposeUiTest { val onAddRoomClick = lambdaRecorder { } - rule.setSpaceView( + setSpaceView( aSpaceState( children = emptyList(), hasMoreToLoad = false, @@ -230,12 +229,12 @@ class SpaceViewTest { ), onAddRoomClick = onAddRoomClick, ) - rule.clickOn(CommonStrings.action_add_existing_rooms) + clickOn(CommonStrings.action_add_existing_rooms) onAddRoomClick.assertions().isCalledOnce() } } -private fun AndroidComposeTestRule.setSpaceView( +private fun AndroidComposeUiTest.setSpaceView( state: SpaceState, onBackClick: () -> Unit = EnsureNeverCalled(), onRoomClick: (SpaceRoom) -> Unit = EnsureNeverCalledWithParam(), diff --git a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt index 92162ca82c..dd992a9d2f 100644 --- a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt +++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt @@ -6,56 +6,54 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.startchat.impl.joinbyaddress import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.startchat.impl.R import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.setSafeContent -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class JoinBaseRoomByAddressViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `entering text emits the expected event`() { + fun `entering text emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomByAddressView( + setJoinRoomByAddressView( aJoinRoomByAddressState( eventSink = eventsRecorder, ) ) - val text = rule.activity.getString(R.string.screen_start_chat_join_room_by_address_action) - rule.onNodeWithText(text).performTextInput("#address:matrix.org") + val text = activity!!.getString(R.string.screen_start_chat_join_room_by_address_action) + onNodeWithText(text).performTextInput("#address:matrix.org") eventsRecorder.assertSingle(JoinRoomByAddressEvent.UpdateAddress("#address:matrix.org")) } @Test - fun `clicking on continue emits the expected event`() { + fun `clicking on continue emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomByAddressView( + setJoinRoomByAddressView( aJoinRoomByAddressState( eventSink = eventsRecorder, ) ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(JoinRoomByAddressEvent.Continue) } } -private fun AndroidComposeTestRule.setJoinRoomByAddressView( +private fun AndroidComposeUiTest.setJoinRoomByAddressView( state: JoinRoomByAddressState, ) { setSafeContent { diff --git a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatViewTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatViewTest.kt index 9237f3433c..abcb70113b 100644 --- a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatViewTest.kt +++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatViewTest.kt @@ -6,13 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.startchat.impl.root import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.startchat.impl.R import io.element.android.features.startchat.impl.userlist.aRecentDirectRoomList @@ -27,70 +30,65 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class StartChatViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setStartChatView( + setStartChatView( aCreateRoomRootState( eventSink = eventsRecorder, ), onCloseClick = it ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on New room invokes the expected callback`() { + fun `clicking on New room invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setStartChatView( + setStartChatView( aCreateRoomRootState( eventSink = eventsRecorder, ), onNewRoomClick = it ) - rule.clickOn(R.string.screen_create_room_action_create_room) + clickOn(R.string.screen_create_room_action_create_room) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on Invite people invokes the expected callback`() { + fun `clicking on Invite people invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setStartChatView( + setStartChatView( aCreateRoomRootState( applicationName = "test", eventSink = eventsRecorder, ), onInviteFriendsClick = it ) - val text = rule.activity.getString(CommonStrings.action_invite_friends_to_app, "test") - rule.onNodeWithText(text).performClick() + val text = activity!!.getString(CommonStrings.action_invite_friends_to_app, "test") + onNodeWithText(text).performClick() } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on a user suggestion invokes the expected callback`() { + fun `clicking on a user suggestion invokes the expected callback`() = runAndroidComposeUiTest { val recentDirectRoomList = aRecentDirectRoomList() val firstRoom = recentDirectRoomList[0] val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(firstRoom.roomId) { - rule.setStartChatView( + setStartChatView( aCreateRoomRootState( userListState = aUserListState( recentDirectRooms = recentDirectRoomList @@ -99,42 +97,42 @@ class StartChatViewTest { ), onOpenDM = it ) - rule.onNodeWithText(firstRoom.matrixUser.getBestName()).performClick() + onNodeWithText(firstRoom.matrixUser.getBestName()).performClick() } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on Join room by address invokes the expected callback`() { + fun `clicking on Join room by address invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setStartChatView( + setStartChatView( aCreateRoomRootState( eventSink = eventsRecorder, ), onJoinRoomByAddressClick = it ) - rule.clickOn(R.string.screen_start_chat_join_room_by_address_action) + clickOn(R.string.screen_start_chat_join_room_by_address_action) } } @Test - fun `clicking on room directory invokes the expected callback`() { + fun `clicking on room directory invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setStartChatView( + setStartChatView( aCreateRoomRootState( eventSink = eventsRecorder, isRoomDirectorySearchEnabled = true ), onRoomDirectorySearchClick = it ) - rule.clickOn(R.string.screen_room_directory_search_title) + clickOn(R.string.screen_room_directory_search_title) } } } -private fun AndroidComposeTestRule.setStartChatView( +private fun AndroidComposeUiTest.setStartChatView( state: StartChatState, onCloseClick: () -> Unit = EnsureNeverCalled(), onNewRoomClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt index 83b10e2a53..b1d81f374c 100644 --- a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt +++ b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt @@ -6,13 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.userprofile import androidx.activity.ComponentActivity +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.userprofile.api.UserProfileEvents import io.element.android.features.userprofile.api.UserProfileState @@ -39,193 +42,188 @@ import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithTwoParams import io.element.android.tests.testutils.pressBack -import kotlinx.coroutines.test.runTest -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class UserProfileViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `on back button click - the expected callback is called`() = runTest { + fun `on back button click - the expected callback is called`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setUserProfileView( + setUserProfileView( goBack = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `on avatar clicked - the expected callback is called`() = runTest { + fun `on avatar clicked - the expected callback is called`() = runAndroidComposeUiTest { ensureCalledOnceWithTwoParams(A_USER_NAME, AN_AVATAR_URL) { callback -> - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState(userName = A_USER_NAME, avatarUrl = AN_AVATAR_URL), openAvatarPreview = callback, ) - rule.onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick() + onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick() } } @Test - fun `on avatar clicked with no avatar - nothing happens`() = runTest { + fun `on avatar clicked with no avatar - nothing happens`() = runAndroidComposeUiTest { val callback = EnsureNeverCalledWithTwoParams() - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState(userName = A_USER_NAME, avatarUrl = null), openAvatarPreview = callback, ) - rule.onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick() + onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick() } @Test - fun `on Share clicked - the expected callback is called`() = runTest { + fun `on Share clicked - the expected callback is called`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setUserProfileView( + setUserProfileView( onShareUser = callback, ) - rule.clickOn(CommonStrings.action_share) + clickOn(CommonStrings.action_share) } } @Test - fun `on Message clicked - the StartDm event is emitted`() = runTest { + fun `on Message clicked - the StartDm event is emitted`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( dmRoomId = A_ROOM_ID, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_message) + clickOn(CommonStrings.action_message) eventsRecorder.assertSingle(UserProfileEvents.StartDM) } @Test - fun `on Call clicked - the expected callback is called`() = runTest { + fun `on Call clicked - the expected callback is called`() = runAndroidComposeUiTest { ensureCalledOnceWithTwoParams(A_ROOM_ID, CallIntent.AUDIO) { callback -> - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( dmRoomId = A_ROOM_ID, canCall = true, ), onStartCall = callback, ) - rule.clickOn(CommonStrings.action_call) + clickOn(CommonStrings.action_call) } } @Test - fun `on Video Call clicked - the expected callback is called`() = runTest { + fun `on Video Call clicked - the expected callback is called`() = runAndroidComposeUiTest { ensureCalledOnceWithTwoParams(A_ROOM_ID, CallIntent.VIDEO) { callback -> - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( dmRoomId = A_ROOM_ID, canCall = true, ), onStartCall = callback, ) - rule.clickOn(CommonStrings.common_video) + clickOn(CommonStrings.common_video) } } @Config(qualifiers = "h1024dp") @Test - fun `on Block user clicked - a BlockUser event is emitted with needsConfirmation`() = runTest { + fun `on Block user clicked - a BlockUser event is emitted with needsConfirmation`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_dm_details_block_user) + clickOn(R.string.screen_dm_details_block_user) eventsRecorder.assertSingle(UserProfileEvents.BlockUser(needsConfirmation = true)) } @Test - fun `on confirming block user - a BlockUser event is emitted without needsConfirmation`() = runTest { + fun `on confirming block user - a BlockUser event is emitted without needsConfirmation`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_dm_details_block_alert_action) + clickOn(R.string.screen_dm_details_block_alert_action) eventsRecorder.assertSingle(UserProfileEvents.BlockUser(needsConfirmation = false)) } @Test - fun `on canceling blocking a user - a ClearConfirmationDialog event is emitted`() = runTest { + fun `on canceling blocking a user - a ClearConfirmationDialog event is emitted`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) } @Config(qualifiers = "h1024dp") @Test - fun `on Unblock user clicked - an UnblockUser event is emitted with needsConfirmation`() = runTest { + fun `on Unblock user clicked - an UnblockUser event is emitted with needsConfirmation`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( isBlocked = AsyncData.Success(true), eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_dm_details_unblock_user) + clickOn(R.string.screen_dm_details_unblock_user) eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(needsConfirmation = true)) } @Test - fun `on confirming Unblock user - an UnblockUser event is emitted without needsConfirmation`() = runTest { + fun `on confirming Unblock user - an UnblockUser event is emitted without needsConfirmation`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( isBlocked = AsyncData.Success(true), displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_dm_details_unblock_alert_action) + clickOn(R.string.screen_dm_details_unblock_alert_action) eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(needsConfirmation = false)) } @Test - fun `on canceling unblocking a user - a ClearConfirmationDialog event is emitted`() = runTest { + fun `on canceling unblocking a user - a ClearConfirmationDialog event is emitted`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( isBlocked = AsyncData.Success(true), displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) } @Test - fun `on verify user clicked - the right callback is called`() = runTest { + fun `on verify user clicked - the right callback is called`() = runAndroidComposeUiTest { ensureCalledOnceWithParam(A_USER_ID) { callback -> - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState(userId = A_USER_ID, verificationState = UserProfileVerificationState.UNVERIFIED), onVerifyClick = callback, ) - rule.clickOn(CommonStrings.common_verify_user) + clickOn(CommonStrings.common_verify_user) } } } -private fun AndroidComposeTestRule.setUserProfileView( +private fun AndroidComposeUiTest.setUserProfileView( state: UserProfileState = aUserProfileState( eventSink = EventsRecorder(expectEvents = false), ), diff --git a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt index 3219658796..3498ad7714 100644 --- a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt +++ b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt @@ -6,10 +6,12 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.userprofile.shared.blockuser -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.userprofile.api.UserProfileEvents import io.element.android.features.userprofile.api.UserProfileState @@ -18,18 +20,15 @@ import io.element.android.features.userprofile.shared.aUserProfileState import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class BlockUserDialogsTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `confirm block user emit expected Event`() { + fun `confirm block user emit expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setContent { + setContent { BlockUserDialogs( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, @@ -37,14 +36,14 @@ class BlockUserDialogsTest { ) ) } - rule.clickOn(R.string.screen_dm_details_block_alert_action) + clickOn(R.string.screen_dm_details_block_alert_action) eventsRecorder.assertSingle(UserProfileEvents.BlockUser(false)) } @Test - fun `cancel block user emit expected Event`() { + fun `cancel block user emit expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setContent { + setContent { BlockUserDialogs( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, @@ -52,14 +51,14 @@ class BlockUserDialogsTest { ) ) } - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) } @Test - fun `confirm unblock user emit expected Event`() { + fun `confirm unblock user emit expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setContent { + setContent { BlockUserDialogs( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, @@ -67,14 +66,14 @@ class BlockUserDialogsTest { ) ) } - rule.clickOn(R.string.screen_dm_details_unblock_alert_action) + clickOn(R.string.screen_dm_details_unblock_alert_action) eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(false)) } @Test - fun `cancel unblock user emit expected Event`() { + fun `cancel unblock user emit expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setContent { + setContent { BlockUserDialogs( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, @@ -82,7 +81,7 @@ class BlockUserDialogsTest { ) ) } - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) } } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt index 4aa63f3ab8..afab77ad76 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.verifysession.impl.incoming import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.verifysession.impl.R import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData @@ -18,59 +21,55 @@ import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class IncomingVerificationViewTest { - @get:Rule val rule = createAndroidComposeRule() - // region step Initial @Test - fun `back key pressed - ignore the verification`() { + fun `back key pressed - ignore the verification`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = aStepInitial(), eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @Test - fun `ignore incoming verification emits the expected event`() { + fun `ignore incoming verification emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = aStepInitial(), eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_ignore) + clickOn(CommonStrings.action_ignore) eventsRecorder.assertSingle(IncomingVerificationViewEvents.IgnoreVerification) } @Test - fun `start incoming verification emits the expected event`() { + fun `start incoming verification emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = aStepInitial(), eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_start_verification) + clickOn(CommonStrings.action_start_verification) eventsRecorder.assertSingle(IncomingVerificationViewEvents.StartVerification) } @Test - fun `back key pressed - when awaiting response cancels the verification`() { + fun `back key pressed - when awaiting response cancels the verification`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = aStepInitial( isWaiting = true, @@ -78,16 +77,16 @@ class IncomingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } // endregion step Initial // region step Verifying @Test - fun `back key pressed - when ready to verify cancels the verification`() { + fun `back key pressed - when ready to verify cancels the verification`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -96,14 +95,14 @@ class IncomingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @Test - fun `back key pressed - when verifying and loading emits the expected event`() { + fun `back key pressed - when verifying and loading emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -112,14 +111,14 @@ class IncomingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @Test - fun `clicking on they do not match emits the expected event`() { + fun `clicking on they do not match emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -128,14 +127,14 @@ class IncomingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_session_verification_they_dont_match) + clickOn(R.string.screen_session_verification_they_dont_match) eventsRecorder.assertSingle(IncomingVerificationViewEvents.DeclineVerification) } @Test - fun `clicking on they match emits the expected event`() { + fun `clicking on they match emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -144,35 +143,35 @@ class IncomingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_session_verification_they_match) + clickOn(R.string.screen_session_verification_they_match) eventsRecorder.assertSingle(IncomingVerificationViewEvents.ConfirmVerification) } // endregion // region step Failure @Test - fun `back key pressed - when failure resets the flow`() { + fun `back key pressed - when failure resets the flow`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Failure, eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @Test - fun `click on done - when failure resets the flow`() { + fun `click on done - when failure resets the flow`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Failure, eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_done) + clickOn(CommonStrings.action_done) eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @@ -180,33 +179,33 @@ class IncomingVerificationViewTest { // region step Completed @Test - fun `back key pressed - on Completed step emits the expected event`() { + fun `back key pressed - on Completed step emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Completed, eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @Test - fun `when flow is completed and the user clicks on the done button, the expected event is emitted`() { + fun `when flow is completed and the user clicks on the done button, the expected event is emitted`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Completed, eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_done) + clickOn(CommonStrings.action_done) eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } // endregion - private fun AndroidComposeTestRule.setIncomingVerificationView( + private fun AndroidComposeUiTest.setIncomingVerificationView( state: IncomingVerificationState, ) { setContent { diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewTest.kt index 71b55fac10..1c96c5c2af 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.verifysession.impl.outgoing import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.verifysession.impl.R import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData @@ -21,58 +24,54 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class OutgoingVerificationViewTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `back key pressed - when canceled resets the flow`() { + fun `back key pressed - when canceled resets the flow`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setOutgoingVerificationView( + setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Canceled, eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Reset) } @Test - fun `back key pressed - when awaiting response cancels the verification`() { + fun `back key pressed - when awaiting response cancels the verification`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setOutgoingVerificationView( + setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.AwaitingOtherDeviceResponse, eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel) } @Test - fun `back key pressed - when ready to verify cancels the verification`() { + fun `back key pressed - when ready to verify cancels the verification`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setOutgoingVerificationView( + setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Ready, eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel) } @Test - fun `back key pressed - when verifying and not loading declines the verification`() { + fun `back key pressed - when verifying and not loading declines the verification`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setOutgoingVerificationView( + setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -81,14 +80,14 @@ class OutgoingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification) } @Test - fun `back key pressed - when verifying and loading does nothing`() { + fun `back key pressed - when verifying and loading does nothing`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setOutgoingVerificationView( + setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -97,42 +96,42 @@ class OutgoingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertEmpty() } @Test - fun `back key pressed - on Completed exits the flow`() { + fun `back key pressed - on Completed exits the flow`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setOutgoingVerificationView( + setOutgoingVerificationView( onBack = callback, state = anOutgoingVerificationState( step = OutgoingVerificationState.Step.Completed, ), ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `when flow is completed and the user clicks on the continue button, the expected callback is invoked`() { + fun `when flow is completed and the user clicks on the continue button, the expected callback is invoked`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setOutgoingVerificationView( + setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Completed, eventSink = eventsRecorder ), onFinished = callback, ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) } } @Test - fun `clicking on they match emits the expected event`() { + fun `clicking on they match emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setOutgoingVerificationView( + setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -141,14 +140,14 @@ class OutgoingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_session_verification_they_match) + clickOn(R.string.screen_session_verification_they_match) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.ConfirmVerification) } @Test - fun `clicking on they do not match emits the expected event`() { + fun `clicking on they do not match emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setOutgoingVerificationView( + setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -157,11 +156,11 @@ class OutgoingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_session_verification_they_dont_match) + clickOn(R.string.screen_session_verification_they_dont_match) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification) } - private fun AndroidComposeTestRule.setOutgoingVerificationView( + private fun AndroidComposeUiTest.setOutgoingVerificationView( state: OutgoingVerificationState, onLearnMoreClick: () -> Unit = EnsureNeverCalled(), onFinished: () -> Unit = EnsureNeverCalled(), diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt index 4cbb35a85e..3f4c3aa6c3 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt @@ -6,12 +6,15 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.libraries.mediaviewer.impl.details import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.strings.CommonStrings @@ -21,43 +24,38 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.setSafeContent -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class MediaDeleteConfirmationBottomSheetTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `clicking on Cancel invokes expected callback`() { + fun `clicking on Cancel invokes expected callback`() = runAndroidComposeUiTest { val state = aMediaBottomSheetStateDeleteConfirmation() ensureCalledOnce { callback -> - rule.setMediaDeleteConfirmationBottomSheet( + setMediaDeleteConfirmationBottomSheet( state = state, onDismiss = callback, ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) } } @Test - fun `clicking on Remove invokes expected callback`() { + fun `clicking on Remove invokes expected callback`() = runAndroidComposeUiTest { val state = aMediaBottomSheetStateDeleteConfirmation() ensureCalledOnceWithParam(state.eventId) { callback -> - rule.setMediaDeleteConfirmationBottomSheet( + setMediaDeleteConfirmationBottomSheet( state = state, onDelete = callback, ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertExists() - rule.clickOn(CommonStrings.action_remove) + onNodeWithText(activity!!.getString(CommonStrings.action_remove)).assertExists() + clickOn(CommonStrings.action_remove) } } } -private fun AndroidComposeTestRule.setMediaDeleteConfirmationBottomSheet( +private fun AndroidComposeUiTest.setMediaDeleteConfirmationBottomSheet( state: MediaBottomSheetState.DeleteConfirmation, onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDismiss: () -> Unit = EnsureNeverCalled(), diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt index 21a06f9568..5b0f105aea 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt @@ -6,12 +6,15 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.libraries.mediaviewer.impl.details import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.strings.CommonStrings @@ -20,97 +23,92 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.setSafeContent -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class MediaDetailsBottomSheetTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test @Config(qualifiers = "h1024dp") - fun `clicking on View in timeline invokes expected callback`() { + fun `clicking on View in timeline invokes expected callback`() = runAndroidComposeUiTest { val state = aMediaBottomSheetStateDetails() ensureCalledOnceWithParam(state.eventId) { callback -> - rule.setMediaDetailsBottomSheet( + setMediaDetailsBottomSheet( state = state, onViewInTimeline = callback, ) - rule.clickOn(CommonStrings.action_view_in_timeline) + clickOn(CommonStrings.action_view_in_timeline) } } @Test @Config(qualifiers = "h1024dp") - fun `clicking on Share invokes expected callback`() { + fun `clicking on Share invokes expected callback`() = runAndroidComposeUiTest { val state = aMediaBottomSheetStateDetails() ensureCalledOnceWithParam(state.eventId) { callback -> - rule.setMediaDetailsBottomSheet( + setMediaDetailsBottomSheet( state = state, onShare = callback, ) - rule.clickOn(CommonStrings.action_share) + clickOn(CommonStrings.action_share) } } @Test @Config(qualifiers = "h1024dp") - fun `clicking on Forward invokes expected callback`() { + fun `clicking on Forward invokes expected callback`() = runAndroidComposeUiTest { val state = aMediaBottomSheetStateDetails() ensureCalledOnceWithParam(state.eventId) { callback -> - rule.setMediaDetailsBottomSheet( + setMediaDetailsBottomSheet( state = state, onForward = callback, ) - rule.clickOn(CommonStrings.action_forward) + clickOn(CommonStrings.action_forward) } } @Test @Config(qualifiers = "h1024dp") - fun `clicking on Download invokes expected callback`() { + fun `clicking on Download invokes expected callback`() = runAndroidComposeUiTest { val state = aMediaBottomSheetStateDetails() ensureCalledOnceWithParam(state.eventId) { callback -> - rule.setMediaDetailsBottomSheet( + setMediaDetailsBottomSheet( state = state, onDownload = callback, ) - rule.clickOn(CommonStrings.action_download) + clickOn(CommonStrings.action_download) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on Delete invokes expected callback`() { + fun `clicking on Delete invokes expected callback`() = runAndroidComposeUiTest { val state = aMediaBottomSheetStateDetails() ensureCalledOnceWithParam(state.eventId) { callback -> - rule.setMediaDetailsBottomSheet( + setMediaDetailsBottomSheet( state = state, onDelete = callback, ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.action_delete)).assertExists() - rule.clickOn(CommonStrings.action_delete) + onNodeWithText(activity!!.getString(CommonStrings.action_delete)).assertExists() + clickOn(CommonStrings.action_delete) } } @Config(qualifiers = "h1024dp") @Test - fun `Remove is not present if canDelete is false`() { + fun `Remove is not present if canDelete is false`() = runAndroidComposeUiTest { val state = aMediaBottomSheetStateDetails( canDelete = false, ) - rule.setMediaDetailsBottomSheet( + setMediaDetailsBottomSheet( state = state, ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.action_remove)).assertDoesNotExist() } } -private fun AndroidComposeTestRule.setMediaDetailsBottomSheet( +private fun AndroidComposeUiTest.setMediaDetailsBottomSheet( state: MediaBottomSheetState.Details, onViewInTimeline: (EventId) -> Unit = EnsureNeverCalledWithParam(), onShare: (EventId) -> Unit = EnsureNeverCalledWithParam(), diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt index 9eded788aa..fdd447c4a6 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt @@ -6,18 +6,21 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.libraries.mediaviewer.impl.viewer import android.net.Uri import androidx.activity.ComponentActivity import androidx.annotation.StringRes +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.mediaviewer.impl.details.aMediaBottomSheetStateDetails @@ -30,30 +33,26 @@ import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.setSafeContent import io.mockk.mockk -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class MediaViewerViewTest { - @get:Rule val rule = createAndroidComposeRule() - private val mockMediaUrl: Uri = mockk("localMediaUri") @Test - fun `clicking on back invokes expected callback`() { + fun `clicking on back invokes expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMediaViewerState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - rule.setMediaViewerView( + setMediaViewerView( state = state, onBackClick = callback, ) - rule.pressBack() + pressBack() } eventsRecorder.assertList( listOf( @@ -103,16 +102,16 @@ class MediaViewerViewTest { data: MediaViewerPageData.MediaViewerData, @StringRes contentDescriptionRes: Int, expectedEvent: MediaViewerEvent, - ) { + ) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setMediaViewerView( + setMediaViewerView( aMediaViewerState( listData = listOf(data), eventSink = eventsRecorder ), ) - val contentDescription = rule.activity.getString(contentDescriptionRes) - rule.onNodeWithContentDescription(contentDescription).performClick() + val contentDescription = activity!!.getString(contentDescriptionRes) + onNodeWithContentDescription(contentDescription).performClick() eventsRecorder.assertList( listOf( MediaViewerEvent.OnNavigateTo(0), @@ -159,16 +158,16 @@ class MediaViewerViewTest { data: MediaViewerPageData.MediaViewerData, @StringRes textRes: Int, expectedEvent: MediaViewerEvent, - ) { + ) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setMediaViewerView( + setMediaViewerView( aMediaViewerState( listData = listOf(data), mediaBottomSheetState = aMediaBottomSheetStateDetails(), eventSink = eventsRecorder ), ) - rule.clickOn(textRes) + clickOn(textRes) eventsRecorder.assertList( listOf( MediaViewerEvent.OnNavigateTo(0), @@ -179,24 +178,25 @@ class MediaViewerViewTest { } @Test - fun `clicking on image hides the overlay`() { + fun `clicking on image hides the overlay`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMediaViewerState( eventSink = eventsRecorder ) - rule.setMediaViewerView( + setMediaViewerView( state = state, ) // Ensure that the action are visible - val contentDescription = rule.activity.getString(CommonStrings.action_share) - rule.onNodeWithContentDescription(contentDescription) + val resources = activity!!.resources + val contentDescription = resources.getString(CommonStrings.action_share) + onNodeWithContentDescription(contentDescription) .assertExists() .assertHasClickAction() - val imageContentDescription = rule.activity.getString(CommonStrings.common_image) - rule.onNodeWithContentDescription(imageContentDescription).performClick() + val imageContentDescription = resources.getString(CommonStrings.common_image) + onNodeWithContentDescription(imageContentDescription).performClick() // Give time for the animation (? since even by removing AnimatedVisibility it still fails) - rule.mainClock.advanceTimeBy(1_000) - rule.onNodeWithContentDescription(contentDescription) + mainClock.advanceTimeBy(1_000) + onNodeWithContentDescription(contentDescription) .assertDoesNotExist() eventsRecorder.assertList( listOf( @@ -207,19 +207,19 @@ class MediaViewerViewTest { } @Test - fun `clicking swipe on the image invokes the expected callback`() { + fun `clicking swipe on the image invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMediaViewerState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - rule.setMediaViewerView( + setMediaViewerView( state = state, onBackClick = callback, ) - val imageContentDescription = rule.activity.getString(CommonStrings.common_image) - rule.onNodeWithContentDescription(imageContentDescription).performTouchInput { swipeDown(startY = centerY) } - rule.mainClock.advanceTimeBy(1_000) + val imageContentDescription = activity!!.getString(CommonStrings.common_image) + onNodeWithContentDescription(imageContentDescription).performTouchInput { swipeDown(startY = centerY) } + mainClock.advanceTimeBy(1_000) } eventsRecorder.assertList( listOf( @@ -230,18 +230,18 @@ class MediaViewerViewTest { } @Test - fun `error case, click on retry emits the expected Event`() { + fun `error case, click on retry emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val data = aMediaViewerPageData( downloadedMedia = AsyncData.Failure(IllegalStateException("error")), ) - rule.setMediaViewerView( + setMediaViewerView( aMediaViewerState( listData = listOf(data), eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_retry) + clickOn(CommonStrings.action_retry) eventsRecorder.assertList( listOf( MediaViewerEvent.OnNavigateTo(0), @@ -252,18 +252,18 @@ class MediaViewerViewTest { } @Test - fun `error case, click on cancel emits the expected Event`() { + fun `error case, click on cancel emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val data = aMediaViewerPageData( downloadedMedia = AsyncData.Failure(IllegalStateException("error")), ) - rule.setMediaViewerView( + setMediaViewerView( aMediaViewerState( listData = listOf(data), eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertList( listOf( MediaViewerEvent.OnNavigateTo(0), @@ -274,7 +274,7 @@ class MediaViewerViewTest { } } -private fun AndroidComposeTestRule.setMediaViewerView( +private fun AndroidComposeUiTest.setMediaViewerView( state: MediaViewerState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/components/markdown/MarkdownTextInputTest.kt b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/components/markdown/MarkdownTextInputTest.kt index 9a65ca0ad5..4840569c0e 100644 --- a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/components/markdown/MarkdownTextInputTest.kt +++ b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/components/markdown/MarkdownTextInputTest.kt @@ -6,12 +6,15 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.libraries.textcomposer.impl.components.markdown import android.widget.EditText import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.core.text.getSpans import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -32,66 +35,54 @@ import io.element.android.libraries.textcomposer.model.SuggestionType import io.element.android.libraries.textcomposer.model.aMarkdownTextEditorState import io.element.android.tests.testutils.EnsureCalledOnceWithParam import io.element.android.tests.testutils.EventsRecorder -import kotlinx.coroutines.test.runTest -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class MarkdownTextInputTest { - @get:Rule val rule = createAndroidComposeRule() - @Test - fun `when user types onTyping is triggered with value 'true'`() = runTest { + fun `when user types onTyping is triggered with value 'true'`() = runAndroidComposeUiTest { val state = aMarkdownTextEditorState(initialFocus = true) val onTyping = EnsureCalledOnceWithParam(expectedParam = true, result = Unit) - rule.setMarkdownTextInput(state = state, onTyping = onTyping) - rule.activityRule.scenario.onActivity { - it.findEditor().setText("Test") - } - rule.awaitIdle() + setMarkdownTextInput(state = state, onTyping = onTyping) + activity!!.findEditor().setText("Test") + awaitIdle() onTyping.assertSuccess() } @Test - fun `when user removes text onTyping is triggered with value 'false'`() = runTest { + fun `when user removes text onTyping is triggered with value 'false'`() = runAndroidComposeUiTest { val state = aMarkdownTextEditorState(initialFocus = true) val onTyping = EventsRecorder() - rule.setMarkdownTextInput(state = state, onTyping = onTyping) - rule.activityRule.scenario.onActivity { - val editText = it.findEditor() - editText.setText("Test") - editText.setText("") - editText.setText(null) - } - rule.awaitIdle() + setMarkdownTextInput(state = state, onTyping = onTyping) + val editText = activity!!.findEditor() + editText.setText("Test") + editText.setText("") + editText.setText(null) + awaitIdle() onTyping.assertList(listOf(true, false, false)) } @Test - fun `when user types something that's not a mention onSuggestionReceived is triggered with 'null'`() = runTest { + fun `when user types something that's not a mention onSuggestionReceived is triggered with 'null'`() = runAndroidComposeUiTest { val state = aMarkdownTextEditorState(initialFocus = true) val onSuggestionReceived = EventsRecorder() - rule.setMarkdownTextInput(state = state, onSuggestionReceived = onSuggestionReceived) - rule.activityRule.scenario.onActivity { - it.findEditor().setText("Test") - } - rule.awaitIdle() + setMarkdownTextInput(state = state, onSuggestionReceived = onSuggestionReceived) + activity!!.findEditor().setText("Test") + awaitIdle() onSuggestionReceived.assertSingle(null) } @Test - fun `when user types something that's a mention onSuggestionReceived is triggered a real value`() = runTest { + fun `when user types something that's a mention onSuggestionReceived is triggered a real value`() = runAndroidComposeUiTest { val state = aMarkdownTextEditorState(initialFocus = true) val onSuggestionReceived = EventsRecorder() - rule.setMarkdownTextInput(state = state, onSuggestionReceived = onSuggestionReceived) - rule.activityRule.scenario.onActivity { - it.findEditor().setText("@") - it.findEditor().setText("#") - it.findEditor().setText("/") - } - rule.awaitIdle() + setMarkdownTextInput(state = state, onSuggestionReceived = onSuggestionReceived) + val editor = activity!!.findEditor() + editor.setText("@") + editor.setText("#") + editor.setText("/") + awaitIdle() onSuggestionReceived.assertList( listOf( // User mention suggestion @@ -105,69 +96,59 @@ class MarkdownTextInputTest { } @Test - fun `when the selection changes in the UI the state is updated`() = runTest { + fun `when the selection changes in the UI the state is updated`() = runAndroidComposeUiTest { val state = aMarkdownTextEditorState(initialText = "Test", initialFocus = true) - rule.setMarkdownTextInput(state = state) - rule.activityRule.scenario.onActivity { - val editor = it.findEditor() - editor.setSelection(2) - } - rule.awaitIdle() + setMarkdownTextInput(state = state) + val editor = activity!!.findEditor() + editor.setSelection(2) + awaitIdle() // Selection is updated assertThat(state.selection).isEqualTo(2..2) } @Test - fun `when the selection state changes in the view is updated`() = runTest { + fun `when the selection state changes in the view is updated`() = runAndroidComposeUiTest { val state = aMarkdownTextEditorState(initialText = "Test", initialFocus = true) - rule.setMarkdownTextInput(state = state) - var editor: EditText? = null - rule.activityRule.scenario.onActivity { - editor = it.findEditor() - state.selection = 2..2 - } - rule.awaitIdle() + setMarkdownTextInput(state = state) + val editor = activity!!.findEditor() + state.selection = 2..2 + awaitIdle() // Selection state is updated - assertThat(editor?.selectionStart).isEqualTo(2) - assertThat(editor?.selectionEnd).isEqualTo(2) + assertThat(editor.selectionStart).isEqualTo(2) + assertThat(editor.selectionEnd).isEqualTo(2) } @Test - fun `when the view focus changes the state is updated`() = runTest { + fun `when the view focus changes the state is updated`() = runAndroidComposeUiTest { val state = aMarkdownTextEditorState(initialText = "Test", initialFocus = false) - rule.setMarkdownTextInput(state = state) - rule.activityRule.scenario.onActivity { - val editor = it.findEditor() - editor.requestFocus() - } + setMarkdownTextInput(state = state) + val editor = activity!!.findEditor() + editor.requestFocus() // Focus state is updated assertThat(state.hasFocus).isTrue() } @Test - fun `inserting a mention replaces the existing text with a span`() = runTest { + fun `inserting a mention replaces the existing text with a span`() = runAndroidComposeUiTest { val permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(A_SESSION_ID) }) val state = aMarkdownTextEditorState(initialText = "@", initialFocus = true) state.currentSuggestion = Suggestion(0, 1, SuggestionType.Mention, "") - rule.setMarkdownTextInput(state = state) - var editor: EditText? = null - rule.activityRule.scenario.onActivity { - editor = it.findEditor() - state.insertSuggestion( - ResolvedSuggestion.Member(roomMember = aRoomMember()), - aMentionSpanProvider(permalinkParser), - ) - } - rule.awaitIdle() + setMarkdownTextInput(state = state) + val editor = activity!!.findEditor() + state.insertSuggestion( + ResolvedSuggestion.Member(roomMember = aRoomMember()), + aMentionSpanProvider(permalinkParser), + ) + awaitIdle() // Text is replaced with a placeholder - assertThat(editor?.editableText.toString()).isEqualTo("@ ") + assertThat(editor.editableText.toString()).isEqualTo("@ ") // The placeholder contains a MentionSpan - val mentionSpans = editor?.editableText?.getSpans(0, 2).orEmpty() + val mentionSpans = editor.editableText?.getSpans(0, 2).orEmpty() assertThat(mentionSpans).isNotEmpty() } - private fun AndroidComposeTestRule.setMarkdownTextInput( + private fun AndroidComposeUiTest.setMarkdownTextInput( state: MarkdownTextEditorState = aMarkdownTextEditorState(), onTyping: (Boolean) -> Unit = {}, onSuggestionReceived: (Suggestion?) -> Unit = {}, diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsViewTest.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsViewTest.kt index 0ba6c22710..0244673ea5 100644 --- a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsViewTest.kt +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsViewTest.kt @@ -6,60 +6,58 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.libraries.troubleshoot.impl import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class TroubleshootNotificationsViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `press menu back invokes the expected callback`() { + fun `press menu back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setTroubleshootNotificationsView( + setTroubleshootNotificationsView( state = aTroubleshootNotificationsState( eventSink = eventsRecorder ), onBackClick = it, ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on run test emits the expected Event`() { + fun `clicking on run test emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTroubleshootNotificationsView( + setTroubleshootNotificationsView( aTroubleshootNotificationsState( eventSink = eventsRecorder ), ) - rule.onNodeWithText("Run tests").performClick() + onNodeWithText("Run tests").performClick() eventsRecorder.assertSingle(TroubleshootNotificationsEvents.StartTests) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on run test again emits the expected Event`() { + fun `clicking on run test again emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTroubleshootNotificationsView( + setTroubleshootNotificationsView( aTroubleshootNotificationsState( tests = listOf( aTroubleshootTestStateFailure( @@ -69,7 +67,7 @@ class TroubleshootNotificationsViewTest { eventSink = eventsRecorder ), ) - rule.onNodeWithText("Run tests again").performClick() + onNodeWithText("Run tests again").performClick() eventsRecorder.assertList( listOf( TroubleshootNotificationsEvents.RetryFailedTests, @@ -80,9 +78,9 @@ class TroubleshootNotificationsViewTest { @Config(qualifiers = "h1024dp") @Test - fun `clicking on quick fix emits the expected Event`() { + fun `clicking on quick fix emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTroubleshootNotificationsView( + setTroubleshootNotificationsView( aTroubleshootNotificationsState( tests = listOf( aTroubleshootTestStateFailure( @@ -92,7 +90,7 @@ class TroubleshootNotificationsViewTest { eventSink = eventsRecorder ), ) - rule.onNodeWithText("Attempt to fix").performClick() + onNodeWithText("Attempt to fix").performClick() eventsRecorder.assertList( listOf( TroubleshootNotificationsEvents.RetryFailedTests, @@ -102,7 +100,7 @@ class TroubleshootNotificationsViewTest { } } -private fun AndroidComposeTestRule.setTroubleshootNotificationsView( +private fun AndroidComposeUiTest.setTroubleshootNotificationsView( state: TroubleshootNotificationsState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt index fa4e65ad9a..94cde37452 100644 --- a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt @@ -6,14 +6,17 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.libraries.troubleshoot.impl.history import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_FORMATTED_DATE @@ -23,67 +26,62 @@ import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PushHistoryViewTest { - @get:Rule - val rule = createAndroidComposeRule() - @Test - fun `clicking on Reset sends a PushHistoryEvents`() { + fun `clicking on Reset sends a PushHistoryEvents`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPushHistoryView( + setPushHistoryView( aPushHistoryState( pushCounter = 123, eventSink = eventsRecorder, ), ) - val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu) - rule.onNodeWithContentDescription(menuContentDescription).performClick() - rule.clickOn(CommonStrings.action_reset) + val menuContentDescription = activity!!.getString(CommonStrings.a11y_user_menu) + onNodeWithContentDescription(menuContentDescription).performClick() + clickOn(CommonStrings.action_reset) eventsRecorder.assertSingle(PushHistoryEvents.Reset(requiresConfirmation = true)) // Also check that the push counter is rendered - rule.onNodeWithText("123").assertExists() + onNodeWithText("123").assertExists() } @Test - fun `clicking on show only errors sends a PushHistoryEvents(true)`() { + fun `clicking on show only errors sends a PushHistoryEvents(true)`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPushHistoryView( + setPushHistoryView( aPushHistoryState( showOnlyErrors = false, eventSink = eventsRecorder, ), ) - val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu) - rule.onNodeWithContentDescription(menuContentDescription).performClick() - rule.onNodeWithText("Show only errors").performClick() + val menuContentDescription = activity!!.getString(CommonStrings.a11y_user_menu) + onNodeWithContentDescription(menuContentDescription).performClick() + onNodeWithText("Show only errors").performClick() eventsRecorder.assertSingle(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = true)) } @Test - fun `clicking on show only errors sends a PushHistoryEvents(false)`() { + fun `clicking on show only errors sends a PushHistoryEvents(false)`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPushHistoryView( + setPushHistoryView( aPushHistoryState( showOnlyErrors = true, eventSink = eventsRecorder, ), ) - val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu) - rule.onNodeWithContentDescription(menuContentDescription).performClick() - rule.onNodeWithText("Show only errors").performClick() + val menuContentDescription = activity!!.getString(CommonStrings.a11y_user_menu) + onNodeWithContentDescription(menuContentDescription).performClick() + onNodeWithText("Show only errors").performClick() eventsRecorder.assertSingle(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = false)) } @Test - fun `clicking on an invalid event has no effect`() { + fun `clicking on an invalid event has no effect`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setPushHistoryView( + setPushHistoryView( aPushHistoryState( pushHistoryItems = listOf( aPushHistoryItem( @@ -93,14 +91,14 @@ class PushHistoryViewTest { eventSink = eventsRecorder, ), ) - rule.onNodeWithText(A_FORMATTED_DATE).performClick() + onNodeWithText(A_FORMATTED_DATE).performClick() // No callback invoked } @Test - fun `clicking on a valid event emits the expected Event`() { + fun `clicking on a valid event emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPushHistoryView( + setPushHistoryView( aPushHistoryState( pushHistoryItems = listOf( aPushHistoryItem( @@ -113,7 +111,7 @@ class PushHistoryViewTest { eventSink = eventsRecorder, ), ) - rule.onNodeWithText(A_FORMATTED_DATE).performClick() + onNodeWithText(A_FORMATTED_DATE).performClick() eventsRecorder.assertSingle( PushHistoryEvents.NavigateTo( sessionId = A_SESSION_ID, @@ -124,7 +122,7 @@ class PushHistoryViewTest { } } -private fun AndroidComposeTestRule.setPushHistoryView( +private fun AndroidComposeUiTest.setPushHistoryView( state: PushHistoryState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt index 12cfe44b44..d7ce9e2d28 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt @@ -6,15 +6,17 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.tests.testutils import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import io.element.android.libraries.designsystem.utils.LocalUiTestMode import org.junit.Assert.assertFalse -import org.junit.rules.TestRule import kotlin.coroutines.CoroutineContext object RobolectricDispatcherCleaner { @@ -52,7 +54,7 @@ object RobolectricDispatcherCleaner { } } -fun AndroidComposeTestRule.setSafeContent( +fun AndroidComposeUiTest.setSafeContent( clearAndroidUiDispatcher: Boolean = false, content: @Composable () -> Unit, ) { diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt index d78f570a31..a473e6bd22 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt @@ -6,10 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.tests.testutils import androidx.activity.ComponentActivity import androidx.annotation.StringRes +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.assertIsDisplayed @@ -19,19 +23,17 @@ import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.isDialog -import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import io.element.android.libraries.ui.strings.CommonStrings -import org.junit.rules.TestRule val trueMatcher = SemanticsMatcher("true matcher") { true } -fun AndroidComposeTestRule.clickOn( +fun AndroidComposeUiTest.clickOn( @StringRes res: Int, inDialog: Boolean = false, ) { - val text = activity.getString(res) + val text = activity!!.getString(res) onNode( hasText(text) and hasClickAction() and if (inDialog) hasAnyAncestor(isDialog()) else trueMatcher ) @@ -41,28 +43,28 @@ fun AndroidComposeTestRule.clickOn( /** * Press the back button in the app bar. */ -fun AndroidComposeTestRule.pressBack() { - val text = activity.getString(CommonStrings.action_back) +fun AndroidComposeUiTest.pressBack() { + val text = activity!!.getString(CommonStrings.action_back) onNode(hasContentDescription(text)).performClick() } /** * Press the back key. */ -fun AndroidComposeTestRule.pressBackKey() { - activity.onBackPressedDispatcher.onBackPressed() +fun AndroidComposeUiTest.pressBackKey() { + activity!!.onBackPressedDispatcher.onBackPressed() } fun SemanticsNodeInteractionsProvider.pressTag(tag: String) { onNode(hasTestTag(tag)).performClick() } -fun AndroidComposeTestRule.assertNoNodeWithText(@StringRes res: Int) { - val text = activity.getString(res) +fun AndroidComposeUiTest.assertNoNodeWithText(@StringRes res: Int) { + val text = activity!!.getString(res) onNodeWithText(text).assertDoesNotExist() } -fun AndroidComposeTestRule.assertNodeWithTextIsDisplayed(@StringRes res: Int) { - val text = activity.getString(res) +fun AndroidComposeUiTest.assertNodeWithTextIsDisplayed(@StringRes res: Int) { + val text = activity!!.getString(res) onNodeWithText(text).assertIsDisplayed() } From 6c7c48da698afb7daf5a6f7b23849110e8a5d9b0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Apr 2026 16:59:14 +0200 Subject: [PATCH 18/21] Fix compilation issue --- .../android/features/call/ui/CallScreenViewTest.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt index fed9f90de0..e4f9c10a3c 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt @@ -18,9 +18,9 @@ import androidx.compose.ui.test.AndroidComposeUiTest import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.features.call.impl.pip.PictureInPictureEvents +import io.element.android.features.call.impl.pip.PictureInPictureEvent import io.element.android.features.call.impl.pip.aPictureInPictureState -import io.element.android.features.call.impl.ui.CallScreenEvents +import io.element.android.features.call.impl.ui.CallScreenEvent import io.element.android.features.call.impl.ui.CallScreenView import io.element.android.features.call.impl.ui.JavascriptBackHandler import io.element.android.features.call.impl.ui.aCallScreenState @@ -39,7 +39,7 @@ import org.robolectric.shadows.ShadowWebView class CallScreenViewTest { @Test fun `pressing back key triggers hangup when no web view is available and pip is unsupported`() = runAndroidComposeUiTest { - val callEvents = EventsRecorder() + val callEvents = EventsRecorder() setCallScreenView( state = aCallScreenState(eventSink = callEvents), @@ -72,7 +72,7 @@ class CallScreenViewTest { @Config(shadows = [RecordingShadowWebView::class]) @Test fun `web view javascript back handler emits pip event when pip is supported`() = runAndroidComposeUiTest { - val pipEvents = EventsRecorder() + val pipEvents = EventsRecorder() setCallScreenView( state = aCallScreenState(), @@ -88,8 +88,8 @@ class CallScreenViewTest { } pipEvents.assertSize(2) - pipEvents.assertTrue(0) { it is PictureInPictureEvents.SetPipController } - pipEvents.assertTrue(1) { it is PictureInPictureEvents.EnterPictureInPicture } + pipEvents.assertTrue(0) { it is PictureInPictureEvent.SetPipController } + pipEvents.assertTrue(1) { it is PictureInPictureEvent.EnterPictureInPicture } } } From e81ab35f5b9f483cf412d50dfe545923292579c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 08:06:30 +0200 Subject: [PATCH 19/21] Update dependency io.nlopez.compose.rules:detekt to v0.5.8 (#6711) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index dfd32f0872..474b868eda 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -46,7 +46,7 @@ allprojects { config.from(files("$rootDir/tools/detekt/detekt.yml")) } dependencies { - detektPlugins("io.nlopez.compose.rules:detekt:0.5.7") + detektPlugins("io.nlopez.compose.rules:detekt:0.5.8") detektPlugins(project(":tests:detekt-rules")) } From 5733c3149a7d7cfa319a7ca8e8b9cac7657a7a7e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 08:07:11 +0200 Subject: [PATCH 20/21] Update dependency com.posthog:posthog-android to v3.43.0 (#6704) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3a06ccd49c..3c73a55a15 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -221,7 +221,7 @@ haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = color_picker = "io.mhssn:colorpicker:1.0.0" # Analytics -posthog = "com.posthog:posthog-android:3.39.0" +posthog = "com.posthog:posthog-android:3.43.0" sentry = "io.sentry:sentry-android:8.40.0" # main branch can be tested replacing the version with main-SNAPSHOT matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.33.2" From 27869c0e0427b8a9c289399930cfac0743efcfcf Mon Sep 17 00:00:00 2001 From: Hi Dude! Date: Mon, 4 May 2026 09:30:05 +0300 Subject: [PATCH 21/21] Fix calls on Huawei devices: skip addWebMessageListener on Chromium < 119 (#6640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix calls on Huawei: skip addWebMessageListener on Chromium < 119 * Fix lint issues, log webview version --------- Co-authored-by: manfrommedan Co-authored-by: Jorge Martín --- .../utils/WebViewWidgetMessageInterceptor.kt | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt index f7ab2c57af..c74ae90abd 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt @@ -140,26 +140,33 @@ class WebViewWidgetMessageInterceptor( } } - // Create a WebMessageListener, which will receive messages from the WebView and reply to them - val webMessageListener = WebViewCompat.WebMessageListener { _, message, _, _, _ -> - onMessageReceived(message.data) - } + // Always register JavascriptInterface as the baseline message channel. + // This works on all WebView implementations including Huawei. + webView.addJavascriptInterface(object { + @JavascriptInterface + fun postMessage(json: String?) { + onMessageReceived(json) + } + }, LISTENER_NAME) - // Use WebMessageListener if supported, otherwise use JavascriptInterface - if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + // Additionally register WebMessageListener on WebViews that reliably support it. + // Huawei WebView (Chromium < 119) reports WEB_MESSAGE_LISTENER as supported + // but silently drops messages, so we only trust it on Chromium 119+. + // See: https://github.com/element-hq/element-x-android/issues/6632 + val webViewVersionName = WebViewCompat.getCurrentWebViewPackage(webView.context)?.versionName.orEmpty() + Timber.d("Using WebView version: $webViewVersionName") + val webViewVersionCode = webViewVersionName.split(".").firstOrNull()?.toIntOrNull() ?: 0 + + if (webViewVersionCode >= 119 && + WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { WebViewCompat.addWebMessageListener( webView, LISTENER_NAME, setOf("*"), - webMessageListener - ) - } else { - webView.addJavascriptInterface(object { - @JavascriptInterface - fun postMessage(json: String?) { - onMessageReceived(json) + WebViewCompat.WebMessageListener { _, message, _, _, _ -> + onMessageReceived(message.data) } - }, LISTENER_NAME) + ) } }