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 @@
-
+
+
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/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"))
}
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/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" />
(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/receivers/DeclineCallBroadcastReceiver.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt
index 179e6c2b22..bf27e8d39d 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt
@@ -13,7 +13,7 @@ import android.content.Context
import android.content.Intent
import androidx.core.content.IntentCompat
import dev.zacsweers.metro.Inject
-import io.element.android.features.call.api.CallType
+import io.element.android.features.call.api.CallData
import io.element.android.features.call.impl.di.CallBindings
import io.element.android.features.call.impl.notifications.CallNotificationData
import io.element.android.features.call.impl.utils.ActiveCallManager
@@ -42,7 +42,7 @@ class DeclineCallBroadcastReceiver : BroadcastReceiver() {
context.bindings().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/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/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 da2c57c0ac..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
@@ -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,25 +139,22 @@ 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 = ""
}
}
}
- 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) {
@@ -187,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()
}
@@ -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..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,6 +15,5 @@ data class CallScreenState(
val webViewError: String?,
val userAgent: String,
val isCallActive: Boolean,
- val isInWidgetMode: 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 3e72f96f87..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,15 +26,13 @@ internal fun aCallScreenState(
webViewError: String? = null,
userAgent: String = "",
isCallActive: Boolean = true,
- isInWidgetMode: Boolean = false,
- eventSink: (CallScreenEvents) -> Unit = {},
+ eventSink: (CallScreenEvent) -> Unit = {},
): CallScreenState {
return CallScreenState(
urlState = urlState,
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/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt
index f8657a9ece..ea3668316b 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
@@ -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(PictureInPictureEvent.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(
@@ -84,7 +88,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) }
@@ -111,6 +115,7 @@ internal fun CallScreenView(
},
onConsoleMessage = onConsoleMessage,
onCreateWebView = { webView ->
+ callWebView = webView
webView.addBackHandler(onBackPressed = ::handleBack)
val interceptor = WebViewWidgetMessageInterceptor(
webView = webView,
@@ -123,18 +128,19 @@ 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))
+ pipState.eventSink(PictureInPictureEvent.SetPipController(pipController))
},
onDestroyWebView = {
+ callWebView = null
// Reset audio mode
webViewAudioManager?.onCallStopped()
}
@@ -143,13 +149,15 @@ 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(
content = state.urlState.error.message.orEmpty(),
- onSubmit = { state.eventSink(CallScreenEvents.Hangup) },
+ onSubmit = { state.eventSink(CallScreenEvent.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/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..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
@@ -35,16 +35,14 @@ 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
+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
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,9 +77,9 @@ class ElementCallActivity :
private val requestPermissionsLauncher = registerPermissionResultLauncher()
- private val webViewTarget = mutableStateOf(null)
+ 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)
@@ -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()
}
}
@@ -163,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)
@@ -173,10 +169,10 @@ 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(CallScreenEvents.Hangup)
+ eventSink?.invoke(CallScreenEvent.Hangup)
}
}
addOnPictureInPictureModeChangedListener(onPictureInPictureModeChangedListener)
@@ -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()
@@ -287,7 +280,7 @@ class ElementCallActivity :
}
override fun hangUp() {
- eventSink?.invoke(CallScreenEvents.Hangup)
+ eventSink?.invoke(CallScreenEvent.Hangup)
}
}
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/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)
+ )
}
}
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/impl/pip/PictureInPicturePresenterTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt
index c087fa3c35..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,18 +47,16 @@ class PictureInPicturePresenterTest {
enterPipModeResult = enterPipModeResult,
),
)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
+ presenter.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()
}
@@ -80,12 +72,10 @@ class PictureInPicturePresenterTest {
handUpResult = handUpResult
),
)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
+ presenter.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()
}
}
@@ -102,12 +92,10 @@ class PictureInPicturePresenterTest {
enterPipModeResult = enterPipModeResult
),
)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
+ presenter.test {
val initialState = awaitItem()
initialState.eventSink(
- PictureInPictureEvents.SetPipController(
+ PictureInPictureEvent.SetPipController(
FakePipController(
canEnterPipResult = { true },
enterPipResult = enterPipResult,
@@ -115,16 +103,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()
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/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/CallScreenPresenterTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt
index b6b0120451..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
@@ -13,8 +13,8 @@ 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.impl.ui.CallScreenEvents
+import io.element.android.features.call.api.CallData
+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
@@ -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
@@ -59,46 +60,19 @@ 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),
activeCallManager = FakeActiveCallManager(joinedCallResult = joinedCallLambda),
)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
+ presenter.test {
// Wait until the URL is loaded
advanceTimeBy(1.seconds)
skipItems(1)
@@ -107,7 +81,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,19 +96,17 @@ 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 {},
)
val messageInterceptor = FakeWidgetMessageInterceptor()
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
+ presenter.test {
// Give it time to load the URL and WidgetDriver
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")
@@ -154,24 +125,22 @@ 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),
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
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()
@@ -188,22 +157,20 @@ 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),
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
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"}""")
@@ -223,22 +190,20 @@ 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),
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)
val initialState = awaitItem()
assertThat(initialState.isCallActive).isFalse()
- initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor))
+ initialState.eventSink(CallScreenEvent.SetupMessageChannels(messageInterceptor))
messageInterceptor.givenInterceptedMessage(
"""
{
@@ -260,22 +225,20 @@ 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),
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)
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
@@ -300,7 +263,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 +301,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 +319,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/CallScreenViewTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt
new file mode 100644
index 0000000000..e4f9c10a3c
--- /dev/null
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt
@@ -0,0 +1,148 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalTestApi::class)
+
+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.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.PictureInPictureEvent
+import io.element.android.features.call.impl.pip.aPictureInPictureState
+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
+import io.element.android.tests.testutils.EventsRecorder
+import io.element.android.tests.testutils.pressBackKey
+import org.junit.Assert.assertEquals
+import org.junit.Test
+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 {
+ @Test
+ fun `pressing back key triggers hangup when no web view is available and pip is unsupported`() = runAndroidComposeUiTest {
+ val callEvents = EventsRecorder()
+
+ setCallScreenView(
+ state = aCallScreenState(eventSink = callEvents),
+ useInspectionMode = true,
+ )
+
+ pressBackKey()
+
+ callEvents.assertEmpty()
+ }
+
+ @Config(shadows = [RecordingShadowWebView::class])
+ @Test
+ fun `pressing back key dispatches escape key events to web view when pip is unsupported`() = runAndroidComposeUiTest {
+ setCallScreenView(
+ state = aCallScreenState(),
+ useInspectionMode = false,
+ )
+
+ 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`() = runAndroidComposeUiTest {
+ val pipEvents = EventsRecorder()
+
+ setCallScreenView(
+ state = aCallScreenState(),
+ useInspectionMode = false,
+ pipState = aPictureInPictureState(
+ supportPip = true,
+ eventSink = pipEvents,
+ ),
+ )
+
+ runOnIdle {
+ RecordingShadowWebView.invokeJavascriptBackHandler()
+ }
+
+ pipEvents.assertSize(2)
+ pipEvents.assertTrue(0) { it is PictureInPictureEvent.SetPipController }
+ pipEvents.assertTrue(1) { it is PictureInPictureEvent.EnterPictureInPicture }
+ }
+}
+
+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),
+) {
+ 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
+ }
+}
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/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/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/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/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/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/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/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/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/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/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 4ccdb6b387..30f456240f 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
@@ -151,10 +151,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/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/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/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