Skip to content

Commit

Permalink
Let UI initiate ViewModel actions
Browse files Browse the repository at this point in the history
  • Loading branch information
LaurentTreguier committed Nov 12, 2024
1 parent 2d3cc9b commit a2467d0
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -135,6 +134,10 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
icon = Icons.Outlined.Code
)
}

LaunchedEffect(viewModel) {
viewModel.loadCurrentUser()
}
}
}

Expand All @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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..<minLength) {
Expand Down Expand Up @@ -79,7 +82,7 @@ class LoginViewModelTests : TestsBase() {
runCurrent()
viewModel.submit()
runCurrent()
assertEquals(0, eventBus.storedEvents.filterIsInstance<Event.Failure>().count())
assertTrue(eventBus.storedEvents.filterIsInstance<Event.Failure>().isEmpty())
assertTrue(viewModel.isWaitingForRandomCode.value)
}

Expand All @@ -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)
Expand Down Expand Up @@ -154,11 +161,11 @@ class LoginViewModelTests : TestsBase() {
runCurrent()
viewModel.submit()
runCurrent()
assertEquals(0, eventBus.storedEvents.filterIsInstance<Event.Failure>().count())
assertTrue(eventBus.storedEvents.filterIsInstance<Event.Failure>().isEmpty())
}

private fun TestScope.makeViewModel(
eventBus: EventBus,
private fun makeViewModel(
eventBus: EventBus = FakeEventBus(),
identifierMinLength: Int,
identifierMaxLength: Int,
randomCodeMinLength: Int
Expand All @@ -175,5 +182,5 @@ class LoginViewModelTests : TestsBase() {
storeResolver = FakeStoreResolver(),
secretsHandler = FakeSecretsHandler(),
apiResolver = FakeApiResolver()
).also { runCurrent() }
)
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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() }
Expand All @@ -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() }
Expand All @@ -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() }
Expand All @@ -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() }
Expand Down Expand Up @@ -170,13 +169,13 @@ class RegisterViewModelTests : TestsBase() {
runCurrent()
viewModel.submit()
runCurrent()
assertEquals(0, eventBus.storedEvents.filterIsInstance<Event.Failure>().count())
assertTrue(eventBus.storedEvents.filterIsInstance<Event.Failure>().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)
Expand Down Expand Up @@ -231,10 +230,10 @@ class RegisterViewModelTests : TestsBase() {
runCurrent()
viewModel.submit()
runCurrent()
assertEquals(0, eventBus.storedEvents.filterIsInstance<Event.Failure>().count())
assertTrue(eventBus.storedEvents.filterIsInstance<Event.Failure>().isEmpty())
}

private fun TestScope.makeViewModel(eventBus: EventBus): Triple<Int, Int, RegisterViewModel> {
private fun makeViewModel(eventBus: EventBus = FakeEventBus()): Triple<Int, Int, RegisterViewModel> {
val minLength = 5
val maxLength = 100
val resources = FakeResourceResolver(
Expand All @@ -254,7 +253,6 @@ class RegisterViewModelTests : TestsBase() {
secretsHandler = FakeSecretsHandler(),
apiResolver = FakeApiResolver()
)
runCurrent()
return Triple(minLength, maxLength, viewModel)
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -28,19 +28,22 @@ 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<Event.Failure>().isEmpty())
assertNotNull(viewModel.currentUser.value)
}

@Test
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() }

Expand All @@ -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() }

Expand All @@ -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<Event.Failure>().count())
assertTrue(eventBus.storedEvents.filterIsInstance<Event.Failure>().isEmpty())
assertEquals(
FakeUsersEndpointApi.NORMAL_IMAGE_FILE.path,
viewModel.currentUser.value?.avatar
Expand All @@ -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<Event.Failure>().count())
assertTrue(eventBus.storedEvents.filterIsInstance<Event.Failure>().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() }

Expand All @@ -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() }

Expand All @@ -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<Event.Failure>().count())
assertTrue(eventBus.storedEvents.filterIsInstance<Event.Failure>().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,
Expand Down

0 comments on commit a2467d0

Please sign in to comment.