mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +00:00
Merge pull request #7488 from element-hq/feature/bma/fixColdStartDeepLink
Fix cold start with a mobile deep link
This commit is contained in:
@@ -61,17 +61,23 @@ dependencies {
|
|||||||
testCommonDependencies(libs)
|
testCommonDependencies(libs)
|
||||||
testImplementation(projects.features.login.test)
|
testImplementation(projects.features.login.test)
|
||||||
testImplementation(projects.features.share.test)
|
testImplementation(projects.features.share.test)
|
||||||
|
testImplementation(projects.libraries.accountselect.test)
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.libraries.matrixmedia.test)
|
||||||
testImplementation(projects.libraries.oauth.test)
|
testImplementation(projects.libraries.oauth.test)
|
||||||
testImplementation(projects.libraries.permissions.test)
|
testImplementation(projects.libraries.permissions.test)
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.libraries.pushproviders.test)
|
testImplementation(projects.libraries.pushproviders.test)
|
||||||
|
testImplementation(projects.libraries.sessionStorage.test)
|
||||||
|
testImplementation(projects.features.announcement.test)
|
||||||
testImplementation(projects.features.forward.test)
|
testImplementation(projects.features.forward.test)
|
||||||
testImplementation(projects.features.messages.test)
|
testImplementation(projects.features.messages.test)
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
|
testImplementation(projects.features.preferences.test)
|
||||||
testImplementation(projects.features.rageshake.test)
|
testImplementation(projects.features.rageshake.test)
|
||||||
|
testImplementation(projects.features.signedout.test)
|
||||||
testImplementation(projects.services.apperror.test)
|
testImplementation(projects.services.apperror.test)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import dev.zacsweers.metro.Assisted
|
|||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.appnav.di.MatrixSessionCache
|
|
||||||
import io.element.android.appnav.intent.IntentResolver
|
import io.element.android.appnav.intent.IntentResolver
|
||||||
import io.element.android.appnav.intent.ResolvedIntent
|
import io.element.android.appnav.intent.ResolvedIntent
|
||||||
import io.element.android.appnav.room.RoomFlowNode
|
import io.element.android.appnav.room.RoomFlowNode
|
||||||
@@ -40,6 +39,7 @@ import io.element.android.appnav.room.RoomNavigationTarget
|
|||||||
import io.element.android.appnav.root.RootNavStateFlowFactory
|
import io.element.android.appnav.root.RootNavStateFlowFactory
|
||||||
import io.element.android.appnav.root.RootPresenter
|
import io.element.android.appnav.root.RootPresenter
|
||||||
import io.element.android.appnav.root.RootView
|
import io.element.android.appnav.root.RootView
|
||||||
|
import io.element.android.appnav.session.MatrixSessionCache
|
||||||
import io.element.android.features.announcement.api.AnnouncementService
|
import io.element.android.features.announcement.api.AnnouncementService
|
||||||
import io.element.android.features.login.api.LoginParams
|
import io.element.android.features.login.api.LoginParams
|
||||||
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
|
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
|
||||||
@@ -109,6 +109,13 @@ class RootFlowNode(
|
|||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins
|
plugins = plugins
|
||||||
) {
|
) {
|
||||||
|
/**
|
||||||
|
* Login params coming from a launch or new [Intent], waiting to be consumed by the not logged in flow.
|
||||||
|
* Kept here so that the root nav target can be computed from both the logged in state and the pending
|
||||||
|
* login params, whatever the order in which the intent and the first nav state emission are processed.
|
||||||
|
*/
|
||||||
|
private var pendingLoginParams: LoginParams? = null
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
analyticsColdStartWatcher.start()
|
analyticsColdStartWatcher.start()
|
||||||
appCoroutineScope.launch {
|
appCoroutineScope.launch {
|
||||||
@@ -152,7 +159,7 @@ class RootFlowNode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
LoggedInState.NotLoggedIn -> {
|
LoggedInState.NotLoggedIn -> {
|
||||||
switchToNotLoggedInFlow(null)
|
switchToNotLoggedInFlow(pendingLoginParams)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,15 +201,18 @@ class RootFlowNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
|
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
|
||||||
|
pendingLoginParams = null
|
||||||
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId))
|
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun switchToNotLoggedInFlow(params: LoginParams?) {
|
private fun switchToNotLoggedInFlow(params: LoginParams?) {
|
||||||
|
Timber.d("switchToNotLoggedInFlow, hasLoginParams=${params != null}")
|
||||||
matrixSessionCache.removeAll()
|
matrixSessionCache.removeAll()
|
||||||
backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
|
backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun switchToSignedOutFlow(sessionId: SessionId) {
|
private fun switchToSignedOutFlow(sessionId: SessionId) {
|
||||||
|
pendingLoginParams = null
|
||||||
backstack.safeRoot(NavTarget.SignedOutFlow(sessionId))
|
backstack.safeRoot(NavTarget.SignedOutFlow(sessionId))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,6 +329,7 @@ class RootFlowNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDone() {
|
override fun onDone() {
|
||||||
|
pendingLoginParams = null
|
||||||
backstack.pop()
|
backstack.pop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,6 +429,7 @@ class RootFlowNode(
|
|||||||
Timber.w("Login link ignored, multi account is disabled")
|
Timber.w("Login link ignored, multi account is disabled")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
pendingLoginParams = params
|
||||||
switchToNotLoggedInFlow(params)
|
switchToNotLoggedInFlow(params)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ package io.element.android.appnav.root
|
|||||||
import com.bumble.appyx.core.state.MutableSavedStateMap
|
import com.bumble.appyx.core.state.MutableSavedStateMap
|
||||||
import com.bumble.appyx.core.state.SavedStateMap
|
import com.bumble.appyx.core.state.SavedStateMap
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.appnav.di.MatrixSessionCache
|
import io.element.android.appnav.session.MatrixSessionCache
|
||||||
import io.element.android.features.preferences.api.CacheService
|
import io.element.android.features.preferences.api.CacheService
|
||||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.appnav.di
|
package io.element.android.appnav.session
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.bumble.appyx.core.state.MutableSavedStateMap
|
import com.bumble.appyx.core.state.MutableSavedStateMap
|
||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.appnav.di
|
package io.element.android.appnav.session
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* 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(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
|
package io.element.android.appnav
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
|
import com.bumble.appyx.core.modality.AncestryInfo
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.node
|
||||||
|
import com.bumble.appyx.navmodel.backstack.activeElement
|
||||||
|
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||||
|
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
|
||||||
|
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.appnav.intent.IntentResolver
|
||||||
|
import io.element.android.appnav.root.RootNavStateFlowFactory
|
||||||
|
import io.element.android.appnav.root.RootPresenter
|
||||||
|
import io.element.android.appnav.session.FakeSyncOrchestratorFactory
|
||||||
|
import io.element.android.appnav.session.MatrixSessionCache
|
||||||
|
import io.element.android.features.login.api.LoginEntryPoint
|
||||||
|
import io.element.android.features.login.api.LoginParams
|
||||||
|
import io.element.android.features.login.test.FakeLoginEntryPoint
|
||||||
|
import io.element.android.features.login.test.FakeLoginIntentResolver
|
||||||
|
import io.element.android.features.login.test.accesscontrol.FakeAccountProviderAccessControl
|
||||||
|
import io.element.android.features.preferences.test.FakeCacheService
|
||||||
|
import io.element.android.features.rageshake.test.FakeBugReportEntryPoint
|
||||||
|
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
|
||||||
|
import io.element.android.features.share.test.FakeShareIntentHandler
|
||||||
|
import io.element.android.features.signedout.test.FakeSignedOutEntryPoint
|
||||||
|
import io.element.android.libraries.accountselect.test.FakeAccountSelectEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.AssistedNodeFactory
|
||||||
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
|
import io.element.android.libraries.matrix.test.FakeSdkMetadata
|
||||||
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
|
import io.element.android.libraries.matrix.ui.media.test.FakeImageLoaderHolder
|
||||||
|
import io.element.android.libraries.oauth.test.FakeOAuthActionFlow
|
||||||
|
import io.element.android.libraries.oauth.test.FakeOAuthIntentResolver
|
||||||
|
import io.element.android.libraries.preferences.test.FakeSessionPreferencesStoreFactory
|
||||||
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
|
import io.element.android.services.analytics.test.watchers.FakeAnalyticsColdStartWatcher
|
||||||
|
import io.element.android.services.apperror.test.FakeAppErrorStateService
|
||||||
|
import io.element.android.tests.testutils.node.FakeNodeFactoriesBindings
|
||||||
|
import io.element.android.tests.testutils.node.FakeParentNode
|
||||||
|
import io.element.android.tests.testutils.presenter.NotUsedPresenter
|
||||||
|
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private const val A_LOGIN_LINK = "https://mobile.element.io/element/?account_provider=example.com&login_hint=mxid:@alice:example.com"
|
||||||
|
|
||||||
|
private val A_LOGIN_PARAMS = LoginParams(
|
||||||
|
accountProvider = "example.com",
|
||||||
|
loginHint = "mxid:@alice:example.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
private val A_LOGIN_ENTRY_POINT_PARAMS = LoginEntryPoint.Params(
|
||||||
|
accountProvider = A_LOGIN_PARAMS.accountProvider,
|
||||||
|
loginHint = A_LOGIN_PARAMS.loginHint,
|
||||||
|
)
|
||||||
|
|
||||||
|
class RootFlowNodeTest : RobolectricTest() {
|
||||||
|
@get:Rule
|
||||||
|
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val mainDispatcherRule = MainDispatcherRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no session, when a login link is handled before the nav state is observed, then the login params are kept`() = runTest {
|
||||||
|
var loginEntryPointParams: LoginEntryPoint.Params? = null
|
||||||
|
val rootFlowNode = createRootFlowNode(
|
||||||
|
loginEntryPoint = FakeLoginEntryPoint { buildContext, params ->
|
||||||
|
loginEntryPointParams = params
|
||||||
|
node(buildContext) {}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
rootFlowNode.parentNodeTestHelper()
|
||||||
|
// The intent is handled first, this is what happens when the deep link cold starts the app.
|
||||||
|
rootFlowNode.handleIntent(aLoginIntent())
|
||||||
|
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(A_LOGIN_PARAMS))
|
||||||
|
// Then the first nav state emission lands, it must not override the login params.
|
||||||
|
runCurrent()
|
||||||
|
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(A_LOGIN_PARAMS))
|
||||||
|
assertThat(loginEntryPointParams).isEqualTo(A_LOGIN_ENTRY_POINT_PARAMS)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no session, when a login link is handled after the nav state is observed, then the login params are applied`() = runTest {
|
||||||
|
var loginEntryPointParams: LoginEntryPoint.Params? = null
|
||||||
|
val rootFlowNode = createRootFlowNode(
|
||||||
|
loginEntryPoint = FakeLoginEntryPoint { buildContext, params ->
|
||||||
|
loginEntryPointParams = params
|
||||||
|
node(buildContext) {}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
rootFlowNode.parentNodeTestHelper()
|
||||||
|
// The nav state is observed first, this is what happens when the app is already running.
|
||||||
|
runCurrent()
|
||||||
|
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(null))
|
||||||
|
rootFlowNode.handleIntent(aLoginIntent())
|
||||||
|
runCurrent()
|
||||||
|
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(A_LOGIN_PARAMS))
|
||||||
|
assertThat(loginEntryPointParams).isEqualTo(A_LOGIN_ENTRY_POINT_PARAMS)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aLoginIntent() = Intent(Intent.ACTION_VIEW, Uri.parse(A_LOGIN_LINK))
|
||||||
|
|
||||||
|
private fun TestScope.createRootFlowNode(
|
||||||
|
loginEntryPoint: LoginEntryPoint,
|
||||||
|
sessionStore: SessionStore = InMemorySessionStore(),
|
||||||
|
): RootFlowNode {
|
||||||
|
val matrixSessionCache = MatrixSessionCache(
|
||||||
|
authenticationService = FakeMatrixAuthenticationService(),
|
||||||
|
syncOrchestratorFactory = FakeSyncOrchestratorFactory(),
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
)
|
||||||
|
val parentNode = FakeParentNode(
|
||||||
|
graph = FakeNodeFactoriesBindings(
|
||||||
|
mapOf(
|
||||||
|
NotLoggedInFlowNode::class to AssistedNodeFactory { buildContext, plugins ->
|
||||||
|
NotLoggedInFlowNode(
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = plugins,
|
||||||
|
loginEntryPoint = loginEntryPoint,
|
||||||
|
imageLoaderHolder = FakeImageLoaderHolder(),
|
||||||
|
analyticsColdStartWatcher = FakeAnalyticsColdStartWatcher(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return RootFlowNode(
|
||||||
|
buildContext = BuildContext(
|
||||||
|
ancestryInfo = AncestryInfo.Child(anchor = parentNode),
|
||||||
|
savedStateMap = null,
|
||||||
|
customisations = NodeCustomisationDirectoryImpl(),
|
||||||
|
),
|
||||||
|
plugins = emptyList(),
|
||||||
|
sessionStore = sessionStore,
|
||||||
|
accountProviderAccessControl = FakeAccountProviderAccessControl { true },
|
||||||
|
navStateFlowFactory = RootNavStateFlowFactory(
|
||||||
|
sessionStore = sessionStore,
|
||||||
|
cacheService = FakeCacheService(),
|
||||||
|
matrixSessionCache = matrixSessionCache,
|
||||||
|
imageLoaderHolder = FakeImageLoaderHolder(),
|
||||||
|
sessionPreferencesStoreFactory = FakeSessionPreferencesStoreFactory(),
|
||||||
|
),
|
||||||
|
matrixSessionCache = matrixSessionCache,
|
||||||
|
presenter = RootPresenter(
|
||||||
|
crashDetectionPresenter = NotUsedPresenter(),
|
||||||
|
rageshakeDetectionPresenter = NotUsedPresenter(),
|
||||||
|
appErrorStateService = FakeAppErrorStateService(),
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
sdkMetadata = FakeSdkMetadata("sha"),
|
||||||
|
),
|
||||||
|
bugReportEntryPoint = FakeBugReportEntryPoint(),
|
||||||
|
signedOutEntryPoint = FakeSignedOutEntryPoint(),
|
||||||
|
accountSelectEntryPoint = FakeAccountSelectEntryPoint(),
|
||||||
|
intentResolver = IntentResolver(
|
||||||
|
deeplinkParser = { null },
|
||||||
|
loginIntentResolver = FakeLoginIntentResolver { A_LOGIN_PARAMS },
|
||||||
|
oAuthIntentResolver = FakeOAuthIntentResolver { null },
|
||||||
|
permalinkParser = FakePermalinkParser(),
|
||||||
|
shareIntentHandler = FakeShareIntentHandler(),
|
||||||
|
),
|
||||||
|
oAuthActionFlow = FakeOAuthActionFlow(),
|
||||||
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
|
announcementService = FakeAnnouncementService(),
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
analyticsColdStartWatcher = FakeAnalyticsColdStartWatcher(),
|
||||||
|
appCoroutineScope = backgroundScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
package io.element.android.appnav
|
package io.element.android.appnav
|
||||||
|
|
||||||
import io.element.android.appnav.di.SyncOrchestrator
|
import io.element.android.appnav.session.SyncOrchestrator
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
|
|||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* 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.appnav.session
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
|
class FakeSyncOrchestratorFactory : SyncOrchestrator.Factory {
|
||||||
|
override fun create(
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
sessionCoroutineScope: CoroutineScope,
|
||||||
|
): SyncOrchestrator = lambdaError()
|
||||||
|
}
|
||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.appnav.di
|
package io.element.android.appnav.session
|
||||||
|
|
||||||
import com.bumble.appyx.core.state.MutableSavedStateMapImpl
|
import com.bumble.appyx.core.state.MutableSavedStateMapImpl
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
+1
-1
@@ -16,7 +16,7 @@ import io.element.android.features.login.api.LoginEntryPoint
|
|||||||
import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.classic.FakeElementClassicConnection
|
import io.element.android.features.login.impl.classic.FakeElementClassicConnection
|
||||||
import io.element.android.features.preferences.test.FakePreferencesEntryPoint
|
import io.element.android.features.preferences.test.FakePreferencesEntryPoint
|
||||||
import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
|
import io.element.android.libraries.oauth.test.FakeOAuthActionFlow
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.node.TestParentNode
|
import io.element.android.tests.testutils.node.TestParentNode
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationSer
|
|||||||
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
|
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
|
||||||
import io.element.android.libraries.oauth.api.OAuthAction
|
import io.element.android.libraries.oauth.api.OAuthAction
|
||||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||||
import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
|
import io.element.android.libraries.oauth.test.FakeOAuthActionFlow
|
||||||
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ 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.auth.FakeMatrixAuthenticationService
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||||
import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
|
import io.element.android.libraries.oauth.test.FakeOAuthActionFlow
|
||||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
|
||||||
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.features.login.api)
|
implementation(projects.features.login.api)
|
||||||
implementation(projects.tests.testutils)
|
implementation(projects.tests.testutils)
|
||||||
}
|
}
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* 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.login.test
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import io.element.android.features.login.api.LoginEntryPoint
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
|
class FakeLoginEntryPoint(
|
||||||
|
private val createNodeResult: (BuildContext, LoginEntryPoint.Params) -> Node = { _, _ -> lambdaError() },
|
||||||
|
) : LoginEntryPoint {
|
||||||
|
override fun createNode(
|
||||||
|
parentNode: Node,
|
||||||
|
buildContext: BuildContext,
|
||||||
|
params: LoginEntryPoint.Params,
|
||||||
|
callback: LoginEntryPoint.Callback,
|
||||||
|
): Node = createNodeResult(buildContext, params)
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* 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.login.test.accesscontrol
|
||||||
|
|
||||||
|
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
|
class FakeAccountProviderAccessControl(
|
||||||
|
private val isAllowedToConnectToAccountProviderResult: (String) -> Boolean = { lambdaError() },
|
||||||
|
) : AccountProviderAccessControl {
|
||||||
|
override suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String): Boolean {
|
||||||
|
return isAllowedToConnectToAccountProviderResult(accountProviderUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,5 +16,7 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.features.preferences.api)
|
implementation(projects.features.preferences.api)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(libs.coroutines.core)
|
||||||
implementation(projects.tests.testutils)
|
implementation(projects.tests.testutils)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* 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.preferences.test
|
||||||
|
|
||||||
|
import io.element.android.features.preferences.api.CacheService
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
|
||||||
|
class FakeCacheService(
|
||||||
|
override val clearedCacheEventFlow: Flow<SessionId> = emptyFlow(),
|
||||||
|
) : CacheService
|
||||||
@@ -14,6 +14,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.features.rageshake.api)
|
implementation(projects.features.rageshake.api)
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
implementation(projects.tests.testutils)
|
implementation(projects.tests.testutils)
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* 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.rageshake.test
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
|
class FakeBugReportEntryPoint(
|
||||||
|
private val createNodeResult: (BuildContext, BugReportEntryPoint.Callback) -> Node = { _, _ -> lambdaError() },
|
||||||
|
) : BugReportEntryPoint {
|
||||||
|
override fun createNode(
|
||||||
|
parentNode: Node,
|
||||||
|
buildContext: BuildContext,
|
||||||
|
callback: BugReportEntryPoint.Callback,
|
||||||
|
): Node = createNodeResult(buildContext, callback)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.signedout.test"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.features.signedout.api)
|
||||||
|
implementation(projects.tests.testutils)
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* 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.signedout.test
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import io.element.android.features.signedout.api.SignedOutEntryPoint
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
|
class FakeSignedOutEntryPoint(
|
||||||
|
private val createNodeResult: (BuildContext, SignedOutEntryPoint.Params) -> Node = { _, _ -> lambdaError() },
|
||||||
|
) : SignedOutEntryPoint {
|
||||||
|
override fun createNode(
|
||||||
|
parentNode: Node,
|
||||||
|
buildContext: BuildContext,
|
||||||
|
params: SignedOutEntryPoint.Params,
|
||||||
|
): Node = createNodeResult(buildContext, params)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.libraries.accountselect.test"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.accountselect.api)
|
||||||
|
implementation(projects.tests.testutils)
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* 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.libraries.accountselect.test
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
|
class FakeAccountSelectEntryPoint(
|
||||||
|
private val createNodeResult: (BuildContext, AccountSelectEntryPoint.Callback) -> Node = { _, _ -> lambdaError() },
|
||||||
|
) : AccountSelectEntryPoint {
|
||||||
|
override fun createNode(
|
||||||
|
parentNode: Node,
|
||||||
|
buildContext: BuildContext,
|
||||||
|
callback: AccountSelectEntryPoint.Callback,
|
||||||
|
): Node = createNodeResult(buildContext, callback)
|
||||||
|
}
|
||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.oauth.test.customtab
|
package io.element.android.libraries.oauth.test
|
||||||
|
|
||||||
import io.element.android.libraries.oauth.api.OAuthAction
|
import io.element.android.libraries.oauth.api.OAuthAction
|
||||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||||
@@ -104,6 +104,7 @@ class KonsistClassNameTest {
|
|||||||
"FakeImageLoader",
|
"FakeImageLoader",
|
||||||
"FakeKeyStore",
|
"FakeKeyStore",
|
||||||
"FakeListenableFuture",
|
"FakeListenableFuture",
|
||||||
|
"FakeParentNode",
|
||||||
)
|
)
|
||||||
.assertTrue {
|
.assertTrue {
|
||||||
val interfaceName = it.name
|
val interfaceName = it.name
|
||||||
|
|||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* 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.tests.testutils.node
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import io.element.android.libraries.architecture.AssistedNodeFactory
|
||||||
|
import io.element.android.libraries.architecture.NodeFactoriesBindings
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class FakeNodeFactoriesBindings(
|
||||||
|
private val nodeFactories: Map<KClass<out Node>, AssistedNodeFactory<*>>,
|
||||||
|
) : NodeFactoriesBindings {
|
||||||
|
override fun nodeFactories() = nodeFactories
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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.tests.testutils.node
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.node.ParentNode
|
||||||
|
import com.bumble.appyx.core.node.node
|
||||||
|
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Node creates its children using the dependency injection graph of its ancestors, so a Node under test
|
||||||
|
* needs such a parent to be able to create its children.
|
||||||
|
*/
|
||||||
|
class FakeParentNode(
|
||||||
|
override val graph: Any,
|
||||||
|
) : ParentNode<Unit>(
|
||||||
|
navModel = PermanentNavModel(navTargets = setOf(Unit), savedStateMap = null),
|
||||||
|
buildContext = BuildContext.root(savedStateMap = null),
|
||||||
|
),
|
||||||
|
DependencyInjectionGraphOwner {
|
||||||
|
override fun resolve(navTarget: Unit, buildContext: BuildContext): Node = node(buildContext) {}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) = Unit
|
||||||
|
}
|
||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.tests.testutils
|
package io.element.android.tests.testutils.presenter
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* 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.tests.testutils.presenter
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A presenter that cannot be used, util for testing Nodes.
|
||||||
|
*/
|
||||||
|
class NotUsedPresenter<T> : Presenter<T> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): T = error("Not used")
|
||||||
|
}
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# 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_name="io.element.android.x.debug"
|
||||||
|
|
||||||
|
echo " => Force stop ${package_name}"
|
||||||
|
|
||||||
|
adb shell am force-stop "${package_name}"
|
||||||
Reference in New Issue
Block a user