diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/request/ChatRequestsViewModel.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/request/ChatRequestsViewModel.kt index f3764039e5..acaf7ebd4f 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/request/ChatRequestsViewModel.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/request/ChatRequestsViewModel.kt @@ -66,6 +66,12 @@ internal class ChatRequestsViewModel @Inject constructor( /** The deck, front card first. Empty hides it. */ val deck: StateFlow> = _deck.asStateFlow() + // Above init: viewModelScope runs on Main.immediate, so the deck collector publishes before + // the constructor returns, and fields declared after init would still be null. + private var shownNow = _now.value + private var settle: Job? = null + private var settledDeck: List = emptyList() + init { viewModelScope.launch { combine( @@ -89,10 +95,6 @@ internal class ChatRequestsViewModel @Inject constructor( } } - private var shownNow = _now.value - private var settle: Job? = null - private var settledDeck: List = emptyList() - /** * Puts [next] on the deck. A request ending moves it in the sort order; the deck keeps its * order for the ended look's fade first, and only then lets the list slide it into place. diff --git a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/request/ChatRequestsViewModelTest.kt b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/request/ChatRequestsViewModelTest.kt new file mode 100644 index 0000000000..5b143e3bfc --- /dev/null +++ b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/request/ChatRequestsViewModelTest.kt @@ -0,0 +1,55 @@ +package com.flipcash.app.messenger.internal.request + +import com.flipcash.services.user.UserManager +import com.flipcash.shared.chat.ChatCoordinator +import com.flipcash.shared.chat.PaymentRequests +import androidx.lifecycle.ViewModelStore +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +/** + * `viewModelScope` runs on `Main.immediate`, so the deck collector emits inside the constructor. + * An unconfined main dispatcher reproduces that: the ViewModel has to be fully initialized by then. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class ChatRequestsViewModelTest { + + private val requests = mockk(relaxed = true) { + every { state } returns MutableStateFlow(emptyMap()) + every { missing } returns MutableStateFlow(emptySet()) + } + private val chatCoordinator = mockk(relaxed = true) + private val userManager = mockk(relaxed = true) + + @Before + fun setUp() { + Dispatchers.setMain(UnconfinedTestDispatcher()) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `constructs when the deck collector emits in place`() = runTest { + val store = ViewModelStore() + val viewModel = ChatRequestsViewModel(requests, chatCoordinator, userManager) + store.put("requests", viewModel) + + assertTrue(viewModel.deck.value.isEmpty()) + // Cancels the minute ticker, which would otherwise keep runTest from going idle. + store.clear() + } +}