diff --git a/feature-splash/build.gradle b/feature-splash/build.gradle index 1060b3ad..be3c72d5 100644 --- a/feature-splash/build.gradle +++ b/feature-splash/build.gradle @@ -33,6 +33,7 @@ dependencies { testImplementation jUnitDep testImplementation mockitoDep + testImplementation project(':test-shared') implementation coroutinesDep implementation coroutinesAndroidDep diff --git a/feature-splash/src/main/java/io/novafoundation/nova/splash/presentation/SplashViewModel.kt b/feature-splash/src/main/java/io/novafoundation/nova/splash/presentation/SplashViewModel.kt index 160dcf9d..0046f717 100644 --- a/feature-splash/src/main/java/io/novafoundation/nova/splash/presentation/SplashViewModel.kt +++ b/feature-splash/src/main/java/io/novafoundation/nova/splash/presentation/SplashViewModel.kt @@ -5,6 +5,7 @@ import io.novafoundation.nova.common.base.BaseViewModel import io.novafoundation.nova.common.utils.splash.SplashPassedObserver import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.splash.SplashRouter +import kotlinx.coroutines.Job import kotlinx.coroutines.launch class SplashViewModel( @@ -13,8 +14,16 @@ class SplashViewModel( private val splashPassedObserver: SplashPassedObserver ) : BaseViewModel() { + private var initialDestinationJob: Job? = null + + // Called from the splash screen's onResume, which runs again when the activity is + // recreated (rotation, theme, locale) while this view model survives. Starting a + // second navigation then finds the first one already on the welcome screen, which + // has no route to welcome, and crashes the app. The first call decides. fun openInitialDestination() { - viewModelScope.launch { + if (initialDestinationJob != null) return + + initialDestinationJob = viewModelScope.launch { if (repository.isAccountSelected()) { openPinCode() } else { diff --git a/feature-splash/src/test/java/io/novafoundation/nova/splash/presentation/SplashViewModelTest.kt b/feature-splash/src/test/java/io/novafoundation/nova/splash/presentation/SplashViewModelTest.kt new file mode 100644 index 00000000..84a29240 --- /dev/null +++ b/feature-splash/src/test/java/io/novafoundation/nova/splash/presentation/SplashViewModelTest.kt @@ -0,0 +1,49 @@ +package io.novafoundation.nova.splash.presentation + +import io.novafoundation.nova.common.utils.splash.SplashPassedObserver +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.splash.SplashRouter +import io.novafoundation.nova.test_shared.whenever +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@OptIn(ExperimentalCoroutinesApi::class) +class SplashViewModelTest { + + private val router = mock(SplashRouter::class.java) + private val repository = mock(AccountRepository::class.java) + private val splashPassedObserver = mock(SplashPassedObserver::class.java) + + @Before + fun setUp() { + Dispatchers.setMain(UnconfinedTestDispatcher()) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + // The splash screen asks again on every onResume, including the one after the + // activity is recreated; navigating twice crashed the app on the welcome screen. + @Test + fun `navigates to the welcome screen once however often it is asked`() { + runBlocking { whenever(repository.isAccountSelected()).thenReturn(false) } + val viewModel = SplashViewModel(router, repository, splashPassedObserver) + + viewModel.openInitialDestination() + viewModel.openInitialDestination() + + verify(router, times(1)).openWelcomeScreen() + } +}