From a2467d028c127ff4c906268a2a7a41574ee51e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 12 Nov 2024 14:11:27 +0100 Subject: [PATCH] Let UI initiate ViewModel actions --- .../fyreplace/ui/screens/SettingsScreen.kt | 12 ++++----- .../viewmodels/screens/SettingsViewModel.kt | 20 +++----------- .../LoginViewModelTests.kt | 25 ++++++++++------- .../MainViewModelTests.kt | 2 +- .../RegisterViewModelTests.kt | 20 +++++++------- .../SettingsViewModelTests.kt | 27 ++++++++++++------- 6 files changed, 54 insertions(+), 52 deletions(-) rename app/src/test/kotlin/app/fyreplace/fyreplace/test/{screens => viewmodels}/LoginViewModelTests.kt (90%) rename app/src/test/kotlin/app/fyreplace/fyreplace/test/{screens => viewmodels}/MainViewModelTests.kt (96%) rename app/src/test/kotlin/app/fyreplace/fyreplace/test/{screens => viewmodels}/RegisterViewModelTests.kt (92%) rename app/src/test/kotlin/app/fyreplace/fyreplace/test/{screens => viewmodels}/SettingsViewModelTests.kt (84%) diff --git a/app/src/main/kotlin/app/fyreplace/fyreplace/ui/screens/SettingsScreen.kt b/app/src/main/kotlin/app/fyreplace/fyreplace/ui/screens/SettingsScreen.kt index ee0d15e..9ce0550 100644 --- a/app/src/main/kotlin/app/fyreplace/fyreplace/ui/screens/SettingsScreen.kt +++ b/app/src/main/kotlin/app/fyreplace/fyreplace/ui/screens/SettingsScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.ListItem import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource @@ -30,14 +31,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.compose.collectAsStateWithLifecycle -import app.fyreplace.api.data.User import app.fyreplace.fyreplace.R import app.fyreplace.fyreplace.extensions.codePointCount import app.fyreplace.fyreplace.fakes.FakeApiResolver import app.fyreplace.fyreplace.fakes.FakeEventBus import app.fyreplace.fyreplace.fakes.FakeResourceResolver import app.fyreplace.fyreplace.fakes.FakeStoreResolver -import app.fyreplace.fyreplace.fakes.placeholder import app.fyreplace.fyreplace.ui.theme.AppTheme import app.fyreplace.fyreplace.ui.views.settings.AvatarListItem import app.fyreplace.fyreplace.ui.views.settings.LinkListItem @@ -135,6 +134,10 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) { icon = Icons.Outlined.Code ) } + + LaunchedEffect(viewModel) { + viewModel.loadCurrentUser() + } } } @@ -144,10 +147,7 @@ fun SettingsScreenPreview() { AppTheme { SettingsScreen( viewModel = SettingsViewModel( - SavedStateHandle().apply { - this[SettingsViewModel::currentUser.name] = User.placeholder - this[SettingsViewModel::bio.name] = User.placeholder.bio - }, + SavedStateHandle(), FakeEventBus(), FakeResourceResolver(mapOf(R.integer.bio_max_length to 100)), FakeStoreResolver(), diff --git a/app/src/main/kotlin/app/fyreplace/fyreplace/viewmodels/screens/SettingsViewModel.kt b/app/src/main/kotlin/app/fyreplace/fyreplace/viewmodels/screens/SettingsViewModel.kt index 954cb8a..50af6da 100644 --- a/app/src/main/kotlin/app/fyreplace/fyreplace/viewmodels/screens/SettingsViewModel.kt +++ b/app/src/main/kotlin/app/fyreplace/fyreplace/viewmodels/screens/SettingsViewModel.kt @@ -17,9 +17,6 @@ import app.fyreplace.fyreplace.viewmodels.ApiViewModelBase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.io.File import javax.inject.Inject @@ -44,19 +41,10 @@ class SettingsViewModel @Inject constructor( } .asState(false) - init { - viewModelScope.launch { - storeResolver.secretsStore.data - .map { it.token } - .distinctUntilChanged() - .filter { !it.isEmpty } - .collect { - call(apiResolver::users) { - state[::currentUser.name] = getCurrentUser().require() - state[::bio.name] = currentUser.value?.bio.orEmpty() - } - } - } + + fun loadCurrentUser() = call(apiResolver::users) { + state[::currentUser.name] = getCurrentUser().require() + state[::bio.name] = currentUser.value?.bio.orEmpty() } fun updateAvatar(file: File) = call(apiResolver::users) { diff --git a/app/src/test/kotlin/app/fyreplace/fyreplace/test/screens/LoginViewModelTests.kt b/app/src/test/kotlin/app/fyreplace/fyreplace/test/viewmodels/LoginViewModelTests.kt similarity index 90% rename from app/src/test/kotlin/app/fyreplace/fyreplace/test/screens/LoginViewModelTests.kt rename to app/src/test/kotlin/app/fyreplace/fyreplace/test/viewmodels/LoginViewModelTests.kt index fbff83f..12b2969 100644 --- a/app/src/test/kotlin/app/fyreplace/fyreplace/test/screens/LoginViewModelTests.kt +++ b/app/src/test/kotlin/app/fyreplace/fyreplace/test/viewmodels/LoginViewModelTests.kt @@ -1,4 +1,4 @@ -package app.fyreplace.fyreplace.test.screens +package app.fyreplace.fyreplace.test.viewmodels import androidx.lifecycle.SavedStateHandle import app.fyreplace.fyreplace.R @@ -16,7 +16,6 @@ import app.fyreplace.fyreplace.viewmodels.screens.LoginViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -30,7 +29,11 @@ class LoginViewModelTests : TestsBase() { fun `Identifier must have correct length`() = runTest { val minLength = 5 val maxLength = 100 - val viewModel = makeViewModel(FakeEventBus(), minLength, maxLength, 8) + val viewModel = makeViewModel( + identifierMinLength = minLength, + identifierMaxLength = maxLength, + randomCodeMinLength = 8 + ) backgroundScope.launch { viewModel.canSubmit.collect() } for (i in 0..().count()) + assertTrue(eventBus.storedEvents.filterIsInstance().isEmpty()) assertTrue(viewModel.isWaitingForRandomCode.value) } @@ -102,7 +105,11 @@ class LoginViewModelTests : TestsBase() { @Test fun `Random code must have correct length`() = runTest { val minLength = 10 - val viewModel = makeViewModel(FakeEventBus(), 3, 50, minLength) + val viewModel = makeViewModel( + identifierMinLength = 3, + identifierMaxLength = 50, + randomCodeMinLength = minLength + ) backgroundScope.launch { viewModel.canSubmit.collect() } viewModel.updateIdentifier(FakeUsersEndpointApi.GOOD_USERNAME) @@ -154,11 +161,11 @@ class LoginViewModelTests : TestsBase() { runCurrent() viewModel.submit() runCurrent() - assertEquals(0, eventBus.storedEvents.filterIsInstance().count()) + assertTrue(eventBus.storedEvents.filterIsInstance().isEmpty()) } - private fun TestScope.makeViewModel( - eventBus: EventBus, + private fun makeViewModel( + eventBus: EventBus = FakeEventBus(), identifierMinLength: Int, identifierMaxLength: Int, randomCodeMinLength: Int @@ -175,5 +182,5 @@ class LoginViewModelTests : TestsBase() { storeResolver = FakeStoreResolver(), secretsHandler = FakeSecretsHandler(), apiResolver = FakeApiResolver() - ).also { runCurrent() } + ) } diff --git a/app/src/test/kotlin/app/fyreplace/fyreplace/test/screens/MainViewModelTests.kt b/app/src/test/kotlin/app/fyreplace/fyreplace/test/viewmodels/MainViewModelTests.kt similarity index 96% rename from app/src/test/kotlin/app/fyreplace/fyreplace/test/screens/MainViewModelTests.kt rename to app/src/test/kotlin/app/fyreplace/fyreplace/test/viewmodels/MainViewModelTests.kt index 5a597f4..4be94eb 100644 --- a/app/src/test/kotlin/app/fyreplace/fyreplace/test/screens/MainViewModelTests.kt +++ b/app/src/test/kotlin/app/fyreplace/fyreplace/test/viewmodels/MainViewModelTests.kt @@ -1,4 +1,4 @@ -package app.fyreplace.fyreplace.test.screens +package app.fyreplace.fyreplace.test.viewmodels import androidx.lifecycle.SavedStateHandle import app.fyreplace.fyreplace.events.Event diff --git a/app/src/test/kotlin/app/fyreplace/fyreplace/test/screens/RegisterViewModelTests.kt b/app/src/test/kotlin/app/fyreplace/fyreplace/test/viewmodels/RegisterViewModelTests.kt similarity index 92% rename from app/src/test/kotlin/app/fyreplace/fyreplace/test/screens/RegisterViewModelTests.kt rename to app/src/test/kotlin/app/fyreplace/fyreplace/test/viewmodels/RegisterViewModelTests.kt index 15d3e58..2d433e0 100644 --- a/app/src/test/kotlin/app/fyreplace/fyreplace/test/screens/RegisterViewModelTests.kt +++ b/app/src/test/kotlin/app/fyreplace/fyreplace/test/viewmodels/RegisterViewModelTests.kt @@ -1,4 +1,4 @@ -package app.fyreplace.fyreplace.test.screens +package app.fyreplace.fyreplace.test.viewmodels import androidx.lifecycle.SavedStateHandle import app.fyreplace.fyreplace.R @@ -16,7 +16,6 @@ import app.fyreplace.fyreplace.viewmodels.screens.RegisterViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -28,7 +27,7 @@ import org.junit.Test class RegisterViewModelTests : TestsBase() { @Test fun `Username must have correct length`() = runTest { - val (minLength, maxLength, viewModel) = makeViewModel(FakeEventBus()) + val (minLength, maxLength, viewModel) = makeViewModel() viewModel.updateEmail(FakeUsersEndpointApi.GOOD_EMAIL) viewModel.updateHasAcceptedTerms(true) backgroundScope.launch { viewModel.canSubmit.collect() } @@ -53,7 +52,7 @@ class RegisterViewModelTests : TestsBase() { @Test fun `Email must have correct length`() = runTest { - val (minLength, maxLength, viewModel) = makeViewModel(FakeEventBus()) + val (minLength, maxLength, viewModel) = makeViewModel() viewModel.updateUsername(FakeUsersEndpointApi.GOOD_USERNAME) viewModel.updateHasAcceptedTerms(true) backgroundScope.launch { viewModel.canSubmit.collect() } @@ -78,7 +77,7 @@ class RegisterViewModelTests : TestsBase() { @Test fun `Email must have @`() = runTest { - val (_, _, viewModel) = makeViewModel(FakeEventBus()) + val (_, _, viewModel) = makeViewModel() viewModel.updateUsername(FakeUsersEndpointApi.GOOD_USERNAME) viewModel.updateHasAcceptedTerms(true) backgroundScope.launch { viewModel.canSubmit.collect() } @@ -94,7 +93,7 @@ class RegisterViewModelTests : TestsBase() { @Test fun `Terms must be accepted`() = runTest { - val (_, _, viewModel) = makeViewModel(FakeEventBus()) + val (_, _, viewModel) = makeViewModel() viewModel.updateUsername(FakeUsersEndpointApi.GOOD_USERNAME) viewModel.updateEmail(FakeUsersEndpointApi.GOOD_EMAIL) backgroundScope.launch { viewModel.canSubmit.collect() } @@ -170,13 +169,13 @@ class RegisterViewModelTests : TestsBase() { runCurrent() viewModel.submit() runCurrent() - assertEquals(0, eventBus.storedEvents.filterIsInstance().count()) + assertTrue(eventBus.storedEvents.filterIsInstance().isEmpty()) assertTrue(viewModel.isWaitingForRandomCode.value) } @Test fun `Random code must have correct length`() = runTest { - val (minLength, _, viewModel) = makeViewModel(FakeEventBus()) + val (minLength, _, viewModel) = makeViewModel() backgroundScope.launch { viewModel.canSubmit.collect() } viewModel.updateUsername(FakeUsersEndpointApi.GOOD_USERNAME) @@ -231,10 +230,10 @@ class RegisterViewModelTests : TestsBase() { runCurrent() viewModel.submit() runCurrent() - assertEquals(0, eventBus.storedEvents.filterIsInstance().count()) + assertTrue(eventBus.storedEvents.filterIsInstance().isEmpty()) } - private fun TestScope.makeViewModel(eventBus: EventBus): Triple { + private fun makeViewModel(eventBus: EventBus = FakeEventBus()): Triple { val minLength = 5 val maxLength = 100 val resources = FakeResourceResolver( @@ -254,7 +253,6 @@ class RegisterViewModelTests : TestsBase() { secretsHandler = FakeSecretsHandler(), apiResolver = FakeApiResolver() ) - runCurrent() return Triple(minLength, maxLength, viewModel) } } diff --git a/app/src/test/kotlin/app/fyreplace/fyreplace/test/screens/SettingsViewModelTests.kt b/app/src/test/kotlin/app/fyreplace/fyreplace/test/viewmodels/SettingsViewModelTests.kt similarity index 84% rename from app/src/test/kotlin/app/fyreplace/fyreplace/test/screens/SettingsViewModelTests.kt rename to app/src/test/kotlin/app/fyreplace/fyreplace/test/viewmodels/SettingsViewModelTests.kt index 7907923..621c510 100644 --- a/app/src/test/kotlin/app/fyreplace/fyreplace/test/screens/SettingsViewModelTests.kt +++ b/app/src/test/kotlin/app/fyreplace/fyreplace/test/viewmodels/SettingsViewModelTests.kt @@ -1,4 +1,4 @@ -package app.fyreplace.fyreplace.test.screens +package app.fyreplace.fyreplace.test.viewmodels import androidx.lifecycle.SavedStateHandle import app.fyreplace.fyreplace.R @@ -28,12 +28,14 @@ import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) class SettingsViewModelTests : TestsBase() { @Test - fun `ViewModel retrieves current user`() = runTest { + fun `Loading current user produces no failures`() = runTest { val eventBus = FakeEventBus() val viewModel = makeViewModel(eventBus) backgroundScope.launch { viewModel.currentUser.collect() } + viewModel.loadCurrentUser() runCurrent() + assertTrue(eventBus.storedEvents.filterIsInstance().isEmpty()) assertNotNull(viewModel.currentUser.value) } @@ -41,6 +43,7 @@ class SettingsViewModelTests : TestsBase() { fun `Updating avatar with a too large image produces a failure`() = runTest { val eventBus = FakeEventBus() val viewModel = makeViewModel(eventBus) + viewModel.loadCurrentUser() runCurrent() backgroundScope.launch { viewModel.currentUser.collect() } @@ -54,6 +57,7 @@ class SettingsViewModelTests : TestsBase() { fun `Updating avatar with an invalid produces a failure`() = runTest { val eventBus = FakeEventBus() val viewModel = makeViewModel(eventBus) + viewModel.loadCurrentUser() runCurrent() backgroundScope.launch { viewModel.currentUser.collect() } @@ -67,12 +71,13 @@ class SettingsViewModelTests : TestsBase() { fun `Updating avatar with a valid image produces no failures`() = runTest { val eventBus = FakeEventBus() val viewModel = makeViewModel(eventBus) + viewModel.loadCurrentUser() runCurrent() backgroundScope.launch { viewModel.currentUser.collect() } viewModel.updateAvatar(FakeUsersEndpointApi.NORMAL_IMAGE_FILE) runCurrent() - assertEquals(0, eventBus.storedEvents.filterIsInstance().count()) + assertTrue(eventBus.storedEvents.filterIsInstance().isEmpty()) assertEquals( FakeUsersEndpointApi.NORMAL_IMAGE_FILE.path, viewModel.currentUser.value?.avatar @@ -83,20 +88,22 @@ class SettingsViewModelTests : TestsBase() { fun `Removing avatar produces no failures`() = runTest { val eventBus = FakeEventBus() val viewModel = makeViewModel(eventBus) - runCurrent() + viewModel.loadCurrentUser() viewModel.updateAvatar(FakeUsersEndpointApi.NORMAL_IMAGE_FILE) + runCurrent() backgroundScope.launch { viewModel.currentUser.collect() } viewModel.removeAvatar() runCurrent() - assertEquals(0, eventBus.storedEvents.filterIsInstance().count()) + assertTrue(eventBus.storedEvents.filterIsInstance().isEmpty()) assertEquals("", viewModel.currentUser.value?.avatar) } @Test fun `Bio must have correct length`() = runTest { val maxLength = 30 - val viewModel = makeViewModel(FakeEventBus(), maxLength) + val viewModel = makeViewModel(bioMaxLength = maxLength) + viewModel.loadCurrentUser() runCurrent() backgroundScope.launch { viewModel.canUpdateBio.collect() } @@ -113,7 +120,8 @@ class SettingsViewModelTests : TestsBase() { @Test fun `Bio must be different`() = runTest { - val viewModel = makeViewModel(FakeEventBus()) + val viewModel = makeViewModel() + viewModel.loadCurrentUser() runCurrent() backgroundScope.launch { viewModel.canUpdateBio.collect() } @@ -129,17 +137,18 @@ class SettingsViewModelTests : TestsBase() { fun `Updating bio produces no failures`() = runTest { val eventBus = FakeEventBus() val viewModel = makeViewModel(eventBus) + viewModel.loadCurrentUser() runCurrent() backgroundScope.launch { viewModel.currentUser.collect() } viewModel.updatePendingBio("Hello") viewModel.updateBio() runCurrent() - assertEquals(0, eventBus.storedEvents.filterIsInstance().count()) + assertTrue(eventBus.storedEvents.filterIsInstance().isEmpty()) assertEquals("Hello", viewModel.currentUser.value?.bio) } - private suspend fun makeViewModel(eventBus: EventBus, bioMaxLength: Int = 100) = + private suspend fun makeViewModel(eventBus: EventBus = FakeEventBus(), bioMaxLength: Int = 100) = SettingsViewModel( SavedStateHandle(), eventBus,