diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt index 6e7570b9db..ccf45ce1e9 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt @@ -12,6 +12,7 @@ import org.dhis2.android.rtsm.utils.NetworkUtils import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.resources.ResourceManager +import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.ui.ThemeManager import org.dhis2.usescases.teiDashboard.DashboardRepositoryImpl import org.dhis2.usescases.teiDashboard.DashboardViewModel @@ -50,6 +51,8 @@ class TeiDashboardMobileActivityTest { private var repository: DashboardRepositoryImpl = mock { } + + private var dispatcher:DispatcherProvider = mock() var tei = Observable.just( TrackedEntityInstance.builder() .uid(TEI_Uid) @@ -87,7 +90,8 @@ class TeiDashboardMobileActivityTest { private fun initViewModel() { viewModel = DashboardViewModel( repository, - analyticsHelper + analyticsHelper, + dispatcher, ) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt index a90c7245b6..ce5fbd6d61 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt @@ -4,11 +4,11 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.utils.AuthorityException import org.dhis2.utils.analytics.ACTIVE_FOLLOW_UP import org.dhis2.utils.analytics.AnalyticsHelper @@ -21,6 +21,7 @@ import timber.log.Timber class DashboardViewModel( private val repository: DashboardRepository, private val analyticsHelper: AnalyticsHelper, + private val dispatcher: DispatcherProvider, ) : ViewModel() { private val eventUid = MutableLiveData() @@ -52,8 +53,8 @@ class DashboardViewModel( } private fun fetchDashboardModel() { - viewModelScope.launch { - val result = async(context = Dispatchers.IO) { + viewModelScope.launch(dispatcher.io()) { + val result = async { repository.getDashboardModel() } try { @@ -74,8 +75,8 @@ class DashboardViewModel( } private fun fetchGrouping() { - viewModelScope.launch { - val result = async(context = Dispatchers.IO) { + viewModelScope.launch(dispatcher.io()) { + val result = async { repository.getGrouping() } try { @@ -110,7 +111,7 @@ class DashboardViewModel( repository.setFollowUp((dashboardModel.value as DashboardEnrollmentModel).currentEnrollment.uid()) _syncNeeded.value = true _state.value = State.TO_UPDATE - analyticsHelper.setEvent(ACTIVE_FOLLOW_UP, _showFollowUpBar.toString(), FOLLOW_UP) + analyticsHelper.setEvent(ACTIVE_FOLLOW_UP, _showFollowUpBar.value.toString(), FOLLOW_UP) updateDashboard() } } @@ -137,8 +138,8 @@ class DashboardViewModel( } fun deleteEnrollment(onAuthorityError: () -> Unit) { - viewModelScope.launch { - val result = async(context = Dispatchers.IO) { + viewModelScope.launch(dispatcher.io()) { + val result = async { dashboardModel.value.takeIf { it is DashboardEnrollmentModel }?.let { repository.deleteEnrollmentIfPossible((it as DashboardEnrollmentModel).currentEnrollment.uid()) .blockingGet() diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModelFactory.kt index 8ab87ac303..5f632375b8 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModelFactory.kt @@ -2,18 +2,21 @@ package org.dhis2.usescases.teiDashboard import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.utils.analytics.AnalyticsHelper @Suppress("UNCHECKED_CAST") class DashboardViewModelFactory( val repository: DashboardRepository, val analyticsHelper: AnalyticsHelper, + val dispatcher: DispatcherProvider, ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return DashboardViewModel( repository, analyticsHelper, + dispatcher, ) as T } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt index bc4407e8c5..651c9c7681 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt @@ -7,6 +7,7 @@ import org.dhis2.commons.di.dagger.PerActivity import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.schedulers.SchedulerProvider +import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.forms.EnrollmentFormRepository import org.dhis2.data.forms.FormRepository import org.dhis2.data.forms.dataentry.EnrollmentRuleEngineRepository @@ -123,7 +124,8 @@ class TeiDashboardModule( fun providesViewModelFactory( repository: DashboardRepository, analyticsHelper: AnalyticsHelper, + dispatcher: DispatcherProvider, ): DashboardViewModelFactory { - return DashboardViewModelFactory(repository, analyticsHelper) + return DashboardViewModelFactory(repository, analyticsHelper, dispatcher) } } diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/DashboardViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/DashboardViewModelTest.kt new file mode 100644 index 0000000000..952b190c9a --- /dev/null +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/DashboardViewModelTest.kt @@ -0,0 +1,188 @@ +package org.dhis2.usescases.teiDashboard + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import io.reactivex.Observable +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.setMain +import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.utils.analytics.ACTIVE_FOLLOW_UP +import org.dhis2.utils.analytics.AnalyticsHelper +import org.dhis2.utils.analytics.FOLLOW_UP +import org.hisp.dhis.android.core.common.State +import org.hisp.dhis.android.core.enrollment.Enrollment +import org.hisp.dhis.android.core.enrollment.EnrollmentStatus +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class DashboardViewModelTest { + + @get:Rule + val instantExecutorRule = InstantTaskExecutorRule() + + private val repository: DashboardRepository = mock() + private val analyticsHelper: AnalyticsHelper = mock() + private val testingDispatcher = StandardTestDispatcher() + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + Dispatchers.setMain(testingDispatcher) + } + + @Test + fun shouldFetchEnrollmentModel() { + mockEnrollmentModel() + mockGrouping(true) + + val dashboardViewModel = getViewModel() + + with(dashboardViewModel) { + assertTrue(dashboardModel.value == mockedEnrollmentModel) + assertTrue(showFollowUpBar.value) + assertTrue(!syncNeeded.value) + assertTrue(showStatusBar.value == EnrollmentStatus.ACTIVE) + assertTrue(state.value == State.SYNCED) + } + } + + @Test + fun shouldFetchTeiModel() { + mockTeiModel() + mockGrouping(false) + + val dashboardViewModel = getViewModel() + + with(dashboardViewModel) { + assertTrue(dashboardModel.value == mockedTeiModel) + assertTrue(!showFollowUpBar.value) + assertTrue(!syncNeeded.value) + assertTrue(showStatusBar.value == null) + assertTrue(state.value == null) + } + } + + @Test + fun shouldSetGrouping() { + mockEnrollmentModel() + mockGrouping(false) + + val dashboardViewModel = getViewModel() + + with(dashboardViewModel) { + assertTrue(groupByStage.value == false) + setGrouping(true) + verify(repository).setGrouping(true) + assertTrue(groupByStage.value == true) + } + } + + @Test + fun shouldUpdateEventUid() { + mockEnrollmentModel() + mockGrouping(false) + + with(getViewModel()) { + assertTrue(eventUid().value == null) + updateEventUid("eventUid") + assertTrue(eventUid().value == "eventUid") + } + } + + @Test + fun shouldSetFollowUpOnEnrollment() { + mockEnrollmentModel() + mockGrouping(false) + + with(getViewModel()) { + onFollowUp() + verify(repository).setFollowUp("enrollmentUid") + assertTrue(state.value == State.TO_UPDATE) + verify(analyticsHelper).setEvent(ACTIVE_FOLLOW_UP, "false", FOLLOW_UP) + } + } + + @Test + fun shouldUpdateEnrollmentStatus() { + mockEnrollmentModel() + mockGrouping(false) + + with(getViewModel()) { + whenever(repository.updateEnrollmentStatus(any(), any())) doReturn Observable.just( + StatusChangeResultCode.CHANGED, + ) + updateEnrollmentStatus(EnrollmentStatus.COMPLETED) + verify(repository).updateEnrollmentStatus("enrollmentUid", EnrollmentStatus.COMPLETED) + assertTrue(showStatusBar.value == EnrollmentStatus.COMPLETED) + assertTrue(syncNeeded.value) + assertTrue(state.value == State.TO_UPDATE) + assertTrue(updateEnrollment.value == true) + } + } + + @Test + fun shouldShowMessageIfErrorWhileUpdatingEnrollmentStatus() { + mockEnrollmentModel() + mockGrouping(false) + + with(getViewModel()) { + whenever(repository.updateEnrollmentStatus(any(), any())) doReturn Observable.just( + StatusChangeResultCode.FAILED, + ) + updateEnrollmentStatus(EnrollmentStatus.COMPLETED) + assertTrue(showStatusErrorMessages.value == StatusChangeResultCode.FAILED) + } + } + + private fun getViewModel() = DashboardViewModel( + repository, + analyticsHelper, + object : + DispatcherProvider { + override fun io(): CoroutineDispatcher { + return testingDispatcher + } + + override fun computation(): CoroutineDispatcher { + return testingDispatcher + } + + override fun ui(): CoroutineDispatcher { + return testingDispatcher + } + }, + ).also { + testingDispatcher.scheduler.advanceUntilIdle() + } + + private fun mockEnrollmentModel() { + whenever(repository.getDashboardModel()) doReturn mockedEnrollmentModel + whenever(mockedEnrollmentModel.currentEnrollment) doReturn mockedEnrollment + } + + private fun mockTeiModel() { + whenever(repository.getDashboardModel()) doReturn mockedTeiModel + } + + private fun mockGrouping(group: Boolean) { + whenever(repository.getGrouping()) doReturn group + } + + private val mockedEnrollmentModel: DashboardEnrollmentModel = mock() + private val mockedTeiModel: DashboardTEIModel = mock() + private val mockedEnrollment: Enrollment = mock { + on { uid() } doReturn "enrollmentUid" + on { followUp() } doReturn true + on { aggregatedSyncState() } doReturn State.SYNCED + on { status() } doReturn EnrollmentStatus.ACTIVE + } +}