Add test for ChangeAccountProviderPresenter and other presenters.
This commit is contained in:
parent
97090b9007
commit
ae38ffa914
10 changed files with 432 additions and 403 deletions
|
|
@ -43,6 +43,7 @@ class ChangeAccountProviderFormPresenter @Inject constructor(
|
||||||
fun handleEvents(event: ChangeAccountProviderFormEvents) {
|
fun handleEvents(event: ChangeAccountProviderFormEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is ChangeAccountProviderFormEvents.UserInput -> {
|
is ChangeAccountProviderFormEvents.UserInput -> {
|
||||||
|
userInput.value = event.input
|
||||||
localCoroutineScope.userInput(event.input)
|
localCoroutineScope.userInput(event.input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,3 +51,11 @@ fun aHomeserverDataList(): List<HomeserverData> {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun aHomeserverData(): HomeserverData {
|
||||||
|
return HomeserverData(
|
||||||
|
userInput = "matrix",
|
||||||
|
homeserverUrl = "https://matrix.org",
|
||||||
|
isWellknownValid = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.changeaccountprovider.form
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellknownRequest
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
|
import io.element.android.libraries.core.uri.isValidUrl
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.joinAll
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve homeserver base on search terms
|
||||||
|
*/
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultHomeserverResolver @Inject constructor(
|
||||||
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
private val wellknownRequest: WellknownRequest,
|
||||||
|
): HomeserverResolver {
|
||||||
|
private val mutableFlow: MutableStateFlow<Async<List<HomeserverData>>> = MutableStateFlow(Async.Uninitialized)
|
||||||
|
|
||||||
|
override fun flow(): StateFlow<Async<List<HomeserverData>>> = mutableFlow
|
||||||
|
|
||||||
|
private var currentJob: Job? = null
|
||||||
|
|
||||||
|
override suspend fun accept(userInput: String) {
|
||||||
|
currentJob?.cancel()
|
||||||
|
val cleanedUpUserInput = userInput.trim()
|
||||||
|
mutableFlow.tryEmit(Async.Uninitialized)
|
||||||
|
if (cleanedUpUserInput.length > 3) {
|
||||||
|
delay(300)
|
||||||
|
mutableFlow.tryEmit(Async.Loading())
|
||||||
|
withContext(dispatchers.io) {
|
||||||
|
val list = getUrlCandidate(cleanedUpUserInput)
|
||||||
|
currentJob = resolveList(userInput, list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.resolveList(userInput: String, list: List<String>): Job {
|
||||||
|
val currentList = mutableListOf<HomeserverData>()
|
||||||
|
return launch {
|
||||||
|
list.map {
|
||||||
|
async {
|
||||||
|
val isValid = tryOrNull { wellknownRequest.execute(it) }.orFalse()
|
||||||
|
if (isValid) {
|
||||||
|
// Emit the list as soon as possible
|
||||||
|
currentList.add(HomeserverData(userInput, it, true))
|
||||||
|
mutableFlow.tryEmit(Async.Success(currentList))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.joinAll()
|
||||||
|
.also {
|
||||||
|
// If list is empty, and the user as entered an URL, do not block the user.
|
||||||
|
if (currentList.isEmpty()) {
|
||||||
|
if (userInput.isValidUrl()) {
|
||||||
|
mutableFlow.tryEmit(
|
||||||
|
Async.Success(
|
||||||
|
listOf(
|
||||||
|
HomeserverData(
|
||||||
|
userInput = userInput,
|
||||||
|
homeserverUrl = userInput,
|
||||||
|
isWellknownValid = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
mutableFlow.tryEmit(Async.Uninitialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUrlCandidate(data: String): List<String> {
|
||||||
|
return buildList {
|
||||||
|
val s = data.ensureProtocol()
|
||||||
|
.removeSuffix("/")
|
||||||
|
|
||||||
|
// Always try what the user has entered
|
||||||
|
add(s)
|
||||||
|
|
||||||
|
if (s.contains(".")) {
|
||||||
|
// TLD detected?
|
||||||
|
} else {
|
||||||
|
add("$s.org")
|
||||||
|
add("$s.com")
|
||||||
|
add("$s.io")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.changeaccountprovider.form
|
package io.element.android.features.login.impl.changeaccountprovider.form
|
||||||
|
|
||||||
data class HomeserverData(
|
data class HomeserverData constructor(
|
||||||
// What the user has entered
|
// What the user has entered
|
||||||
val userInput: String,
|
val userInput: String,
|
||||||
// The computed homeserver url, for which a wellknown file has been retrieved, or just a valid Url
|
// The computed homeserver url, for which a wellknown file has been retrieved, or just a valid Url
|
||||||
|
|
|
||||||
|
|
@ -16,102 +16,13 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.changeaccountprovider.form
|
package io.element.android.features.login.impl.changeaccountprovider.form
|
||||||
|
|
||||||
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellknownRequest
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
|
||||||
import io.element.android.libraries.core.uri.ensureProtocol
|
|
||||||
import io.element.android.libraries.core.uri.isValidUrl
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.joinAll
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve homeserver base on search terms
|
* Resolve homeserver base on search terms.
|
||||||
*/
|
*/
|
||||||
class HomeserverResolver @Inject constructor(
|
interface HomeserverResolver {
|
||||||
private val dispatchers: CoroutineDispatchers,
|
fun flow(): StateFlow<Async<List<HomeserverData>>>
|
||||||
private val wellknownRequest: WellknownRequest,
|
suspend fun accept(userInput: String)
|
||||||
) {
|
|
||||||
private val mutableFlow: MutableStateFlow<Async<List<HomeserverData>>> = MutableStateFlow(Async.Uninitialized)
|
|
||||||
|
|
||||||
fun flow(): StateFlow<Async<List<HomeserverData>>> = mutableFlow
|
|
||||||
|
|
||||||
private var currentJob: Job? = null
|
|
||||||
|
|
||||||
suspend fun accept(userInput: String) {
|
|
||||||
currentJob?.cancel()
|
|
||||||
val cleanedUpUserInput = userInput.trim()
|
|
||||||
mutableFlow.tryEmit(Async.Uninitialized)
|
|
||||||
if (cleanedUpUserInput.length > 3) {
|
|
||||||
delay(300)
|
|
||||||
mutableFlow.tryEmit(Async.Loading())
|
|
||||||
withContext(dispatchers.io) {
|
|
||||||
val list = getUrlCandidate(cleanedUpUserInput)
|
|
||||||
currentJob = resolveList(userInput, list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun CoroutineScope.resolveList(userInput: String, list: List<String>): Job {
|
|
||||||
val currentList = mutableListOf<HomeserverData>()
|
|
||||||
return launch {
|
|
||||||
list.map {
|
|
||||||
async {
|
|
||||||
val isValid = tryOrNull { wellknownRequest.execute(it) }.orFalse()
|
|
||||||
if (isValid) {
|
|
||||||
// Emit the list as soon as possible
|
|
||||||
currentList.add(HomeserverData(userInput, it, true))
|
|
||||||
mutableFlow.tryEmit(Async.Success(currentList))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.joinAll()
|
|
||||||
.also {
|
|
||||||
// If list is empty, and the user as entered an URL, do not block the user.
|
|
||||||
if (currentList.isEmpty()) {
|
|
||||||
if (userInput.isValidUrl()) {
|
|
||||||
mutableFlow.tryEmit(
|
|
||||||
Async.Success(
|
|
||||||
listOf(
|
|
||||||
HomeserverData(
|
|
||||||
userInput = userInput,
|
|
||||||
homeserverUrl = userInput,
|
|
||||||
isWellknownValid = false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
mutableFlow.tryEmit(Async.Uninitialized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getUrlCandidate(data: String): List<String> {
|
|
||||||
return buildList {
|
|
||||||
val s = data.ensureProtocol()
|
|
||||||
.removeSuffix("/")
|
|
||||||
|
|
||||||
// Always try what the user has entered
|
|
||||||
add(s)
|
|
||||||
|
|
||||||
if (s.contains(".")) {
|
|
||||||
// TLD detected?
|
|
||||||
} else {
|
|
||||||
add("$s.org")
|
|
||||||
add("$s.com")
|
|
||||||
add("$s.io")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.changeaccountprovider.form
|
||||||
|
|
||||||
|
import app.cash.molecule.RecompositionClock
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ChangeAccountProviderFormPresenterTest {
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val homeServerResolver = FakeHomeServerResolver()
|
||||||
|
val presenter = ChangeAccountProviderFormPresenter(
|
||||||
|
homeServerResolver
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.userInput).isEmpty()
|
||||||
|
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - enter text no result`() = runTest {
|
||||||
|
val homeServerResolver = FakeHomeServerResolver()
|
||||||
|
val presenter = ChangeAccountProviderFormPresenter(
|
||||||
|
homeServerResolver
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(ChangeAccountProviderFormEvents.UserInput("test"))
|
||||||
|
val withInputState = awaitItem()
|
||||||
|
assertThat(withInputState.userInput).isEqualTo("test")
|
||||||
|
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
|
||||||
|
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
|
||||||
|
assertThat(awaitItem().userInputResult).isEqualTo(Async.Uninitialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - enter text one then two results`() = runTest {
|
||||||
|
val homeServerResolver = FakeHomeServerResolver()
|
||||||
|
homeServerResolver.givenResult(
|
||||||
|
listOf(
|
||||||
|
listOf(aHomeserverData()),
|
||||||
|
listOf(aHomeserverData(), aHomeserverData()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val presenter = ChangeAccountProviderFormPresenter(
|
||||||
|
homeServerResolver
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(ChangeAccountProviderFormEvents.UserInput("test"))
|
||||||
|
val withInputState = awaitItem()
|
||||||
|
assertThat(withInputState.userInput).isEqualTo("test")
|
||||||
|
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
|
||||||
|
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
|
||||||
|
assertThat(awaitItem().userInputResult).isEqualTo(Async.Success(listOf(aHomeserverData())))
|
||||||
|
assertThat(awaitItem().userInputResult).isEqualTo(Async.Success(listOf(aHomeserverData(), aHomeserverData())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.changeaccountprovider.form
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
class FakeHomeServerResolver : HomeserverResolver {
|
||||||
|
private var pendingResult: List<List<HomeserverData>> = emptyList()
|
||||||
|
fun givenResult(result: List<List<HomeserverData>>) {
|
||||||
|
pendingResult = result
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mutableFlow: MutableStateFlow<Async<List<HomeserverData>>> = MutableStateFlow(Async.Uninitialized)
|
||||||
|
|
||||||
|
override fun flow(): StateFlow<Async<List<HomeserverData>>> = mutableFlow
|
||||||
|
|
||||||
|
override suspend fun accept(userInput: String) {
|
||||||
|
mutableFlow.tryEmit(Async.Uninitialized)
|
||||||
|
delay(FAKE_DELAY_IN_MS)
|
||||||
|
mutableFlow.tryEmit(Async.Loading())
|
||||||
|
// Sending the pending result
|
||||||
|
if (pendingResult.isEmpty()) {
|
||||||
|
mutableFlow.tryEmit(Async.Uninitialized)
|
||||||
|
} else {
|
||||||
|
pendingResult.forEach {
|
||||||
|
delay(FAKE_DELAY_IN_MS)
|
||||||
|
mutableFlow.tryEmit(Async.Success(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.loginpassword
|
||||||
|
|
||||||
|
import app.cash.molecule.RecompositionClock
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.login.impl.datasource.AccountProviderDataSource
|
||||||
|
import io.element.android.features.login.impl.util.defaultAccountProvider
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.matrix.test.A_HOMESERVER
|
||||||
|
import io.element.android.libraries.matrix.test.A_PASSWORD
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
|
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class LoginPasswordPresenterTest {
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val authenticationService = FakeAuthenticationService()
|
||||||
|
val accountProviderDataSource = AccountProviderDataSource()
|
||||||
|
val presenter = LoginPasswordPresenter(
|
||||||
|
authenticationService,
|
||||||
|
accountProviderDataSource,
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider)
|
||||||
|
assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
|
||||||
|
assertThat(initialState.loginAction).isEqualTo(Async.Uninitialized)
|
||||||
|
assertThat(initialState.submitEnabled).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - enter login and password`() = runTest {
|
||||||
|
val authenticationService = FakeAuthenticationService()
|
||||||
|
val accountProviderDataSource = AccountProviderDataSource()
|
||||||
|
val presenter = LoginPasswordPresenter(
|
||||||
|
authenticationService,
|
||||||
|
accountProviderDataSource,
|
||||||
|
)
|
||||||
|
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME))
|
||||||
|
val loginState = awaitItem()
|
||||||
|
assertThat(loginState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = ""))
|
||||||
|
assertThat(loginState.submitEnabled).isFalse()
|
||||||
|
initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD))
|
||||||
|
val loginAndPasswordState = awaitItem()
|
||||||
|
assertThat(loginAndPasswordState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = A_PASSWORD))
|
||||||
|
assertThat(loginAndPasswordState.submitEnabled).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - submit`() = runTest {
|
||||||
|
val authenticationService = FakeAuthenticationService()
|
||||||
|
val accountProviderDataSource = AccountProviderDataSource()
|
||||||
|
val presenter = LoginPasswordPresenter(
|
||||||
|
authenticationService,
|
||||||
|
accountProviderDataSource,
|
||||||
|
)
|
||||||
|
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME))
|
||||||
|
initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD))
|
||||||
|
skipItems(1)
|
||||||
|
val loginAndPasswordState = awaitItem()
|
||||||
|
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
|
||||||
|
val submitState = awaitItem()
|
||||||
|
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
|
||||||
|
val loggedInState = awaitItem()
|
||||||
|
assertThat(loggedInState.loginAction).isEqualTo(Async.Success(A_SESSION_ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - submit with error`() = runTest {
|
||||||
|
val authenticationService = FakeAuthenticationService()
|
||||||
|
val accountProviderDataSource = AccountProviderDataSource()
|
||||||
|
val presenter = LoginPasswordPresenter(
|
||||||
|
authenticationService,
|
||||||
|
accountProviderDataSource,
|
||||||
|
)
|
||||||
|
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME))
|
||||||
|
initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD))
|
||||||
|
skipItems(1)
|
||||||
|
val loginAndPasswordState = awaitItem()
|
||||||
|
authenticationService.givenLoginError(A_THROWABLE)
|
||||||
|
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
|
||||||
|
val submitState = awaitItem()
|
||||||
|
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
|
||||||
|
val loggedInState = awaitItem()
|
||||||
|
assertThat(loggedInState.loginAction).isEqualTo(Async.Failure<SessionId>(A_THROWABLE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - clear error`() = runTest {
|
||||||
|
val authenticationService = FakeAuthenticationService()
|
||||||
|
val accountProviderDataSource = AccountProviderDataSource()
|
||||||
|
val presenter = LoginPasswordPresenter(
|
||||||
|
authenticationService,
|
||||||
|
accountProviderDataSource,
|
||||||
|
)
|
||||||
|
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME))
|
||||||
|
initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD))
|
||||||
|
skipItems(1)
|
||||||
|
val loginAndPasswordState = awaitItem()
|
||||||
|
authenticationService.givenLoginError(A_THROWABLE)
|
||||||
|
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
|
||||||
|
val submitState = awaitItem()
|
||||||
|
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
|
||||||
|
val loggedInState = awaitItem()
|
||||||
|
// Check an error was returned
|
||||||
|
assertThat(loggedInState.loginAction).isEqualTo(Async.Failure<SessionId>(A_THROWABLE))
|
||||||
|
// Assert the error is then cleared
|
||||||
|
loggedInState.eventSink(LoginPasswordEvents.ClearError)
|
||||||
|
val clearedState = awaitItem()
|
||||||
|
assertThat(clearedState.loginAction).isEqualTo(Async.Uninitialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,308 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.login.impl.root
|
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.features.login.api.oidc.OidcAction
|
|
||||||
import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
|
|
||||||
import io.element.android.features.login.impl.util.LoginConstants
|
|
||||||
import io.element.android.libraries.architecture.Async
|
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
|
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER
|
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
|
|
||||||
import io.element.android.libraries.matrix.test.A_PASSWORD
|
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
|
||||||
import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA
|
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class LoginRootPresenterTest {
|
|
||||||
@Test
|
|
||||||
fun `present - initial state`() = runTest {
|
|
||||||
val presenter = LoginRootPresenter(
|
|
||||||
FakeAuthenticationService(),
|
|
||||||
DefaultOidcActionFlow(),
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL)
|
|
||||||
assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized)
|
|
||||||
assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
|
|
||||||
assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
|
|
||||||
assertThat(initialState.submitEnabled).isFalse()
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - initial state server load`() = runTest {
|
|
||||||
val authenticationService = FakeAuthenticationService()
|
|
||||||
val oidcActionFlow = DefaultOidcActionFlow()
|
|
||||||
val presenter = LoginRootPresenter(
|
|
||||||
authenticationService,
|
|
||||||
oidcActionFlow,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL)
|
|
||||||
assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized)
|
|
||||||
assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
|
|
||||||
assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
|
|
||||||
assertThat(initialState.submitEnabled).isFalse()
|
|
||||||
val loadingState = awaitItem()
|
|
||||||
assertThat(loadingState.homeserverDetails).isEqualTo(Async.Loading<MatrixHomeServerDetails>())
|
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
|
||||||
skipItems(1)
|
|
||||||
val loadedState = awaitItem()
|
|
||||||
assertThat(loadedState.homeserverDetails).isEqualTo(Async.Success(A_HOMESERVER))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - initial state server load error and retry`() = runTest {
|
|
||||||
val authenticationService = FakeAuthenticationService()
|
|
||||||
val oidcActionFlow = DefaultOidcActionFlow()
|
|
||||||
val presenter = LoginRootPresenter(
|
|
||||||
authenticationService,
|
|
||||||
oidcActionFlow,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL)
|
|
||||||
assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized)
|
|
||||||
assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
|
|
||||||
assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
|
|
||||||
assertThat(initialState.submitEnabled).isFalse()
|
|
||||||
val loadingState = awaitItem()
|
|
||||||
assertThat(loadingState.homeserverDetails).isEqualTo(Async.Loading<MatrixHomeServerDetails>())
|
|
||||||
val aThrowable = Throwable("Error")
|
|
||||||
authenticationService.givenChangeServerError(aThrowable)
|
|
||||||
val errorState = awaitItem()
|
|
||||||
assertThat(errorState.homeserverDetails).isEqualTo(Async.Failure<MatrixHomeServerDetails>(aThrowable))
|
|
||||||
// Retry
|
|
||||||
errorState.eventSink.invoke(LoginRootEvents.RetryFetchServerInfo)
|
|
||||||
val loadingState2 = awaitItem()
|
|
||||||
assertThat(loadingState2.homeserverDetails).isEqualTo(Async.Loading<MatrixHomeServerDetails>())
|
|
||||||
authenticationService.givenChangeServerError(null)
|
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
|
||||||
skipItems(1)
|
|
||||||
val loadedState = awaitItem()
|
|
||||||
assertThat(loadedState.homeserverDetails).isEqualTo(Async.Success(A_HOMESERVER))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - enter login and password`() = runTest {
|
|
||||||
val authenticationService = FakeAuthenticationService()
|
|
||||||
val oidcActionFlow = DefaultOidcActionFlow()
|
|
||||||
val presenter = LoginRootPresenter(
|
|
||||||
authenticationService,
|
|
||||||
oidcActionFlow,
|
|
||||||
)
|
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME))
|
|
||||||
val loginState = awaitItem()
|
|
||||||
assertThat(loginState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = ""))
|
|
||||||
assertThat(loginState.submitEnabled).isFalse()
|
|
||||||
initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD))
|
|
||||||
val loginAndPasswordState = awaitItem()
|
|
||||||
assertThat(loginAndPasswordState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = A_PASSWORD))
|
|
||||||
assertThat(loginAndPasswordState.submitEnabled).isTrue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - oidc login`() = runTest {
|
|
||||||
val authenticationService = FakeAuthenticationService()
|
|
||||||
val oidcActionFlow = DefaultOidcActionFlow()
|
|
||||||
val presenter = LoginRootPresenter(
|
|
||||||
authenticationService,
|
|
||||||
oidcActionFlow,
|
|
||||||
)
|
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.submitEnabled).isTrue()
|
|
||||||
initialState.eventSink.invoke(LoginRootEvents.Submit)
|
|
||||||
val oidcState = awaitItem()
|
|
||||||
assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.OidcStarted(A_OIDC_DATA))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - oidc login error`() = runTest {
|
|
||||||
val authenticationService = FakeAuthenticationService()
|
|
||||||
val oidcActionFlow = DefaultOidcActionFlow()
|
|
||||||
val presenter = LoginRootPresenter(
|
|
||||||
authenticationService,
|
|
||||||
oidcActionFlow,
|
|
||||||
)
|
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
|
|
||||||
authenticationService.givenOidcError(A_THROWABLE)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.submitEnabled).isTrue()
|
|
||||||
initialState.eventSink.invoke(LoginRootEvents.Submit)
|
|
||||||
val oidcState = awaitItem()
|
|
||||||
assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - oidc custom tab login`() = runTest {
|
|
||||||
val authenticationService = FakeAuthenticationService()
|
|
||||||
val oidcActionFlow = DefaultOidcActionFlow()
|
|
||||||
val presenter = LoginRootPresenter(
|
|
||||||
authenticationService,
|
|
||||||
oidcActionFlow,
|
|
||||||
)
|
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.submitEnabled).isTrue()
|
|
||||||
initialState.eventSink.invoke(LoginRootEvents.Submit)
|
|
||||||
val oidcState = awaitItem()
|
|
||||||
assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.OidcStarted(A_OIDC_DATA))
|
|
||||||
// Oidc cancel, sdk error
|
|
||||||
authenticationService.givenOidcCancelError(A_THROWABLE)
|
|
||||||
oidcActionFlow.post(OidcAction.GoBack)
|
|
||||||
val stateCancelSdkError = awaitItem()
|
|
||||||
assertThat(stateCancelSdkError.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE))
|
|
||||||
// Oidc cancel, sdk OK
|
|
||||||
authenticationService.givenOidcCancelError(null)
|
|
||||||
oidcActionFlow.post(OidcAction.GoBack)
|
|
||||||
val stateCancel = awaitItem()
|
|
||||||
assertThat(stateCancel.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
|
|
||||||
// Oidc success, sdk error
|
|
||||||
authenticationService.givenLoginError(A_THROWABLE)
|
|
||||||
oidcActionFlow.post(OidcAction.Success(A_OIDC_DATA.url))
|
|
||||||
val stateSuccessSdkErrorLoading = awaitItem()
|
|
||||||
assertThat(stateSuccessSdkErrorLoading.loggedInState).isEqualTo(LoggedInState.LoggingIn)
|
|
||||||
val stateSuccessSdkError = awaitItem()
|
|
||||||
assertThat(stateSuccessSdkError.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE))
|
|
||||||
// Oidc success
|
|
||||||
authenticationService.givenLoginError(null)
|
|
||||||
oidcActionFlow.post(OidcAction.Success(A_OIDC_DATA.url))
|
|
||||||
val stateSuccess = awaitItem()
|
|
||||||
assertThat(stateSuccess.loggedInState).isEqualTo(LoggedInState.LoggingIn)
|
|
||||||
val stateSuccessLoggedIn = awaitItem()
|
|
||||||
assertThat(stateSuccessLoggedIn.loggedInState).isEqualTo(LoggedInState.LoggedIn(A_SESSION_ID))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - submit`() = runTest {
|
|
||||||
val authenticationService = FakeAuthenticationService()
|
|
||||||
val oidcActionFlow = DefaultOidcActionFlow()
|
|
||||||
val presenter = LoginRootPresenter(
|
|
||||||
authenticationService,
|
|
||||||
oidcActionFlow,
|
|
||||||
)
|
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME))
|
|
||||||
initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD))
|
|
||||||
skipItems(1)
|
|
||||||
val loginAndPasswordState = awaitItem()
|
|
||||||
loginAndPasswordState.eventSink.invoke(LoginRootEvents.Submit)
|
|
||||||
val submitState = awaitItem()
|
|
||||||
assertThat(submitState.loggedInState).isEqualTo(LoggedInState.LoggingIn)
|
|
||||||
val loggedInState = awaitItem()
|
|
||||||
assertThat(loggedInState.loggedInState).isEqualTo(LoggedInState.LoggedIn(A_SESSION_ID))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - submit with error`() = runTest {
|
|
||||||
val authenticationService = FakeAuthenticationService()
|
|
||||||
val oidcActionFlow = DefaultOidcActionFlow()
|
|
||||||
val presenter = LoginRootPresenter(
|
|
||||||
authenticationService,
|
|
||||||
oidcActionFlow,
|
|
||||||
)
|
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME))
|
|
||||||
initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD))
|
|
||||||
skipItems(1)
|
|
||||||
val loginAndPasswordState = awaitItem()
|
|
||||||
authenticationService.givenLoginError(A_THROWABLE)
|
|
||||||
loginAndPasswordState.eventSink.invoke(LoginRootEvents.Submit)
|
|
||||||
val submitState = awaitItem()
|
|
||||||
assertThat(submitState.loggedInState).isEqualTo(LoggedInState.LoggingIn)
|
|
||||||
val loggedInState = awaitItem()
|
|
||||||
assertThat(loggedInState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - clear error`() = runTest {
|
|
||||||
val authenticationService = FakeAuthenticationService()
|
|
||||||
val oidcActionFlow = DefaultOidcActionFlow()
|
|
||||||
val presenter = LoginRootPresenter(
|
|
||||||
authenticationService,
|
|
||||||
oidcActionFlow,
|
|
||||||
)
|
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
// Submit will return an error
|
|
||||||
authenticationService.givenLoginError(A_THROWABLE)
|
|
||||||
initialState.eventSink(LoginRootEvents.Submit)
|
|
||||||
awaitItem() // Skip LoggingIn state
|
|
||||||
|
|
||||||
// Check an error was returned
|
|
||||||
val submittedState = awaitItem()
|
|
||||||
assertThat(submittedState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE))
|
|
||||||
|
|
||||||
// Assert the error is then cleared
|
|
||||||
submittedState.eventSink(LoginRootEvents.ClearError)
|
|
||||||
val clearedState = awaitItem()
|
|
||||||
assertThat(clearedState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -22,5 +22,5 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
public fun textFieldState(stateValue: String): MutableState<String> =
|
fun textFieldState(stateValue: String): MutableState<String> =
|
||||||
remember(stateValue) { mutableStateOf(stateValue) }
|
remember(stateValue) { mutableStateOf(stateValue) }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue