diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 64aa2a6fc5..8e21f247ee 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { testCommonDependencies(libs) testImplementation(projects.features.login.test) testImplementation(projects.features.share.test) + testImplementation(projects.libraries.accountselect.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrixmedia.test) @@ -75,6 +76,7 @@ dependencies { testImplementation(projects.features.messages.test) testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.rageshake.test) + testImplementation(projects.features.signedout.test) testImplementation(projects.services.apperror.test) testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.analytics.test) diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RootFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RootFlowNodeTest.kt index 9710b72df7..1661782871 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RootFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RootFlowNodeTest.kt @@ -33,13 +33,14 @@ import io.element.android.appnav.root.RootPresenter import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.login.api.LoginParams import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl +import io.element.android.features.login.test.FakeLoginEntryPoint import io.element.android.features.login.test.FakeLoginIntentResolver import io.element.android.features.preferences.api.CacheService -import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint +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.api.SignedOutEntryPoint -import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint +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.architecture.NodeFactoriesBindings import io.element.android.libraries.architecture.Presenter @@ -81,8 +82,13 @@ class RootFlowNodeTest : RobolectricTest() { @Test fun `given no session, when a login link is handled before the nav state is observed, then the login params are kept`() = runTest { - val loginEntryPoint = FakeLoginEntryPoint() - val rootFlowNode = createRootFlowNode(loginEntryPoint = loginEntryPoint) + 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()) @@ -90,13 +96,18 @@ class RootFlowNodeTest : RobolectricTest() { // 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(loginEntryPoint.params).isEqualTo(A_LOGIN_ENTRY_POINT_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 { - val loginEntryPoint = FakeLoginEntryPoint() - val rootFlowNode = createRootFlowNode(loginEntryPoint = loginEntryPoint) + 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() @@ -104,13 +115,13 @@ class RootFlowNodeTest : RobolectricTest() { rootFlowNode.handleIntent(aLoginIntent()) runCurrent() assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(A_LOGIN_PARAMS)) - assertThat(loginEntryPoint.params).isEqualTo(A_LOGIN_ENTRY_POINT_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 = FakeLoginEntryPoint(), + loginEntryPoint: LoginEntryPoint, sessionStore: SessionStore = InMemorySessionStore(), ): RootFlowNode { val matrixSessionCache = MatrixSessionCache( @@ -215,33 +226,6 @@ private class FakeAccountProviderAccessControl : AccountProviderAccessControl { override suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String) = true } -private class FakeBugReportEntryPoint : BugReportEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext, callback: BugReportEntryPoint.Callback): Node = node(buildContext) {} -} - -private class FakeSignedOutEntryPoint : SignedOutEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext, params: SignedOutEntryPoint.Params): Node = node(buildContext) {} -} - -private class FakeAccountSelectEntryPoint : AccountSelectEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext, callback: AccountSelectEntryPoint.Callback): Node = node(buildContext) {} -} - -private class FakeLoginEntryPoint : LoginEntryPoint { - var params: LoginEntryPoint.Params? = null - private set - - override fun createNode( - parentNode: Node, - buildContext: BuildContext, - params: LoginEntryPoint.Params, - callback: LoginEntryPoint.Callback, - ): Node { - this.params = params - return node(buildContext) {} - } -} - private class FakeNodeFactoriesBindings( private val nodeFactories: Map, AssistedNodeFactory<*>>, ) : NodeFactoriesBindings { diff --git a/features/login/test/build.gradle.kts b/features/login/test/build.gradle.kts index 7073648dd2..4cbf0910db 100644 --- a/features/login/test/build.gradle.kts +++ b/features/login/test/build.gradle.kts @@ -15,6 +15,7 @@ android { } dependencies { + implementation(projects.libraries.architecture) implementation(projects.features.login.api) implementation(projects.tests.testutils) } diff --git a/features/login/test/src/main/kotlin/io/element/android/features/login/test/FakeLoginEntryPoint.kt b/features/login/test/src/main/kotlin/io/element/android/features/login/test/FakeLoginEntryPoint.kt new file mode 100644 index 0000000000..2c94144890 --- /dev/null +++ b/features/login/test/src/main/kotlin/io/element/android/features/login/test/FakeLoginEntryPoint.kt @@ -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) +} diff --git a/features/rageshake/test/build.gradle.kts b/features/rageshake/test/build.gradle.kts index afc5e51499..faad812d73 100644 --- a/features/rageshake/test/build.gradle.kts +++ b/features/rageshake/test/build.gradle.kts @@ -14,6 +14,7 @@ android { } dependencies { + implementation(projects.libraries.architecture) implementation(projects.features.rageshake.api) implementation(libs.coroutines.core) implementation(projects.tests.testutils) diff --git a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/FakeBugReportEntryPoint.kt b/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/FakeBugReportEntryPoint.kt new file mode 100644 index 0000000000..d6e1a47368 --- /dev/null +++ b/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/FakeBugReportEntryPoint.kt @@ -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) +} diff --git a/features/signedout/test/build.gradle.kts b/features/signedout/test/build.gradle.kts new file mode 100644 index 0000000000..5bbfb33c1a --- /dev/null +++ b/features/signedout/test/build.gradle.kts @@ -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) +} diff --git a/features/signedout/test/src/main/kotlin/io/element/android/features/signedout/test/FakeSignedOutEntryPoint.kt b/features/signedout/test/src/main/kotlin/io/element/android/features/signedout/test/FakeSignedOutEntryPoint.kt new file mode 100644 index 0000000000..863b946871 --- /dev/null +++ b/features/signedout/test/src/main/kotlin/io/element/android/features/signedout/test/FakeSignedOutEntryPoint.kt @@ -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) +} diff --git a/libraries/accountselect/test/build.gradle.kts b/libraries/accountselect/test/build.gradle.kts new file mode 100644 index 0000000000..c38cd8ac69 --- /dev/null +++ b/libraries/accountselect/test/build.gradle.kts @@ -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) +} diff --git a/libraries/accountselect/test/src/main/kotlin/io/element/android/libraries/accountselect/test/FakeAccountSelectEntryPoint.kt b/libraries/accountselect/test/src/main/kotlin/io/element/android/libraries/accountselect/test/FakeAccountSelectEntryPoint.kt new file mode 100644 index 0000000000..acc5ba10df --- /dev/null +++ b/libraries/accountselect/test/src/main/kotlin/io/element/android/libraries/accountselect/test/FakeAccountSelectEntryPoint.kt @@ -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) +}