Merge branch 'element-hq:develop' into feature/scroll-to-unread-messages
This commit is contained in:
Generated
+2
-1
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.3.20" />
|
||||
<option name="externalSystemId" value="Gradle" />
|
||||
<option name="version" value="2.3.21" />
|
||||
</component>
|
||||
</project>
|
||||
|
||||
@@ -103,13 +103,13 @@ android {
|
||||
logger.warnInBox("Building ${defaultConfig.applicationId} ($baseAppName) [$buildType]")
|
||||
|
||||
buildTypes {
|
||||
val oidcRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android"
|
||||
val oAuthRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android"
|
||||
getByName("debug") {
|
||||
resValue("string", "app_name", "$baseAppName dbg")
|
||||
resValue(
|
||||
"string",
|
||||
"login_redirect_scheme",
|
||||
"$oidcRedirectSchemeBase.debug",
|
||||
"$oAuthRedirectSchemeBase.debug",
|
||||
)
|
||||
applicationIdSuffix = ".debug"
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
@@ -120,7 +120,7 @@ android {
|
||||
resValue(
|
||||
"string",
|
||||
"login_redirect_scheme",
|
||||
oidcRedirectSchemeBase,
|
||||
oAuthRedirectSchemeBase,
|
||||
)
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
|
||||
@@ -157,7 +157,7 @@ android {
|
||||
resValue(
|
||||
"string",
|
||||
"login_redirect_scheme",
|
||||
"$oidcRedirectSchemeBase.nightly",
|
||||
"$oAuthRedirectSchemeBase.nightly",
|
||||
)
|
||||
matchingFallbacks += listOf("release")
|
||||
signingConfig = signingConfigs.getByName("nightly")
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
android:scheme="elementx" />
|
||||
</intent-filter>
|
||||
<!--
|
||||
Oidc redirection
|
||||
OAuth redirection
|
||||
-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
+3
-3
@@ -10,14 +10,14 @@ package io.element.android.x.oidc
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthRedirectUrlProvider
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import io.element.android.x.R
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultOidcRedirectUrlProvider(
|
||||
class DefaultOAuthRedirectUrlProvider(
|
||||
private val stringProvider: StringProvider,
|
||||
) : OidcRedirectUrlProvider {
|
||||
) : OAuthRedirectUrlProvider {
|
||||
override fun provide() = buildString {
|
||||
append(stringProvider.getString(R.string.login_redirect_scheme))
|
||||
append(":/")
|
||||
+2
-2
@@ -13,13 +13,13 @@ import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import io.element.android.x.R
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultOidcRedirectUrlProviderTest {
|
||||
class DefaultOAuthRedirectUrlProviderTest {
|
||||
@Test
|
||||
fun `test provide`() {
|
||||
val stringProvider = FakeStringProvider(
|
||||
defaultResult = "str"
|
||||
)
|
||||
val sut = DefaultOidcRedirectUrlProvider(
|
||||
val sut = DefaultOAuthRedirectUrlProvider(
|
||||
stringProvider = stringProvider,
|
||||
)
|
||||
val result = sut.provide()
|
||||
@@ -48,6 +48,8 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(libs.androidx.annotationjvm)
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
||||
@@ -33,13 +33,14 @@ dependencies {
|
||||
implementation(projects.libraries.deeplink.api)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.oidc.api)
|
||||
implementation(projects.libraries.oauth.api)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.push.api)
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
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)
|
||||
@@ -59,7 +60,7 @@ dependencies {
|
||||
testImplementation(projects.features.login.test)
|
||||
testImplementation(projects.features.share.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.oidc.test)
|
||||
testImplementation(projects.libraries.oauth.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.libraries.pushproviders.test)
|
||||
|
||||
@@ -63,8 +63,8 @@ import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.asEventId
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||
import io.element.android.libraries.oauth.api.OAuthAction
|
||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.ui.common.nodes.emptyNode
|
||||
@@ -95,7 +95,7 @@ class RootFlowNode(
|
||||
private val signedOutEntryPoint: SignedOutEntryPoint,
|
||||
private val accountSelectEntryPoint: AccountSelectEntryPoint,
|
||||
private val intentResolver: IntentResolver,
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
private val oAuthActionFlow: OAuthActionFlow,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val announcementService: AnnouncementService,
|
||||
private val analyticsService: AnalyticsService,
|
||||
@@ -392,7 +392,7 @@ class RootFlowNode(
|
||||
navigateTo(resolvedIntent.deeplinkData)
|
||||
}
|
||||
is ResolvedIntent.Login -> onLoginLink(resolvedIntent.params)
|
||||
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
|
||||
is ResolvedIntent.OAuth -> onOAuthAction(resolvedIntent.oAuthAction)
|
||||
is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData)
|
||||
is ResolvedIntent.IncomingShare -> onIncomingShare(resolvedIntent.shareIntentData)
|
||||
}
|
||||
@@ -529,8 +529,8 @@ class RootFlowNode(
|
||||
}
|
||||
}
|
||||
|
||||
private fun onOidcAction(oidcAction: OidcAction) {
|
||||
oidcActionFlow.post(oidcAction)
|
||||
private fun onOAuthAction(oAuthAction: OAuthAction) {
|
||||
oAuthActionFlow.post(oAuthAction)
|
||||
}
|
||||
|
||||
private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
|
||||
|
||||
@@ -18,13 +18,13 @@ import io.element.android.libraries.deeplink.api.DeeplinkData
|
||||
import io.element.android.libraries.deeplink.api.DeeplinkParser
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.api.OidcIntentResolver
|
||||
import io.element.android.libraries.oauth.api.OAuthAction
|
||||
import io.element.android.libraries.oauth.api.OAuthIntentResolver
|
||||
import timber.log.Timber
|
||||
|
||||
sealed interface ResolvedIntent {
|
||||
data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent
|
||||
data class Oidc(val oidcAction: OidcAction) : ResolvedIntent
|
||||
data class OAuth(val oAuthAction: OAuthAction) : ResolvedIntent
|
||||
data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent
|
||||
data class Login(val params: LoginParams) : ResolvedIntent
|
||||
data class IncomingShare(val shareIntentData: ShareIntentData) : ResolvedIntent
|
||||
@@ -34,7 +34,7 @@ sealed interface ResolvedIntent {
|
||||
class IntentResolver(
|
||||
private val deeplinkParser: DeeplinkParser,
|
||||
private val loginIntentResolver: LoginIntentResolver,
|
||||
private val oidcIntentResolver: OidcIntentResolver,
|
||||
private val oAuthIntentResolver: OAuthIntentResolver,
|
||||
private val permalinkParser: PermalinkParser,
|
||||
private val shareIntentHandler: ShareIntentHandler,
|
||||
) {
|
||||
@@ -45,9 +45,9 @@ class IntentResolver(
|
||||
val deepLinkData = deeplinkParser.getFromIntent(intent)
|
||||
if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData)
|
||||
|
||||
// Coming during login using Oidc?
|
||||
val oidcAction = oidcIntentResolver.resolve(intent)
|
||||
if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction)
|
||||
// Coming during login using OAuth?
|
||||
val oAuthAction = oAuthIntentResolver.resolve(intent)
|
||||
if (oAuthAction != null) return ResolvedIntent.OAuth(oAuthAction)
|
||||
|
||||
val actionViewData = intent
|
||||
.takeIf { it.action == Intent.ACTION_VIEW }
|
||||
|
||||
@@ -26,8 +26,8 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.test.FakeOidcIntentResolver
|
||||
import io.element.android.libraries.oauth.api.OAuthAction
|
||||
import io.element.android.libraries.oauth.test.FakeOAuthIntentResolver
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -170,9 +170,9 @@ class IntentResolverTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test resolve oidc`() {
|
||||
fun `test resolve OAuth`() {
|
||||
val sut = createIntentResolver(
|
||||
oidcIntentResolverResult = { OidcAction.GoBack() },
|
||||
oAuthIntentResolverResult = { OAuthAction.GoBack() },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
@@ -180,8 +180,8 @@ class IntentResolverTest {
|
||||
}
|
||||
val result = sut.resolve(intent)
|
||||
assertThat(result).isEqualTo(
|
||||
ResolvedIntent.Oidc(
|
||||
oidcAction = OidcAction.GoBack()
|
||||
ResolvedIntent.OAuth(
|
||||
oAuthAction = OAuthAction.GoBack()
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -194,7 +194,7 @@ class IntentResolverTest {
|
||||
val sut = createIntentResolver(
|
||||
loginIntentResolverResult = { null },
|
||||
permalinkParserResult = { permalinkData },
|
||||
oidcIntentResolverResult = { null },
|
||||
oAuthIntentResolverResult = { null },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
@@ -213,7 +213,7 @@ class IntentResolverTest {
|
||||
val sut = createIntentResolver(
|
||||
permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) },
|
||||
loginIntentResolverResult = { null },
|
||||
oidcIntentResolverResult = { null },
|
||||
oAuthIntentResolverResult = { null },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
@@ -230,7 +230,7 @@ class IntentResolverTest {
|
||||
)
|
||||
val sut = createIntentResolver(
|
||||
permalinkParserResult = { permalinkData },
|
||||
oidcIntentResolverResult = { null },
|
||||
oAuthIntentResolverResult = { null },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_BATTERY_LOW
|
||||
@@ -244,7 +244,7 @@ class IntentResolverTest {
|
||||
fun `test incoming share simple`() {
|
||||
val shareIntentData = ShareIntentData.PlainText("Hello")
|
||||
val sut = createIntentResolver(
|
||||
oidcIntentResolverResult = { null },
|
||||
oAuthIntentResolverResult = { null },
|
||||
onIncomingShareIntent = { shareIntentData },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
@@ -260,7 +260,7 @@ class IntentResolverTest {
|
||||
val fileUri = "content://com.example.app/file1.jpg".toUri()
|
||||
val shareIntentData = ShareIntentData.Uris(text = "Hello", uris = listOf(UriToShare(fileUri, "image/jpg")))
|
||||
val sut = createIntentResolver(
|
||||
oidcIntentResolverResult = { null },
|
||||
oAuthIntentResolverResult = { null },
|
||||
onIncomingShareIntent = { shareIntentData },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
@@ -277,7 +277,7 @@ class IntentResolverTest {
|
||||
val sut = createIntentResolver(
|
||||
permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) },
|
||||
loginIntentResolverResult = { null },
|
||||
oidcIntentResolverResult = { null },
|
||||
oAuthIntentResolverResult = { null },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
@@ -292,7 +292,7 @@ class IntentResolverTest {
|
||||
val aLoginParams = LoginParams("accountProvider", null)
|
||||
val sut = createIntentResolver(
|
||||
loginIntentResolverResult = { aLoginParams },
|
||||
oidcIntentResolverResult = { null },
|
||||
oAuthIntentResolverResult = { null },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
@@ -306,7 +306,7 @@ class IntentResolverTest {
|
||||
deeplinkParserResult: DeeplinkData? = null,
|
||||
permalinkParserResult: (String) -> PermalinkData = { lambdaError() },
|
||||
loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() },
|
||||
oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() },
|
||||
oAuthIntentResolverResult: (Intent) -> OAuthAction? = { lambdaError() },
|
||||
onIncomingShareIntent: (Intent) -> ShareIntentData? = { null },
|
||||
): IntentResolver {
|
||||
return IntentResolver(
|
||||
@@ -314,8 +314,8 @@ class IntentResolverTest {
|
||||
loginIntentResolver = FakeLoginIntentResolver(
|
||||
parseResult = loginIntentResolverResult,
|
||||
),
|
||||
oidcIntentResolver = FakeOidcIntentResolver(
|
||||
resolveResult = oidcIntentResolverResult,
|
||||
oAuthIntentResolver = FakeOAuthIntentResolver(
|
||||
resolveResult = oAuthIntentResolverResult,
|
||||
),
|
||||
permalinkParser = FakePermalinkParser(
|
||||
result = permalinkParserResult
|
||||
|
||||
@@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
||||
import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
This file contains some rough notes about Oidc implementation, with some examples of actual data.
|
||||
This file contains some rough notes about OAuth implementation, with some examples of actual data.
|
||||
|
||||
[ios implementation](https://github.com/element-hq/element-x-ios/compare/develop...doug/oidc-temp)
|
||||
|
||||
@@ -25,7 +25,7 @@ tosUri = "https://element.io/user-terms-of-service",
|
||||
policyUri = "https://element.io/privacy"
|
||||
|
||||
|
||||
Example of OidcData (from presentUrl callback):
|
||||
Example of OAuthData (from presentUrl callback):
|
||||
url: https://auth-oidc.lab.element.dev/authorize?response_type=code&client_id=01GYCAGG3PA70CJ97ZVP0WFJY3&redirect_uri=io.element%3A%2Fcallback&scope=openid+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Aapi%3A*+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Adevice%3AYAgcPW4mcG&state=ex6mNJVFZ5jn9wL8&nonce=NZ93DOyIGQd9exPQ&code_challenge_method=S256&code_challenge=FFRcPALNSPCh-ZgpyTRFu_h8NZJVncfvihbfT9CyX8U&prompt=consent
|
||||
|
||||
Formatted url:
|
||||
@@ -43,8 +43,8 @@ https://auth-oidc.lab.element.dev/authorize?
|
||||
state: ex6mNJVFZ5jn9wL8
|
||||
|
||||
|
||||
Oidc client example: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/examples/oidc_cli/src/main.rs
|
||||
Oidc sdk doc: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/crates/matrix-sdk/src/oidc.rs
|
||||
OAuth client example: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/examples/oidc_cli/src/main.rs
|
||||
OAuth sdk doc: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/crates/matrix-sdk/src/oidc.rs
|
||||
|
||||
|
||||
Test server:
|
||||
+12
-13
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back sends a AnnouncementEvent`() {
|
||||
fun `clicking on back sends a AnnouncementEvent`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<AnnouncementEvent>()
|
||||
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<AnnouncementEvent>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setFullscreenAnnouncementView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setFullscreenAnnouncementView(
|
||||
state: AnnouncementState,
|
||||
) {
|
||||
setContent {
|
||||
|
||||
+6
-19
@@ -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
|
||||
+4
-4
@@ -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?,
|
||||
|
||||
@@ -30,44 +30,10 @@
|
||||
<activity
|
||||
android:name=".ui.ElementCallActivity"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
||||
android:exported="true"
|
||||
android:label="@string/element_call"
|
||||
android:launchMode="singleTask"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:taskAffinity="io.element.android.features.call">
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
|
||||
<!-- Matching asset file: https://call.element.io/.well-known/assetlinks.json -->
|
||||
<data android:host="call.element.io" />
|
||||
</intent-filter>
|
||||
<!-- Custom scheme to handle urls from other domains in the format: element://call?url=https%3A%2F%2Felement.io -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="element" />
|
||||
<data android:host="call" />
|
||||
</intent-filter>
|
||||
<!-- Custom scheme to handle urls from other domains in the format: io.element.call:/?url=https%3A%2F%2Felement.io -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="io.element.call" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
android:taskAffinity="io.element.android.features.call" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.IncomingCallActivity"
|
||||
|
||||
+7
-7
@@ -11,7 +11,7 @@ package io.element.android.features.call.impl
|
||||
import android.content.Context
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
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.notifications.CallNotificationData
|
||||
import io.element.android.features.call.impl.utils.ActiveCallManager
|
||||
@@ -30,12 +30,12 @@ class DefaultElementCallEntryPoint(
|
||||
const val REQUEST_CODE = 2255
|
||||
}
|
||||
|
||||
override fun startCall(callType: CallType) {
|
||||
context.startActivity(IntentProvider.createIntent(context, callType))
|
||||
override fun startCall(callData: CallData) {
|
||||
context.startActivity(IntentProvider.createIntent(context, callData))
|
||||
}
|
||||
|
||||
override suspend fun handleIncomingCall(
|
||||
callType: CallType.RoomCall,
|
||||
callData: CallData,
|
||||
eventId: EventId,
|
||||
senderId: UserId,
|
||||
roomName: String?,
|
||||
@@ -47,8 +47,8 @@ class DefaultElementCallEntryPoint(
|
||||
textContent: String?,
|
||||
) {
|
||||
val incomingCallNotificationData = CallNotificationData(
|
||||
sessionId = callType.sessionId,
|
||||
roomId = callType.roomId,
|
||||
sessionId = callData.sessionId,
|
||||
roomId = callData.roomId,
|
||||
eventId = eventId,
|
||||
senderId = senderId,
|
||||
roomName = roomName,
|
||||
@@ -58,7 +58,7 @@ class DefaultElementCallEntryPoint(
|
||||
expirationTimestamp = expirationTimestamp,
|
||||
notificationChannelId = notificationChannelId,
|
||||
textContent = textContent,
|
||||
audioOnly = callType.isAudioCall
|
||||
audioOnly = callData.isAudioCall,
|
||||
)
|
||||
activeCallManager.registerIncomingCall(notificationData = incomingCallNotificationData)
|
||||
}
|
||||
|
||||
+9
-2
@@ -18,7 +18,7 @@ import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.app.Person
|
||||
import dev.zacsweers.metro.Inject
|
||||
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.impl.receivers.DeclineCallBroadcastReceiver
|
||||
import io.element.android.features.call.impl.ui.IncomingCallActivity
|
||||
import io.element.android.features.call.impl.utils.IntentProvider
|
||||
@@ -89,7 +89,14 @@ class RingingCallNotificationCreator(
|
||||
.setImportant(true)
|
||||
.build()
|
||||
|
||||
val answerIntent = IntentProvider.getPendingIntent(context, CallType.RoomCall(sessionId, roomId, isAudioCall = audioOnly))
|
||||
val answerIntent = IntentProvider.getPendingIntent(
|
||||
context,
|
||||
CallData(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
isAudioCall = audioOnly,
|
||||
),
|
||||
)
|
||||
val notificationData = CallNotificationData(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
|
||||
+4
-4
@@ -10,8 +10,8 @@ package io.element.android.features.call.impl.pip
|
||||
|
||||
import io.element.android.features.call.impl.utils.PipController
|
||||
|
||||
sealed interface PictureInPictureEvents {
|
||||
data class SetPipController(val pipController: PipController) : PictureInPictureEvents
|
||||
data object EnterPictureInPicture : PictureInPictureEvents
|
||||
data class OnPictureInPictureModeChanged(val isInPip: Boolean) : PictureInPictureEvents
|
||||
sealed interface PictureInPictureEvent {
|
||||
data class SetPipController(val pipController: PipController) : PictureInPictureEvent
|
||||
data object EnterPictureInPicture : PictureInPictureEvent
|
||||
data class OnPictureInPictureModeChanged(val isInPip: Boolean) : PictureInPictureEvent
|
||||
}
|
||||
+4
-4
@@ -36,17 +36,17 @@ class PictureInPicturePresenter(
|
||||
var isInPictureInPicture by remember { mutableStateOf(false) }
|
||||
var pipController by remember { mutableStateOf<PipController?>(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) {
|
||||
|
||||
+1
-1
@@ -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,
|
||||
)
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+2
-2
@@ -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<CallBindings>().inject(this)
|
||||
appCoroutineScope.launch {
|
||||
activeCallManager.hangUpCall(
|
||||
callType = CallType.RoomCall(
|
||||
callData = CallData(
|
||||
sessionId = notificationData.sessionId,
|
||||
roomId = notificationData.roomId,
|
||||
isAudioCall = notificationData.audioOnly
|
||||
|
||||
+26
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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
|
||||
}
|
||||
+34
-56
@@ -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<CallScreenState> {
|
||||
@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<AsyncData<String>>,
|
||||
callWidgetDriver: MutableState<MatrixWidgetDriver?>,
|
||||
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)
|
||||
}
|
||||
|
||||
+1
-2
@@ -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,
|
||||
)
|
||||
|
||||
+1
-3
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
+32
-16
@@ -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<String>, 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<WebView?>(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<WebViewAudioManager?>(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()
|
||||
}
|
||||
|
||||
-19
@@ -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
|
||||
}
|
||||
}
|
||||
+24
-31
@@ -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<CallType?>(null)
|
||||
private val webViewTarget = mutableStateOf<CallData?>(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<Array<String>> {
|
||||
return registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
@@ -287,7 +280,7 @@ class ElementCallActivity :
|
||||
}
|
||||
|
||||
override fun hangUp() {
|
||||
eventSink?.invoke(CallScreenEvents.Hangup)
|
||||
eventSink?.invoke(CallScreenEvent.Hangup)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-27
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
-98
@@ -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"
|
||||
+5
-5
@@ -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
|
||||
)!!
|
||||
|
||||
+3
-3
@@ -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",
|
||||
|
||||
+15
-27
@@ -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()
|
||||
|
||||
+23
@@ -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)")
|
||||
}
|
||||
}
|
||||
+96
@@ -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()
|
||||
}
|
||||
}
|
||||
+26
-108
@@ -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<MobileScreen.ScreenName, Unit> {}
|
||||
val joinedCallLambda = lambdaRecorder<CallType, Unit> {}
|
||||
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<MobileScreen.ScreenName, Unit> {}
|
||||
val joinedCallLambda = lambdaRecorder<CallType, Unit> {}
|
||||
val joinedCallLambda = lambdaRecorder<CallData, Unit> {}
|
||||
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,
|
||||
|
||||
+148
@@ -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<CallScreenEvent>()
|
||||
|
||||
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<PictureInPictureEvent>()
|
||||
|
||||
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<ComponentActivity>.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<KeyEvent>()
|
||||
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
|
||||
}
|
||||
}
|
||||
-45
@@ -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)")
|
||||
}
|
||||
}
|
||||
-226
@@ -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"
|
||||
}
|
||||
}
|
||||
+19
-13
@@ -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<NotificationManagerCompat>(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<NotificationManagerCompat>(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,
|
||||
|
||||
+7
-7
@@ -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<ActiveCall?>(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?) {
|
||||
|
||||
+7
-7
@@ -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,
|
||||
|
||||
+27
-28
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back invokes the expected callback`() {
|
||||
fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>(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<AccountDeactivationEvents>()
|
||||
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<AccountDeactivationEvents>()
|
||||
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<AccountDeactivationEvents>()
|
||||
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<AccountDeactivationEvents>()
|
||||
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<AccountDeactivationEvents>()
|
||||
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<AccountDeactivationEvents>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAccountDeactivationView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setAccountDeactivationView(
|
||||
state: AccountDeactivationState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
+11
-12
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `cancel error emits the expected event`() {
|
||||
fun `cancel error emits the expected event`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<ForwardMessagesEvents>()
|
||||
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<ForwardMessagesEvents>(expectEvents = false)
|
||||
ensureCalledOnceWithParam<List<RoomId>?>(data) { callback ->
|
||||
rule.setForwardMessagesView(
|
||||
setForwardMessagesView(
|
||||
aForwardMessagesState(
|
||||
forwardAction = AsyncAction.Success(data),
|
||||
eventSink = eventsRecorder
|
||||
@@ -59,7 +58,7 @@ class ForwardMessagesViewTest {
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setForwardMessagesView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setForwardMessagesView(
|
||||
state: ForwardMessagesState,
|
||||
onForwardSuccess: (List<RoomId>) -> Unit = EnsureNeverCalledWithParam(),
|
||||
) {
|
||||
|
||||
+18
-19
@@ -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<ComponentActivity>()
|
||||
|
||||
@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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setChooseSelfVerificationModeView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setChooseSelfVerificationModeView(
|
||||
state: ChooseSelfVerificationModeState,
|
||||
onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
|
||||
onUseAnotherDevice: () -> Unit = EnsureNeverCalled(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
+10
-10
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on filters generates expected Event`() {
|
||||
fun `clicking on filters generates expected Event`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<RoomListFiltersEvent>()
|
||||
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<ComponentActivity> {
|
||||
val eventsRecorder = EventsRecorder<RoomListFiltersEvent>()
|
||||
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,
|
||||
|
||||
+24
-24
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on Mark as read generates expected Events`() {
|
||||
fun `clicking on Mark as read generates expected Events`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||
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<RoomListEvent>()
|
||||
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<RoomListEvent>()
|
||||
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<RoomListEvent>()
|
||||
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<RoomListEvent>()
|
||||
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<RoomListEvent>()
|
||||
val contextMenu = aContextMenuShown(isDm = false, isFavorite = false)
|
||||
val callback = EnsureNeverCalledWithParam<RoomId>()
|
||||
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<ComponentActivity>.setRoomListContextMenu(
|
||||
contextMenu: RoomListState.ContextMenu.Shown,
|
||||
canReportRoom: Boolean = false,
|
||||
eventSink: (RoomListEvent) -> Unit,
|
||||
|
||||
+16
-17
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on decline emits the expected Events`() {
|
||||
fun `clicking on decline emits the expected Events`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||
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<RoomListEvent>()
|
||||
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<RoomListEvent>()
|
||||
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<RoomListEvent>()
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
+41
-43
@@ -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<ComponentActivity>()
|
||||
|
||||
@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<RoomListEvent>()
|
||||
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<RoomListEvent>()
|
||||
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<RoomListEvent>()
|
||||
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<RoomListEvent>()
|
||||
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<RoomListEvent>()
|
||||
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<RoomListEvent>(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<RoomListEvent>()
|
||||
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<RoomListEvent>()
|
||||
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<RoomListEvent>()
|
||||
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<RoomListEvent>()
|
||||
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<RoomListEvent>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomListView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setRoomListView(
|
||||
state: RoomListState,
|
||||
onRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onSettingsClick: () -> Unit = EnsureNeverCalled(),
|
||||
|
||||
+16
-18
@@ -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<ComponentActivity>()
|
||||
|
||||
@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<SpaceFiltersEvent.Selecting>()
|
||||
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<SpaceFiltersEvent.Selecting>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceFiltersView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setSpaceFiltersView(
|
||||
state: SpaceFiltersState,
|
||||
) {
|
||||
setContent {
|
||||
|
||||
@@ -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)
|
||||
|
||||
+24
-25
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back invoke the expected callback`() {
|
||||
fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(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<DeclineAndBlockEvents>()
|
||||
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<DeclineAndBlockEvents>(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<DeclineAndBlockEvents>()
|
||||
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<DeclineAndBlockEvents>()
|
||||
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<DeclineAndBlockEvents>()
|
||||
rule.setDeclineAndBlockView(
|
||||
setDeclineAndBlockView(
|
||||
aDeclineAndBlockState(
|
||||
reportRoom = true,
|
||||
reportReason = "",
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText("").performTextInput("Spam!")
|
||||
onNodeWithText("").performTextInput("Spam!")
|
||||
eventsRecorder.assertSingle(DeclineAndBlockEvents.UpdateReportReason("Spam!"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDeclineAndBlockView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setDeclineAndBlockView(
|
||||
state: DeclineAndBlockState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
+61
-60
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back invoke the expected callback`() {
|
||||
fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<JoinRoomEvents>(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<JoinRoomEvents>()
|
||||
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<JoinRoomEvents>()
|
||||
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<JoinRoomEvents>()
|
||||
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<JoinRoomEvents>()
|
||||
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<JoinRoomEvents>()
|
||||
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<JoinRoomEvents>()
|
||||
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<JoinRoomEvents>(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<JoinRoomEvents>()
|
||||
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<JoinRoomEvents>()
|
||||
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<JoinRoomEvents>(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<JoinRoomEvents>(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<JoinRoomEvents>()
|
||||
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<JoinRoomEvents>()
|
||||
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<JoinRoomEvents>(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<JoinRoomEvents>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinRoomView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setJoinRoomView(
|
||||
state: JoinRoomState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
onJoinSuccess: () -> Unit = EnsureNeverCalled(),
|
||||
|
||||
+19
-21
@@ -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<ComponentActivity>()
|
||||
|
||||
@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<KnockRequestsBannerEvents>(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<KnockRequestsBannerEvents>(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<KnockRequestsBannerEvents>()
|
||||
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<KnockRequestsBannerEvents>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setKnockRequestsBannerView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setKnockRequestsBannerView(
|
||||
state: KnockRequestsBannerState,
|
||||
onViewRequestsClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
|
||||
+30
-31
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back invoke the expected callback`() {
|
||||
fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>(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<KnockRequestsListEvents>()
|
||||
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<KnockRequestsListEvents>()
|
||||
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<KnockRequestsListEvents>()
|
||||
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<KnockRequestsListEvents>()
|
||||
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<KnockRequestsListEvents>()
|
||||
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<KnockRequestsListEvents>()
|
||||
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<KnockRequestsListEvents>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setKnockRequestsListView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setKnockRequestsListView(
|
||||
state: KnockRequestsListState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
|
||||
@@ -43,7 +43,7 @@ dependencies {
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(projects.libraries.qrcode)
|
||||
implementation(projects.libraries.oidc.api)
|
||||
implementation(projects.libraries.oauth.api)
|
||||
implementation(projects.libraries.uiUtils)
|
||||
implementation(projects.libraries.wellknown.api)
|
||||
implementation(libs.androidx.browser)
|
||||
@@ -56,7 +56,7 @@ dependencies {
|
||||
testImplementation(projects.features.enterprise.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.oidc.test)
|
||||
testImplementation(projects.libraries.oauth.test)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.libraries.wellknown.test)
|
||||
|
||||
+12
-15
@@ -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)
|
||||
}
|
||||
@@ -191,20 +188,20 @@ class LinkNewDeviceFlowNode(
|
||||
|
||||
private fun navigateToError(errorType: ErrorType) {
|
||||
// Map the error to an error screen
|
||||
// TODO Update this mapping
|
||||
val error = when (errorType) {
|
||||
is ErrorType.DeviceIdAlreadyInUse -> ErrorScreenType.UnknownError
|
||||
is ErrorType.InvalidCheckCode -> ErrorScreenType.InsecureChannelDetected
|
||||
is ErrorType.MissingSecretsBackup -> ErrorScreenType.UnknownError
|
||||
is ErrorType.NotFound -> ErrorScreenType.Expired
|
||||
is ErrorType.DeviceNotFound -> ErrorScreenType.UnknownError
|
||||
is ErrorType.Unknown -> ErrorScreenType.UnknownError
|
||||
is ErrorType.UnsupportedProtocol -> ErrorScreenType.UnknownError
|
||||
is ErrorType.Cancelled -> ErrorScreenType.UnknownError
|
||||
is ErrorType.InvalidCheckCode -> ErrorScreenType.Mismatch2Digits
|
||||
is ErrorType.UnsupportedProtocol -> ErrorScreenType.ProtocolNotSupported
|
||||
is ErrorType.Cancelled -> ErrorScreenType.Cancelled
|
||||
is ErrorType.ConnectionInsecure -> ErrorScreenType.InsecureChannelDetected
|
||||
is ErrorType.Expired -> ErrorScreenType.Expired
|
||||
is ErrorType.OtherDeviceAlreadySignedIn -> ErrorScreenType.UnknownError
|
||||
is ErrorType.Expired,
|
||||
is ErrorType.NotFound,
|
||||
is ErrorType.DeviceNotFound -> ErrorScreenType.Expired
|
||||
is ErrorType.OtherDeviceAlreadySignedIn -> ErrorScreenType.OtherDeviceAlreadySignedIn
|
||||
// TODO check if we expect to hit this here or if it should be caught earlier on
|
||||
is ErrorType.UnsupportedQrCodeType -> ErrorScreenType.UnknownError
|
||||
is ErrorType.MissingSecretsBackup,
|
||||
is ErrorType.DeviceIdAlreadyInUse,
|
||||
is ErrorType.Unknown -> ErrorScreenType.UnknownError
|
||||
}
|
||||
// It is OK to push on backstack, since when user leaves the error screen, a new root will be set,
|
||||
// or the whole flow will be popped.
|
||||
|
||||
+3
@@ -20,6 +20,9 @@ sealed interface ErrorScreenType : NodeInputs, Parcelable {
|
||||
@Parcelize
|
||||
data object Expired : ErrorScreenType
|
||||
|
||||
@Parcelize
|
||||
data object OtherDeviceAlreadySignedIn : ErrorScreenType
|
||||
|
||||
@Parcelize
|
||||
data object Mismatch2Digits : ErrorScreenType
|
||||
|
||||
|
||||
+1
@@ -19,5 +19,6 @@ class ErrorScreenTypeProvider : PreviewParameterProvider<ErrorScreenType> {
|
||||
ErrorScreenType.InsecureChannelDetected,
|
||||
ErrorScreenType.SlidingSyncNotAvailable,
|
||||
ErrorScreenType.UnknownError,
|
||||
ErrorScreenType.OtherDeviceAlreadySignedIn,
|
||||
)
|
||||
}
|
||||
|
||||
+27
-5
@@ -47,17 +47,26 @@ fun ErrorView(
|
||||
) {
|
||||
val appName = LocalBuildMeta.current.applicationName
|
||||
BackHandler(onBack = onCancel)
|
||||
val iconStyle = when (errorScreenType) {
|
||||
ErrorScreenType.OtherDeviceAlreadySignedIn -> BigIcon.Style.SuccessSolid
|
||||
else -> BigIcon.Style.AlertSolid
|
||||
}
|
||||
FlowStepPage(
|
||||
modifier = modifier,
|
||||
iconStyle = BigIcon.Style.AlertSolid,
|
||||
iconStyle = iconStyle,
|
||||
title = titleText(errorScreenType, appName),
|
||||
subTitle = subtitleText(errorScreenType, appName),
|
||||
content = { Content(errorScreenType) },
|
||||
buttons = {
|
||||
Buttons(
|
||||
onRetry = onRetry,
|
||||
onCancel = onCancel,
|
||||
)
|
||||
when (errorScreenType) {
|
||||
ErrorScreenType.OtherDeviceAlreadySignedIn -> DoneButton(
|
||||
onDone = onCancel,
|
||||
)
|
||||
else -> Buttons(
|
||||
onRetry = onRetry,
|
||||
onCancel = onCancel,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -72,6 +81,7 @@ private fun titleText(errorScreenType: ErrorScreenType, appName: String) = when
|
||||
ErrorScreenType.Mismatch2Digits -> stringResource(id = R.string.screen_link_new_device_wrong_number_title)
|
||||
ErrorScreenType.SlidingSyncNotAvailable -> stringResource(id = R.string.screen_qr_code_login_error_sliding_sync_not_supported_title, appName)
|
||||
is ErrorScreenType.UnknownError -> stringResource(CommonStrings.common_something_went_wrong)
|
||||
ErrorScreenType.OtherDeviceAlreadySignedIn -> stringResource(R.string.screen_qr_code_login_error_device_already_signed_in_title)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -84,6 +94,7 @@ private fun subtitleText(errorScreenType: ErrorScreenType, appName: String) = wh
|
||||
ErrorScreenType.InsecureChannelDetected -> stringResource(id = R.string.screen_qr_code_login_connection_note_secure_state_description)
|
||||
ErrorScreenType.SlidingSyncNotAvailable -> stringResource(id = R.string.screen_qr_code_login_error_sliding_sync_not_supported_subtitle, appName)
|
||||
is ErrorScreenType.UnknownError -> stringResource(R.string.screen_qr_code_login_unknown_error_description)
|
||||
ErrorScreenType.OtherDeviceAlreadySignedIn -> stringResource(R.string.screen_qr_code_login_error_device_already_signed_in_subtitle)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -124,6 +135,17 @@ private fun Content(errorScreenType: ErrorScreenType) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DoneButton(
|
||||
onDone: () -> Unit,
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(CommonStrings.action_done),
|
||||
onClick = onDone,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Buttons(
|
||||
onRetry: () -> Unit,
|
||||
|
||||
+17
-19
@@ -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<ComponentActivity>()
|
||||
|
||||
@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<DesktopNoticeEvent>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setView(
|
||||
state: DesktopNoticeState,
|
||||
onBackClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onReadyToScanClick: () -> Unit = EnsureNeverCalled(),
|
||||
|
||||
+15
-17
@@ -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<ComponentActivity>()
|
||||
|
||||
@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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setErrorView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setErrorView(
|
||||
onRetry: () -> Unit = EnsureNeverCalled(),
|
||||
onCancel: () -> Unit = EnsureNeverCalled(),
|
||||
errorScreenType: ErrorScreenType = ErrorScreenType.UnknownError,
|
||||
|
||||
+19
-21
@@ -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<ComponentActivity>()
|
||||
|
||||
@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<EnterNumberEvent>()
|
||||
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<EnterNumberEvent>(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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setView(
|
||||
state: EnterNumberState,
|
||||
onBackClicked: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
|
||||
+9
-11
@@ -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<ComponentActivity>()
|
||||
|
||||
@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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setView(
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
|
||||
+18
-20
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `on back pressed - calls the onRetry callback`() {
|
||||
fun `on back pressed - calls the onRetry callback`() = runAndroidComposeUiTest {
|
||||
val eventRecorder = EventsRecorder<LinkNewDeviceRootEvent>(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<LinkNewDeviceRootEvent>(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<LinkNewDeviceRootEvent>()
|
||||
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<LinkNewDeviceRootEvent>(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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setLinkNewDeviceRootView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setLinkNewDeviceRootView(
|
||||
state: LinkNewDeviceRootState = aLinkNewDeviceRootState(),
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
onLinkDesktopDeviceClick: () -> Unit = EnsureNeverCalled(),
|
||||
|
||||
+12
-14
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `on back pressed - calls the expected callback`() {
|
||||
fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventRecorder = EventsRecorder<ScanQrCodeEvent>(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<ScanQrCodeEvent>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setView(
|
||||
state: ScanQrCodeState = aScanQrCodeState(),
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
|
||||
+30
-31
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `test back action`() {
|
||||
fun `test back action`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<ShareLocationEvent>(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<ShareLocationEvent>()
|
||||
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<ShareLocationEvent>()
|
||||
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<ShareLocationEvent>()
|
||||
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<ShareLocationEvent>()
|
||||
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<ShareLocationEvent>()
|
||||
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<ShareLocationEvent>()
|
||||
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<ShareLocationEvent>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setShareLocationView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setShareLocationView(
|
||||
state: ShareLocationState,
|
||||
navigateUp: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
|
||||
+28
-29
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `test back action`() {
|
||||
fun `test back action`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<ShowLocationEvent>(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<ShowLocationEvent>()
|
||||
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<ShowLocationEvent>()
|
||||
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<ShowLocationEvent>()
|
||||
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<ShowLocationEvent>()
|
||||
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<ShowLocationEvent>()
|
||||
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<ShowLocationEvent>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setShowLocationView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setShowLocationView(
|
||||
state: ShowLocationState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+15
-18
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on a number emits the expected event`() {
|
||||
fun `clicking on a number emits the expected event`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PinKeypadModel>()
|
||||
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<PinKeypadModel>()
|
||||
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<PinKeypadModel>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPinKeyPad(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setPinKeyPad(
|
||||
onClick: (PinKeypadModel) -> Unit = EnsureNeverCalledWithParam(),
|
||||
) {
|
||||
setContent {
|
||||
|
||||
@@ -69,7 +69,7 @@ dependencies {
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(projects.libraries.qrcode)
|
||||
implementation(projects.libraries.oidc.api)
|
||||
implementation(projects.libraries.oauth.api)
|
||||
implementation(projects.libraries.uiUtils)
|
||||
implementation(projects.libraries.wellknown.api)
|
||||
implementation(libs.androidx.browser)
|
||||
@@ -83,7 +83,7 @@ dependencies {
|
||||
testImplementation(projects.features.preferences.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.oidc.test)
|
||||
testImplementation(projects.libraries.oauth.test)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.libraries.wellknown.test)
|
||||
|
||||
+15
-15
@@ -50,9 +50,9 @@ import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
import io.element.android.libraries.oauth.api.OAuthAction
|
||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -64,7 +64,7 @@ class LoginFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val accountProviderDataSource: AccountProviderDataSource,
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
private val oAuthActionFlow: OAuthActionFlow,
|
||||
@AppCoroutineScope
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
private val elementClassicConnection: ElementClassicConnection,
|
||||
@@ -100,7 +100,7 @@ class LoginFlowNode(
|
||||
// by pressing back or by closing the Custom Chrome Tab.
|
||||
lifecycleScope.launch {
|
||||
delay(5000)
|
||||
oidcActionFlow.post(OidcAction.GoBack(toUnblock = true))
|
||||
oAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,8 +161,8 @@ class LoginFlowNode(
|
||||
backstack.push(NavTarget.LoginPassword())
|
||||
}
|
||||
|
||||
override fun navigateToOidc(oidcDetails: OidcDetails) {
|
||||
navigateToMas(oidcDetails)
|
||||
override fun navigateToOAuth(oAuthDetails: OAuthDetails) {
|
||||
navigateToMas(oAuthDetails)
|
||||
}
|
||||
|
||||
override fun navigateToCreateAccount(url: String) {
|
||||
@@ -197,8 +197,8 @@ class LoginFlowNode(
|
||||
callback.navigateToBugReport()
|
||||
}
|
||||
|
||||
override fun navigateToOidc(oidcDetails: OidcDetails) {
|
||||
navigateToMas(oidcDetails)
|
||||
override fun navigateToOAuth(oAuthDetails: OAuthDetails) {
|
||||
navigateToMas(oAuthDetails)
|
||||
}
|
||||
|
||||
override fun navigateToCreateAccount(url: String) {
|
||||
@@ -243,8 +243,8 @@ class LoginFlowNode(
|
||||
}
|
||||
NavTarget.ChooseAccountProvider -> {
|
||||
val callback = object : ChooseAccountProviderNode.Callback {
|
||||
override fun navigateToOidc(oidcDetails: OidcDetails) {
|
||||
navigateToMas(oidcDetails)
|
||||
override fun navigateToOAuth(oAuthDetails: OAuthDetails) {
|
||||
navigateToMas(oAuthDetails)
|
||||
}
|
||||
|
||||
override fun navigateToCreateAccount(url: String) {
|
||||
@@ -270,8 +270,8 @@ class LoginFlowNode(
|
||||
isAccountCreation = navTarget.isAccountCreation,
|
||||
)
|
||||
val callback = object : ConfirmAccountProviderNode.Callback {
|
||||
override fun navigateToOidc(oidcDetails: OidcDetails) {
|
||||
navigateToMas(oidcDetails)
|
||||
override fun navigateToOAuth(oAuthDetails: OAuthDetails) {
|
||||
navigateToMas(oAuthDetails)
|
||||
}
|
||||
|
||||
override fun navigateToCreateAccount(url: String) {
|
||||
@@ -333,10 +333,10 @@ class LoginFlowNode(
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToMas(oidcDetails: OidcDetails) {
|
||||
private fun navigateToMas(oAuthDetails: OAuthDetails) {
|
||||
activity?.let {
|
||||
externalAppStarted = true
|
||||
it.openUrlInChromeCustomTab(null, darkTheme, oidcDetails.url)
|
||||
it.openUrlInChromeCustomTab(null, darkTheme, oAuthDetails.url)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ sealed class ChangeServerError : Exception() {
|
||||
// AccountAlreadyLoggedIn error should not happen at this point
|
||||
is AuthenticationException.AccountAlreadyLoggedIn -> Error(messageStr = error.message)
|
||||
is AuthenticationException.Generic -> Error(messageStr = error.message)
|
||||
is AuthenticationException.Oidc -> Error(messageStr = error.message)
|
||||
is AuthenticationException.OAuth -> Error(messageStr = error.message)
|
||||
}
|
||||
}
|
||||
is AccountProviderAccessException.NeedElementProException -> NeedElementPro(
|
||||
|
||||
+19
-19
@@ -23,9 +23,9 @@ import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationR
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.auth.OidcPrompt
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthPrompt
|
||||
import io.element.android.libraries.oauth.api.OAuthAction
|
||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||
|
||||
/**
|
||||
* This class is responsible for managing the login flow, including handling OIDC actions and
|
||||
@@ -35,7 +35,7 @@ import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||
*/
|
||||
@Inject
|
||||
class LoginHelper(
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
private val oAuthActionFlow: OAuthActionFlow,
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever,
|
||||
) {
|
||||
@@ -44,9 +44,9 @@ class LoginHelper(
|
||||
@Composable
|
||||
fun collectLoginMode(): State<AsyncData<LoginMode>> {
|
||||
LaunchedEffect(Unit) {
|
||||
oidcActionFlow.collect { oidcAction ->
|
||||
if (oidcAction != null) {
|
||||
onOidcAction(oidcAction)
|
||||
oAuthActionFlow.collect { oAuthAction ->
|
||||
if (oAuthAction != null) {
|
||||
onOAuthAction(oAuthAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,11 +73,11 @@ class LoginHelper(
|
||||
throw it
|
||||
}
|
||||
}.map { matrixHomeServerDetails ->
|
||||
if (matrixHomeServerDetails.supportsOidcLogin) {
|
||||
if (matrixHomeServerDetails.supportsOAuthLogin) {
|
||||
// Retrieve the details right now
|
||||
val oidcPrompt = if (isAccountCreation) OidcPrompt.Create else OidcPrompt.Login
|
||||
LoginMode.Oidc(
|
||||
authenticationService.getOidcUrl(prompt = oidcPrompt, loginHint = loginHint).getOrThrow()
|
||||
val oAuthPrompt = if (isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login
|
||||
LoginMode.OAuth(
|
||||
authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = loginHint).getOrThrow()
|
||||
)
|
||||
} else if (isAccountCreation) {
|
||||
val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl)
|
||||
@@ -99,16 +99,16 @@ class LoginHelper(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun onOidcAction(oidcAction: OidcAction) {
|
||||
if (oidcAction is OidcAction.GoBack && oidcAction.toUnblock && loginModeState.value !is AsyncData.Loading) {
|
||||
private suspend fun onOAuthAction(oAuthAction: OAuthAction) {
|
||||
if (oAuthAction is OAuthAction.GoBack && oAuthAction.toUnblock && loginModeState.value !is AsyncData.Loading) {
|
||||
// Ignore GoBack action if the current state is not Loading. This GoBack action is coming from LoginFlowNode.
|
||||
// This can happen if there is an error, for instance attempt to login again on the same account.
|
||||
return
|
||||
}
|
||||
loginModeState.value = AsyncData.Loading()
|
||||
when (oidcAction) {
|
||||
is OidcAction.GoBack -> {
|
||||
authenticationService.cancelOidcLogin()
|
||||
when (oAuthAction) {
|
||||
is OAuthAction.GoBack -> {
|
||||
authenticationService.cancelOAuthLogin()
|
||||
.onSuccess {
|
||||
loginModeState.value = AsyncData.Uninitialized
|
||||
}
|
||||
@@ -116,13 +116,13 @@ class LoginHelper(
|
||||
loginModeState.value = AsyncData.Failure(failure)
|
||||
}
|
||||
}
|
||||
is OidcAction.Success -> {
|
||||
authenticationService.loginWithOidc(oidcAction.url)
|
||||
is OAuthAction.Success -> {
|
||||
authenticationService.loginWithOAuth(oAuthAction.url)
|
||||
.onFailure { failure ->
|
||||
loginModeState.value = AsyncData.Failure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
oidcActionFlow.reset()
|
||||
oAuthActionFlow.reset()
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,10 +8,10 @@
|
||||
|
||||
package io.element.android.features.login.impl.login
|
||||
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
|
||||
sealed interface LoginMode {
|
||||
data object PasswordLogin : LoginMode
|
||||
data class Oidc(val oidcDetails: OidcDetails) : LoginMode
|
||||
data class OAuth(val oAuthDetails: OAuthDetails) : LoginMode
|
||||
data class AccountCreation(val url: String) : LoginMode
|
||||
}
|
||||
|
||||
+4
-4
@@ -24,7 +24,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
|
||||
import io.element.android.libraries.matrix.api.auth.AuthenticationException
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
@@ -32,7 +32,7 @@ fun LoginModeView(
|
||||
loginMode: AsyncData<LoginMode>,
|
||||
onClearError: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onOidcDetails: (OidcDetails) -> Unit,
|
||||
onOAuthDetails: (OAuthDetails) -> Unit,
|
||||
onNeedLoginPassword: () -> Unit,
|
||||
onCreateAccountContinue: (url: String) -> Unit
|
||||
) {
|
||||
@@ -118,7 +118,7 @@ fun LoginModeView(
|
||||
is AsyncData.Loading -> Unit // The Continue button shows the loading state
|
||||
is AsyncData.Success -> {
|
||||
when (val loginModeData = loginMode.data) {
|
||||
is LoginMode.Oidc -> onOidcDetails(loginModeData.oidcDetails)
|
||||
is LoginMode.OAuth -> onOAuthDetails(loginModeData.oAuthDetails)
|
||||
LoginMode.PasswordLogin -> onNeedLoginPassword()
|
||||
is LoginMode.AccountCreation -> onCreateAccountContinue(loginModeData.url)
|
||||
}
|
||||
@@ -137,7 +137,7 @@ internal fun LoginModeViewPreview(@PreviewParameter(LoginModeViewErrorProvider::
|
||||
loginMode = AsyncData.Failure(error),
|
||||
onClearError = {},
|
||||
onLearnMoreClick = {},
|
||||
onOidcDetails = {},
|
||||
onOAuthDetails = {},
|
||||
onNeedLoginPassword = {},
|
||||
onCreateAccountContinue = {}
|
||||
)
|
||||
|
||||
+2
-2
@@ -135,8 +135,8 @@ class QrCodeLoginFlowNode(
|
||||
is QrLoginException.SlidingSyncNotAvailable -> {
|
||||
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.SlidingSyncNotAvailable))
|
||||
}
|
||||
is QrLoginException.OidcMetadataInvalid -> {
|
||||
Timber.e(error, "OIDC metadata is invalid")
|
||||
is QrLoginException.OAuthMetadataInvalid -> {
|
||||
Timber.e(error, "OAuth metadata is invalid")
|
||||
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.UnknownError))
|
||||
}
|
||||
QrLoginException.CheckCodeAlreadySent,
|
||||
|
||||
+3
-3
@@ -20,7 +20,7 @@ import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.login.impl.util.openLearnMorePage
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@AssistedInject
|
||||
@@ -31,7 +31,7 @@ class ChooseAccountProviderNode(
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun navigateToLoginPassword()
|
||||
fun navigateToOidc(oidcDetails: OidcDetails)
|
||||
fun navigateToOAuth(oAuthDetails: OAuthDetails)
|
||||
fun navigateToCreateAccount(url: String)
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class ChooseAccountProviderNode(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onBackClick = ::navigateUp,
|
||||
onOidcDetails = callback::navigateToOidc,
|
||||
onOAuthDetails = callback::navigateToOAuth,
|
||||
onNeedLoginPassword = callback::navigateToLoginPassword,
|
||||
onLearnMoreClick = { openLearnMorePage(context) },
|
||||
onCreateAccountContinue = callback::navigateToCreateAccount,
|
||||
|
||||
+4
-4
@@ -43,14 +43,14 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun ChooseAccountProviderView(
|
||||
state: ChooseAccountProviderState,
|
||||
onBackClick: () -> Unit,
|
||||
onOidcDetails: (OidcDetails) -> Unit,
|
||||
onOAuthDetails: (OAuthDetails) -> Unit,
|
||||
onNeedLoginPassword: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onCreateAccountContinue: (url: String) -> Unit,
|
||||
@@ -129,7 +129,7 @@ fun ChooseAccountProviderView(
|
||||
state.eventSink(ChooseAccountProviderEvents.ClearError)
|
||||
},
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onOidcDetails = onOidcDetails,
|
||||
onOAuthDetails = onOAuthDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
@@ -144,7 +144,7 @@ internal fun ChooseAccountProviderViewPreview(@PreviewParameter(ChooseAccountPro
|
||||
state = state,
|
||||
onBackClick = { },
|
||||
onLearnMoreClick = { },
|
||||
onOidcDetails = { },
|
||||
onOAuthDetails = { },
|
||||
onNeedLoginPassword = { },
|
||||
onCreateAccountContinue = { },
|
||||
)
|
||||
|
||||
+4
-4
@@ -31,7 +31,7 @@ import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.appyx.rememberFaderOrSliderTransitionHandler
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -54,7 +54,7 @@ class ClassicFlowNode(
|
||||
interface Callback : Plugin {
|
||||
fun navigateToOnBoarding(allowBackNavigation: Boolean)
|
||||
fun navigateToLoginPassword()
|
||||
fun navigateToOidc(oidcDetails: OidcDetails)
|
||||
fun navigateToOAuth(oAuthDetails: OAuthDetails)
|
||||
fun navigateToCreateAccount(url: String)
|
||||
}
|
||||
|
||||
@@ -111,8 +111,8 @@ class ClassicFlowNode(
|
||||
callback.navigateToLoginPassword()
|
||||
}
|
||||
|
||||
override fun navigateToOidc(oidcDetails: OidcDetails) {
|
||||
callback.navigateToOidc(oidcDetails)
|
||||
override fun navigateToOAuth(oAuthDetails: OAuthDetails) {
|
||||
callback.navigateToOAuth(oAuthDetails)
|
||||
}
|
||||
|
||||
override fun navigateToCreateAccount(url: String) {
|
||||
|
||||
+3
-3
@@ -21,7 +21,7 @@ import io.element.android.features.login.impl.util.openLearnMorePage
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@@ -35,7 +35,7 @@ class LoginWithClassicNode(
|
||||
interface Callback : Plugin {
|
||||
fun navigateToOtherOptions()
|
||||
fun navigateToLoginPassword()
|
||||
fun navigateToOidc(oidcDetails: OidcDetails)
|
||||
fun navigateToOAuth(oAuthDetails: OAuthDetails)
|
||||
fun navigateToCreateAccount(url: String)
|
||||
fun navigateToMissingKeyBackup()
|
||||
}
|
||||
@@ -60,7 +60,7 @@ class LoginWithClassicNode(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onOtherOptionsClick = callback::navigateToOtherOptions,
|
||||
onOidcDetails = callback::navigateToOidc,
|
||||
onOAuthDetails = callback::navigateToOAuth,
|
||||
onNeedLoginPassword = callback::navigateToLoginPassword,
|
||||
onLearnMoreClick = { openLearnMorePage(context) },
|
||||
onCreateAccountContinue = callback::navigateToCreateAccount,
|
||||
|
||||
+4
-4
@@ -49,7 +49,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@@ -59,7 +59,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||
fun LoginWithClassicView(
|
||||
state: LoginWithClassicState,
|
||||
onOtherOptionsClick: () -> Unit,
|
||||
onOidcDetails: (OidcDetails) -> Unit,
|
||||
onOAuthDetails: (OAuthDetails) -> Unit,
|
||||
onNeedLoginPassword: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onCreateAccountContinue: (url: String) -> Unit,
|
||||
@@ -200,7 +200,7 @@ fun LoginWithClassicView(
|
||||
state.eventSink(LoginWithClassicEvent.ClearError)
|
||||
},
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onOidcDetails = onOidcDetails,
|
||||
onOAuthDetails = onOAuthDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
@@ -212,7 +212,7 @@ internal fun LoginWithClassicViewPreview(@PreviewParameter(LoginWithClassicState
|
||||
LoginWithClassicView(
|
||||
state = state,
|
||||
onOtherOptionsClick = {},
|
||||
onOidcDetails = {},
|
||||
onOAuthDetails = {},
|
||||
onNeedLoginPassword = {},
|
||||
onLearnMoreClick = {},
|
||||
onCreateAccountContinue = {},
|
||||
|
||||
+3
-3
@@ -22,7 +22,7 @@ import io.element.android.features.login.impl.util.openLearnMorePage
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@AssistedInject
|
||||
@@ -44,7 +44,7 @@ class ConfirmAccountProviderNode(
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun navigateToLoginPassword()
|
||||
fun navigateToOidc(oidcDetails: OidcDetails)
|
||||
fun navigateToOAuth(oAuthDetails: OAuthDetails)
|
||||
fun navigateToCreateAccount(url: String)
|
||||
fun navigateToChangeAccountProvider()
|
||||
}
|
||||
@@ -58,7 +58,7 @@ class ConfirmAccountProviderNode(
|
||||
ConfirmAccountProviderView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onOidcDetails = callback::navigateToOidc,
|
||||
onOAuthDetails = callback::navigateToOAuth,
|
||||
onNeedLoginPassword = callback::navigateToLoginPassword,
|
||||
onCreateAccountContinue = callback::navigateToCreateAccount,
|
||||
onChange = callback::navigateToChangeAccountProvider,
|
||||
|
||||
+4
-4
@@ -30,7 +30,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@@ -38,7 +38,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@Composable
|
||||
fun ConfirmAccountProviderView(
|
||||
state: ConfirmAccountProviderState,
|
||||
onOidcDetails: (OidcDetails) -> Unit,
|
||||
onOAuthDetails: (OAuthDetails) -> Unit,
|
||||
onNeedLoginPassword: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onCreateAccountContinue: (url: String) -> Unit,
|
||||
@@ -103,7 +103,7 @@ fun ConfirmAccountProviderView(
|
||||
eventSink(ConfirmAccountProviderEvents.ClearError)
|
||||
},
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onOidcDetails = onOidcDetails,
|
||||
onOAuthDetails = onOAuthDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
@@ -117,7 +117,7 @@ internal fun ConfirmAccountProviderViewPreview(
|
||||
) = ElementPreview {
|
||||
ConfirmAccountProviderView(
|
||||
state = state,
|
||||
onOidcDetails = {},
|
||||
onOAuthDetails = {},
|
||||
onNeedLoginPassword = {},
|
||||
onCreateAccountContinue = {},
|
||||
onLearnMoreClick = {},
|
||||
|
||||
+3
-3
@@ -22,7 +22,7 @@ import io.element.android.features.login.impl.util.openLearnMorePage
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@AssistedInject
|
||||
@@ -40,7 +40,7 @@ class OnBoardingNode(
|
||||
fun navigateToQrCode()
|
||||
fun navigateToBugReport()
|
||||
fun navigateToLoginPassword()
|
||||
fun navigateToOidc(oidcDetails: OidcDetails)
|
||||
fun navigateToOAuth(oAuthDetails: OAuthDetails)
|
||||
fun navigateToCreateAccount(url: String)
|
||||
fun navigateToDeveloperSettings()
|
||||
fun onDone()
|
||||
@@ -71,7 +71,7 @@ class OnBoardingNode(
|
||||
onCreateAccount = callback::navigateToSignUpFlow,
|
||||
onSignInWithQrCode = callback::navigateToQrCode,
|
||||
onReportProblem = callback::navigateToBugReport,
|
||||
onOidcDetails = callback::navigateToOidc,
|
||||
onOAuthDetails = callback::navigateToOAuth,
|
||||
onNeedLoginPassword = callback::navigateToLoginPassword,
|
||||
onLearnMoreClick = { openLearnMorePage(context) },
|
||||
onCreateAccountContinue = callback::navigateToCreateAccount,
|
||||
|
||||
+4
-4
@@ -50,7 +50,7 @@ import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@@ -68,7 +68,7 @@ fun OnBoardingView(
|
||||
onSignInWithQrCode: () -> Unit,
|
||||
onSignIn: (mustChooseAccountProvider: Boolean) -> Unit,
|
||||
onCreateAccount: () -> Unit,
|
||||
onOidcDetails: (OidcDetails) -> Unit,
|
||||
onOAuthDetails: (OAuthDetails) -> Unit,
|
||||
onNeedLoginPassword: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onCreateAccountContinue: (url: String) -> Unit,
|
||||
@@ -82,7 +82,7 @@ fun OnBoardingView(
|
||||
state.eventSink(OnBoardingEvents.ClearError)
|
||||
},
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onOidcDetails = onOidcDetails,
|
||||
onOAuthDetails = onOAuthDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
@@ -354,7 +354,7 @@ internal fun OnBoardingViewPreview(
|
||||
onSignIn = {},
|
||||
onCreateAccount = {},
|
||||
onReportProblem = {},
|
||||
onOidcDetails = {},
|
||||
onOAuthDetails = {},
|
||||
onNeedLoginPassword = {},
|
||||
onLearnMoreClick = {},
|
||||
onCreateAccountContinue = {},
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@ import io.element.android.features.login.api.LoginEntryPoint
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.classic.FakeElementClassicConnection
|
||||
import io.element.android.features.preferences.test.FakePreferencesEntryPoint
|
||||
import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow
|
||||
import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -39,7 +39,7 @@ class DefaultLoginEntryPointTest {
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
||||
oidcActionFlow = FakeOidcActionFlow(),
|
||||
oAuthActionFlow = FakeOAuthActionFlow(),
|
||||
appCoroutineScope = backgroundScope,
|
||||
elementClassicConnection = FakeElementClassicConnection(),
|
||||
preferencesEntryPoint = FakePreferencesEntryPoint(),
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ class ChangeServerPresenterTest {
|
||||
fun `present - change server ok`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
createPresenter(
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ class QrCodeLoginFlowNodeTest {
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.ConnectionInsecure)
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.InsecureChannelDetected))
|
||||
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.OidcMetadataInvalid)
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.OAuthMetadataInvalid)
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.UnknownError))
|
||||
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Unknown)
|
||||
|
||||
+18
-20
@@ -6,17 +6,20 @@
|
||||
* 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
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back invokes the expected callback`() {
|
||||
fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventSink = EventsRecorder<ChooseAccountProviderEvents>(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<ChooseAccountProviderEvents>()
|
||||
rule.setChooseAccountProviderView(
|
||||
setChooseAccountProviderView(
|
||||
state = aChooseAccountProviderState(
|
||||
accountProviders = listOf(
|
||||
ChooseAccountProviderPresenterTest.accountProvider1,
|
||||
@@ -64,27 +62,27 @@ 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<ChooseAccountProviderEvents>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setChooseAccountProviderView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setChooseAccountProviderView(
|
||||
state: ChooseAccountProviderState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onOAuthDetails: (OAuthDetails) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onNeedLoginPassword: () -> Unit = EnsureNeverCalled(),
|
||||
onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
|
||||
onCreateAccountContinue: (url: String) -> Unit = EnsureNeverCalledWithParam(),
|
||||
@@ -93,7 +91,7 @@ class ChooseAccountProviderViewTest {
|
||||
ChooseAccountProviderView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
onOidcDetails = onOidcDetails,
|
||||
onOAuthDetails = onOAuthDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
|
||||
+44
-44
@@ -22,9 +22,9 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||
import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow
|
||||
import io.element.android.libraries.oauth.api.OAuthAction
|
||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||
import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -74,7 +74,7 @@ class ConfirmAccountProviderPresenterTest {
|
||||
fun `present - continue oidc`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
@@ -89,21 +89,21 @@ class ConfirmAccountProviderPresenterTest {
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
|
||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - oidc - cancel with failure`() = runTest {
|
||||
fun `present - OAuth - cancel with failure`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
val defaultOidcActionFlow = FakeOidcActionFlow()
|
||||
val defaultOAuthActionFlow = FakeOAuthActionFlow()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
defaultOidcActionFlow = defaultOidcActionFlow,
|
||||
defaultOAuthActionFlow = defaultOAuthActionFlow,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
@@ -114,25 +114,25 @@ class ConfirmAccountProviderPresenterTest {
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
|
||||
authenticationService.givenOidcCancelError(AN_EXCEPTION)
|
||||
defaultOidcActionFlow.post(OidcAction.GoBack())
|
||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
authenticationService.givenOAuthCancelError(AN_EXCEPTION)
|
||||
defaultOAuthActionFlow.post(OAuthAction.GoBack())
|
||||
val cancelFailureState = awaitItem()
|
||||
assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - oidc - cancel with success`() = runTest {
|
||||
fun `present - OAuth - cancel with success`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
val defaultOidcActionFlow = FakeOidcActionFlow()
|
||||
val defaultOAuthActionFlow = FakeOAuthActionFlow()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
defaultOidcActionFlow = defaultOidcActionFlow,
|
||||
defaultOAuthActionFlow = defaultOAuthActionFlow,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
@@ -143,24 +143,24 @@ class ConfirmAccountProviderPresenterTest {
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
|
||||
defaultOidcActionFlow.post(OidcAction.GoBack())
|
||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
defaultOAuthActionFlow.post(OAuthAction.GoBack())
|
||||
val cancelFinalState = awaitItem()
|
||||
assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - oidc - cancel to unblock`() = runTest {
|
||||
fun `present - OAuth - cancel to unblock`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
val defaultOidcActionFlow = FakeOidcActionFlow()
|
||||
val defaultOAuthActionFlow = FakeOAuthActionFlow()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
defaultOidcActionFlow = defaultOidcActionFlow,
|
||||
defaultOAuthActionFlow = defaultOAuthActionFlow,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
@@ -168,23 +168,23 @@ class ConfirmAccountProviderPresenterTest {
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.submitEnabled).isTrue()
|
||||
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||
defaultOidcActionFlow.post(OidcAction.GoBack(toUnblock = true))
|
||||
defaultOAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true))
|
||||
val cancelFinalState = awaitItem()
|
||||
assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - oidc - success with failure`() = runTest {
|
||||
fun `present - OAuth - success with failure`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
val defaultOidcActionFlow = FakeOidcActionFlow()
|
||||
val defaultOAuthActionFlow = FakeOAuthActionFlow()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
defaultOidcActionFlow = defaultOidcActionFlow,
|
||||
defaultOAuthActionFlow = defaultOAuthActionFlow,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
@@ -195,9 +195,9 @@ class ConfirmAccountProviderPresenterTest {
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
|
||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
authenticationService.givenLoginError(AN_EXCEPTION)
|
||||
defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
|
||||
defaultOAuthActionFlow.post(OAuthAction.Success("aUrl"))
|
||||
val cancelLoadingState = awaitItem()
|
||||
assertThat(cancelLoadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val cancelFailureState = awaitItem()
|
||||
@@ -206,16 +206,16 @@ class ConfirmAccountProviderPresenterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - oidc - success with success`() = runTest {
|
||||
fun `present - OAuth - success with success`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
val defaultOidcActionFlow = FakeOidcActionFlow()
|
||||
val defaultOidcActionFlow = FakeOAuthActionFlow()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
defaultOidcActionFlow = defaultOidcActionFlow,
|
||||
defaultOAuthActionFlow = defaultOidcActionFlow,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
@@ -226,8 +226,8 @@ class ConfirmAccountProviderPresenterTest {
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
|
||||
defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
|
||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
defaultOidcActionFlow.post(OAuthAction.Success("aUrl"))
|
||||
val successSuccessState = awaitItem()
|
||||
assertThat(successSuccessState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||
}
|
||||
@@ -311,10 +311,10 @@ class ConfirmAccountProviderPresenterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - confirm account creation with oidc is successful`() = runTest {
|
||||
fun `present - confirm account creation with OAuth is successful`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
@@ -327,16 +327,16 @@ class ConfirmAccountProviderPresenterTest {
|
||||
skipItems(1) // Loading
|
||||
val submittedState = awaitItem()
|
||||
assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
|
||||
assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - confirm account creation with oidc and url continues with oidc`() = runTest {
|
||||
fun `present - confirm account creation with OAuth and url continues with OAuth`() = runTest {
|
||||
val aUrl = "aUrl"
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
@@ -350,12 +350,12 @@ class ConfirmAccountProviderPresenterTest {
|
||||
skipItems(1) // Loading
|
||||
val submittedState = awaitItem()
|
||||
assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
|
||||
assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - confirm account creation without oidc and with url continuing with url`() = runTest {
|
||||
fun `present - confirm account creation without OAuth and with url continuing with url`() = runTest {
|
||||
val aUrl = "aUrl"
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
@@ -380,14 +380,14 @@ class ConfirmAccountProviderPresenterTest {
|
||||
params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
|
||||
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
||||
matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
||||
defaultOidcActionFlow: OidcActionFlow = FakeOidcActionFlow(),
|
||||
defaultOAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(),
|
||||
webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(),
|
||||
) = ConfirmAccountProviderPresenter(
|
||||
params = params,
|
||||
accountProviderDataSource = accountProviderDataSource,
|
||||
loginHelper = createLoginHelper(
|
||||
authenticationService = matrixAuthenticationService,
|
||||
oidcActionFlow = defaultOidcActionFlow,
|
||||
oAuthActionFlow = defaultOAuthActionFlow,
|
||||
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever,
|
||||
),
|
||||
)
|
||||
|
||||
+48
-49
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back invoke back callback`() {
|
||||
fun `clicking on back invoke back callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<LoginPasswordEvents>(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<LoginPasswordEvents>()
|
||||
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<LoginPasswordEvents>()
|
||||
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<LoginPasswordEvents>()
|
||||
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<LoginPasswordEvents>()
|
||||
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<LoginPasswordEvents>(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<LoginPasswordEvents>(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<LoginPasswordEvents>(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<LoginPasswordEvents>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setLoginPasswordView(
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setLoginPasswordView(
|
||||
state: LoginPasswordState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
|
||||
+4
-4
@@ -31,8 +31,8 @@ import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
|
||||
import io.element.android.libraries.matrix.test.A_LOGIN_HINT
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||
import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow
|
||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||
import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
@@ -312,11 +312,11 @@ private fun createPresenter(
|
||||
)
|
||||
|
||||
fun createLoginHelper(
|
||||
oidcActionFlow: OidcActionFlow = FakeOidcActionFlow(),
|
||||
oAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(),
|
||||
authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
||||
webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(),
|
||||
): LoginHelper = LoginHelper(
|
||||
oidcActionFlow = oidcActionFlow,
|
||||
oAuthActionFlow = oAuthActionFlow,
|
||||
authenticationService = authenticationService,
|
||||
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever,
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user