From ae2439939ff89f158f1c16ed77745eb06f3f2167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Miguel=20Rubio?= Date: Thu, 6 Jun 2024 14:11:41 +0200 Subject: [PATCH] fix: [ANDROAPP-6194] Search outside the program (#3664) * fix: [ANDROAPP-6194] Send fetched list as parameter to avoid duplicated on search Signed-off-by: andresmr * fix: [ANDROAPP-6194] Send fetched list as parameter to avoid duplicated on search Signed-off-by: andresmr * fix: [ANDROAPP-6194] Add mockedWebServer response to mock get tracked entity instances Signed-off-by: andresmr --------- Signed-off-by: andresmr --- .../mockwebserver/MockWebServerRobot.kt | 7 + .../flow/searchFlow/SearchFlowTest.kt | 14 + .../usescases/flow/teiFlow/TeiFlowTest.kt | 15 +- .../dhis2/usescases/searchte/SearchTETest.kt | 46 ++- .../old_tracked_entity_empty_response.json | 62 +++ .../searchTrackEntity/SearchRepository.java | 11 +- .../SearchRepositoryImpl.java | 31 +- .../SearchRepositoryImplKt.kt | 31 +- .../searchTrackEntity/SearchTEIViewModel.kt | 8 +- .../usescases/searchTrackEntity/SearchTEUi.kt | 389 ------------------ .../searchTrackEntity/SearchRepositoryTest.kt | 90 ++++ 11 files changed, 278 insertions(+), 426 deletions(-) create mode 100644 app/src/dhisUITesting/assets/mocks/teilist/old_tracked_entity_empty_response.json delete mode 100644 app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEUi.kt create mode 100644 app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt diff --git a/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt b/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt index dc76fafbb7..eb2c53c7af 100644 --- a/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt +++ b/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt @@ -15,4 +15,11 @@ class MockWebServerRobot(private val dhis2MockServer: Dhis2MockServer) { fun addResponse(method: String, path: String, sdkResource: String, responseCode: Int = 200) { dhis2MockServer.addResponse(method, path, sdkResource, responseCode) } + + companion object { + const val API_OLD_TRACKED_ENTITY_PATH = "/api/trackedEntityInstances/query?.*" + const val API_OLD_TRACKED_ENTITY_RESPONSE = + "mocks/teilist/old_tracked_entity_empty_response.json" + + } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowTest.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowTest.kt index 2807eded53..cf42db37d6 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowTest.kt @@ -7,11 +7,14 @@ import androidx.compose.ui.text.intl.Locale import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.ActivityTestRule import org.dhis2.R +import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_PATH +import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_RESPONSE import org.dhis2.usescases.BaseTest import org.dhis2.usescases.flow.teiFlow.entity.DateRegistrationUIModel import org.dhis2.usescases.flow.teiFlow.entity.RegisterTEIUIModel import org.dhis2.usescases.flow.teiFlow.teiFlowRobot import org.dhis2.usescases.searchTrackEntity.SearchTEActivity +import org.hisp.dhis.android.core.mockwebserver.ResponseController import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -28,8 +31,19 @@ class SearchFlowTest : BaseTest() { private val dateRegistration = createFirstSpecificDate() private val dateEnrollment = createEnrollmentDate() + override fun setUp() { + super.setUp() + setupMockServer() + } + @Test fun shouldCreateTEIAndFilterByEnrollment() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + setDatePicker() val registerTEIDetails = createRegisterTEI() val enrollmentStatus = context.getString(R.string.filters_title_enrollment_status) diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowTest.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowTest.kt index 5b8cf533e8..7fad72127c 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowTest.kt @@ -4,12 +4,15 @@ import android.content.Intent import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.ActivityTestRule +import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_PATH +import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_RESPONSE import org.dhis2.usescases.BaseTest import org.dhis2.usescases.flow.teiFlow.entity.DateRegistrationUIModel import org.dhis2.usescases.flow.teiFlow.entity.EnrollmentListUIModel import org.dhis2.usescases.flow.teiFlow.entity.RegisterTEIUIModel import org.dhis2.usescases.searchTrackEntity.SearchTEActivity import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity +import org.hisp.dhis.android.core.mockwebserver.ResponseController import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -33,8 +36,19 @@ class TeiFlowTest : BaseTest() { private val dateEnrollment = createEnrollmentDate() private val currentDate = getCurrentDate() + override fun setUp() { + super.setUp() + setupMockServer() + } + @Test fun shouldEnrollToSameProgramAfterClosingIt() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + val totalEventsPerEnrollment = 3 val enrollmentListDetails = createEnrollmentList() val registerTeiDetails = createRegisterTEI() @@ -105,6 +119,5 @@ class TeiFlowTest : BaseTest() { const val DATE_FORMAT = "dd/M/yyyy" const val DATE_PICKER_FORMAT = ", d MMMM" - } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt index eadaad1c7b..0ee0ad8421 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt @@ -1,9 +1,7 @@ package org.dhis2.usescases.searchte -import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.intl.Locale @@ -19,6 +17,8 @@ import dispatch.android.espresso.IdlingDispatcherProviderRule import org.dhis2.R import org.dhis2.bindings.app import org.dhis2.common.idlingresources.MapIdlingResource +import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_PATH +import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_RESPONSE import org.dhis2.commons.date.DateUtils.SIMPLE_DATE_FORMAT import org.dhis2.lazyActivityScenarioRule import org.dhis2.ui.dialogs.bottomsheet.SECONDARY_BUTTON_TAG @@ -32,6 +32,7 @@ import org.dhis2.usescases.searchte.entity.DisplayListFieldsUIModel import org.dhis2.usescases.searchte.robot.filterRobot import org.dhis2.usescases.searchte.robot.searchTeiRobot import org.dhis2.usescases.teidashboard.robot.teiDashboardRobot +import org.hisp.dhis.android.core.mockwebserver.ResponseController import org.junit.After import org.junit.Ignore import org.junit.Rule @@ -61,8 +62,19 @@ class SearchTETest : BaseTest() { @get:Rule val composeTestRule = createComposeRule() + override fun setUp() { + super.setUp() + setupMockServer() + } + @Test fun shouldSuccessfullySearchByName() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + val firstName = "Tim" val lastName = "Johnson" @@ -82,6 +94,12 @@ class SearchTETest : BaseTest() { @Test fun shouldShowErrorWhenCanNotFindSearchResult() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + val firstName = "asdssds" prepareTestProgramRulesProgrammeIntentAndLaunchActivity(rule) @@ -97,6 +115,12 @@ class SearchTETest : BaseTest() { @Test fun shouldSuccessfullySearchUsingMoreThanOneField() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + val firstName = "Anna" val lastName = "Jones" @@ -133,6 +157,12 @@ class SearchTETest : BaseTest() { @Test fun shouldCheckDisplayInList() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + val displayInListData = createDisplayListFields() prepareTestAdultWomanProgrammeIntentAndLaunchActivity(rule) @@ -286,6 +316,12 @@ class SearchTETest : BaseTest() { @Test fun shouldSuccessfullyFilterBySync() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + val teiName = "Frank" val teiLastName = "Fjordsen" val syncFilter = context.getString(R.string.action_sync) @@ -321,6 +357,12 @@ class SearchTETest : BaseTest() { @Test fun shouldSuccessfullySearchAndFilter() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + val name = "Anna" val lastName = "Jones" val enrollmentStatus = context.getString(R.string.filters_title_enrollment_status) diff --git a/app/src/dhisUITesting/assets/mocks/teilist/old_tracked_entity_empty_response.json b/app/src/dhisUITesting/assets/mocks/teilist/old_tracked_entity_empty_response.json new file mode 100644 index 0000000000..e5104f914d --- /dev/null +++ b/app/src/dhisUITesting/assets/mocks/teilist/old_tracked_entity_empty_response.json @@ -0,0 +1,62 @@ +{ + "headers": [ + { + "name": "instance", + "column": "Instance", + "type": "java.lang.String", + "hidden": false, + "meta": false + }, + { + "name": "created", + "column": "Created", + "type": "java.lang.String", + "hidden": false, + "meta": false + }, + { + "name": "lastupdated", + "column": "Last updated", + "type": "java.lang.String", + "hidden": false, + "meta": false + }, + { + "name": "ou", + "column": "Organisation unit", + "type": "java.lang.String", + "hidden": false, + "meta": false + }, + { + "name": "ouname", + "column": "Organisation unit name", + "type": "java.lang.String", + "hidden": false, + "meta": false + }, + { + "name": "te", + "column": "Tracked entity type", + "type": "java.lang.String", + "hidden": false, + "meta": false + }, + { + "name": "inactive", + "column": "Inactive", + "type": "java.lang.String", + "hidden": false, + "meta": false + } + ], + "metaData": { + "names": { + "nEenWmSyUEp": "Person" + } + }, + "width": 9, + "height": 0, + "rows": [ + ] +} \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java index ae624c091c..8691eea1fd 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java @@ -6,6 +6,7 @@ import org.dhis2.commons.data.EventViewModel; import org.dhis2.commons.data.SearchTeiModel; import org.dhis2.commons.data.tuples.Pair; +import org.dhis2.commons.filters.FilterManager; import org.dhis2.commons.filters.sorting.SortingItem; import org.dhis2.data.search.SearchParametersModel; import org.hisp.dhis.android.core.arch.call.D2Progress; @@ -17,14 +18,16 @@ import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem; import org.jetbrains.annotations.NotNull; -import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import io.reactivex.Flowable; import io.reactivex.Observable; +import kotlin.Deprecated; +@Deprecated(message = "Use SearchRepositoryKt instead") public interface SearchRepository { Observable> programsWithRegistration(String programTypeId); @@ -82,4 +85,10 @@ public interface SearchRepository { List trackedEntityTypeFields(); boolean filtersApplyOnGlobalSearch(); + + @NotNull HashSet getFetchedTeiUIDs(); + + SearchParametersModel getSavedSearchParameters(); + + FilterManager getSavedFilters(); } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java index ff0f8f0769..070bb93b60 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java @@ -24,8 +24,8 @@ import org.dhis2.commons.filters.sorting.SortingItem; import org.dhis2.commons.network.NetworkUtils; import org.dhis2.commons.reporting.CrashReportController; -import org.dhis2.commons.resources.MetadataIconProvider; import org.dhis2.commons.resources.DhisPeriodUtils; +import org.dhis2.commons.resources.MetadataIconProvider; import org.dhis2.commons.resources.ResourceManager; import org.dhis2.data.dhislogic.DhisEnrollmentUtils; import org.dhis2.data.forms.dataentry.SearchTEIRepository; @@ -43,7 +43,6 @@ import org.dhis2.utils.ValueUtils; import org.hisp.dhis.android.core.D2; import org.hisp.dhis.android.core.arch.call.D2Progress; -import org.hisp.dhis.android.core.arch.helpers.Result; import org.hisp.dhis.android.core.arch.helpers.UidsHelper; import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope; import org.hisp.dhis.android.core.common.FeatureType; @@ -57,7 +56,6 @@ import org.hisp.dhis.android.core.event.Event; import org.hisp.dhis.android.core.event.EventCollectionRepository; import org.hisp.dhis.android.core.event.EventStatus; -import org.hisp.dhis.android.core.maintenance.D2Error; import org.hisp.dhis.android.core.organisationunit.OrganisationUnit; import org.hisp.dhis.android.core.program.Program; import org.hisp.dhis.android.core.program.ProgramStage; @@ -79,7 +77,6 @@ import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem; import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItemAttribute; import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItemHelper; -import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -606,6 +603,21 @@ public boolean filtersApplyOnGlobalSearch() { !FilterManager.getInstance().getStateFilters().isEmpty(); } + @Override + public @NotNull HashSet getFetchedTeiUIDs() { + return fetchedTeiUids; + } + + @Override + public SearchParametersModel getSavedSearchParameters() { + return savedSearchParameters; + } + + @Override + public FilterManager getSavedFilters() { + return savedFilters; + } + @Override public Observable getTrackedEntityType(String trackedEntityUid) { return d2.trackedEntityModule().trackedEntityTypes().uid(trackedEntityUid).get().toObservable(); @@ -697,17 +709,6 @@ public TeiDownloadResult download(String teiUid, @Nullable String enrollmentUid, return teiDownloader.download(teiUid, enrollmentUid, reason); } - public SearchTeiModel transformResult(Result result, @Nullable Program selectedProgram, boolean offlineOnly, SortingItem sortingItem) { - try { - return transform(result.getOrThrow(), selectedProgram, offlineOnly, sortingItem); - } catch (Exception e) { - SearchTeiModel errorModel = new SearchTeiModel(); - errorModel.onlineErrorMessage = resources.parseD2Error(e); - errorModel.onlineErrorCode = ((D2Error) e).errorCode(); - return errorModel; - } - } - @Override public SearchTeiModel transform(TrackedEntitySearchItem searchItem, @Nullable Program selectedProgram, boolean offlineOnly, SortingItem sortingItem) { if (!fetchedTeiUids.contains(searchItem.uid())) { diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt index 20e0b4b7c5..dc1a1470c7 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt @@ -30,14 +30,8 @@ class SearchRepositoryImplKt( private val metadataIconProvider: MetadataIconProvider, ) : SearchRepositoryKt { - private lateinit var savedSearchParamenters: SearchParametersModel - - private lateinit var savedFilters: FilterManager - private lateinit var trackedEntityInstanceQuery: TrackedEntitySearchCollectionRepository - private val fetchedTeiUids = HashSet() - override fun searchTrackedEntities( searchParametersModel: SearchParametersModel, isOnline: Boolean, @@ -51,26 +45,23 @@ class SearchRepositoryImplKt( isOnline: Boolean, ): TrackedEntitySearchCollectionRepository { var allowCache = false - savedSearchParamenters = searchParametersModel.copy() - savedFilters = FilterManager.getInstance().copy() - if (searchParametersModel != savedSearchParamenters || !FilterManager.getInstance() - .sameFilters(savedFilters) + if (searchParametersModel != searchRepositoryJava.savedSearchParameters || !FilterManager.getInstance() + .sameFilters(searchRepositoryJava.savedFilters) ) { trackedEntityInstanceQuery = searchRepositoryJava.getFilteredRepository(searchParametersModel) } else { - trackedEntityInstanceQuery = - searchRepositoryJava.getFilteredRepository(searchParametersModel) + searchRepositoryJava.getFilteredRepository(searchParametersModel) allowCache = true } - if (fetchedTeiUids.isNotEmpty() && searchParametersModel.selectedProgram == null) { + if (searchRepositoryJava.fetchedTeiUIDs.isNotEmpty() && searchParametersModel.selectedProgram == null) { trackedEntityInstanceQuery = - trackedEntityInstanceQuery.excludeUids().`in`(fetchedTeiUids.toList()) + trackedEntityInstanceQuery.excludeUids().`in`(searchRepositoryJava.fetchedTeiUIDs.toList()) } - val pagerFlow = if (isOnline && FilterManager.getInstance().stateFilters.isNotEmpty()) { + val pagerFlow = if (isOnline && FilterManager.getInstance().stateFilters.isEmpty()) { trackedEntityInstanceQuery.allowOnlineCache().eq(allowCache).offlineFirst() } else { trackedEntityInstanceQuery.allowOnlineCache().eq(allowCache).offlineOnly() @@ -131,7 +122,8 @@ class SearchRepositoryImplKt( options.associate { it.uid() to metadataIconProvider( it.style(), - program?.style()?.color()?.toColor() ?: SurfaceColor.Primary, + program?.style()?.color()?.toColor() + ?: SurfaceColor.Primary, ) } @@ -177,7 +169,12 @@ class SearchRepositoryImplKt( .blockingGet() val metadataIconMap = - options.associate { it.uid() to metadataIconProvider(it.style(), SurfaceColor.Primary) } + options.associate { + it.uid() to metadataIconProvider( + it.style(), + SurfaceColor.Primary, + ) + } OptionSetConfiguration.OptionConfigData( options = options, diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt index 38441502cd..78b7e72282 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt @@ -373,7 +373,7 @@ class SearchTEIViewModel( suspend fun fetchGlobalResults() = withContext(dispatchers.io()) { val searchParametersModel = SearchParametersModel( - selectedProgram = searchRepository.getProgram(initialProgramUid), + selectedProgram = null, queryData = queryData, ) val getPagingData = searchRepositoryKt.searchTrackedEntities( @@ -927,6 +927,7 @@ class SearchTEIViewModel( ValueType.ORGANISATION_UNIT, ValueType.MULTI_TEXT -> { map[item.uid] = (item.displayName ?: "") } + ValueType.DATE, ValueType.AGE -> { item.value?.let { if (it.isNotEmpty()) { @@ -941,6 +942,7 @@ class SearchTEIViewModel( } } } + ValueType.DATETIME -> { item.value?.let { if (it.isNotEmpty()) { @@ -955,9 +957,11 @@ class SearchTEIViewModel( } } } + ValueType.BOOLEAN -> { map[item.uid] = "${item.label}: ${item.value}" } + ValueType.TRUE_ONLY -> { item.value?.let { if (it == "true") { @@ -965,9 +969,11 @@ class SearchTEIViewModel( } } } + ValueType.PERCENTAGE -> { map[item.uid] = "${item.value}%" } + else -> { map[item.uid] = (item.value ?: "") } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEUi.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEUi.kt deleted file mode 100644 index 44f045cd1d..0000000000 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEUi.kt +++ /dev/null @@ -1,389 +0,0 @@ -package org.dhis2.usescases.searchTrackEntity - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Icon -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import org.dhis2.R -import org.dhis2.usescases.searchTrackEntity.listView.SearchResult - -@Composable -fun SearchResult( - searchResultType: SearchResult.SearchResultType, - onSearchOutsideClick: () -> Unit, -) { - when (searchResultType) { - SearchResult.SearchResultType.LOADING -> - LoadingContent( - loadingDescription = stringResource(R.string.search_loading_more), - ) - - SearchResult.SearchResultType.SEARCH_OUTSIDE -> SearchOutsideProgram( - resultText = stringResource(R.string.search_no_results_in_program), - buttonText = stringResource(R.string.search_outside_action), - onSearchOutsideClick = onSearchOutsideClick, - ) - - SearchResult.SearchResultType.NO_MORE_RESULTS -> NoMoreResults() - SearchResult.SearchResultType.TOO_MANY_RESULTS -> TooManyResults() - SearchResult.SearchResultType.NO_RESULTS -> NoResults() - SearchResult.SearchResultType.SEARCH_OR_CREATE -> SearchOrCreate() - else -> { - // Nothing to do in these cases - } - } -} - -@Composable -fun SearchButton(modifier: Modifier = Modifier, onClick: () -> Unit) { - Button( - modifier = modifier, - onClick = onClick, - colors = ButtonDefaults.buttonColors(backgroundColor = Color.White), - shape = RoundedCornerShape(24.dp), - elevation = ButtonDefaults.elevation(), - ) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_search), - contentDescription = "", - tint = colorResource(id = R.color.colorPrimary), - ) - Spacer(modifier = Modifier.size(16.dp)) - Text( - text = stringResource(id = R.string.search), - color = colorResource(id = R.color.textSecondary), - ) - } - } -} - -@Composable -fun WrappedSearchButton(onClick: () -> Unit) { - SearchButton( - modifier = Modifier - .wrapContentWidth(align = Alignment.CenterHorizontally) - .height(44.dp), - onClick = onClick, - ) -} - -@ExperimentalAnimationApi -@Composable -fun FullSearchButton(modifier: Modifier, visible: Boolean = true, onClick: () -> Unit) { - AnimatedVisibility( - modifier = modifier, - visible = visible, - enter = slideInVertically(), - exit = slideOutVertically(), - ) { - SearchButton( - modifier = Modifier - .fillMaxWidth() - .height(48.dp), - onClick = onClick, - ) - } -} - -@Composable -fun LoadingContent(loadingDescription: String) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - CircularProgressIndicator() - Spacer(modifier = Modifier.size(16.dp)) - Text( - text = loadingDescription, - fontSize = 14.sp, - color = Color.Black.copy(alpha = 0.38f), - style = LocalTextStyle.current.copy( - lineHeight = 10.sp, - fontFamily = FontFamily(Font(R.font.rubik_regular)), - ), - ) - } -} - -@Composable -fun SearchOutsideProgram(resultText: String, buttonText: String, onSearchOutsideClick: () -> Unit) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = resultText, - fontSize = 14.sp, - color = Color.Black.copy(alpha = 0.38f), - style = LocalTextStyle.current.copy( - lineHeight = 10.sp, - fontFamily = FontFamily(Font(R.font.rubik_regular)), - ), - ) - Spacer(modifier = Modifier.size(16.dp)) - Button( - onClick = onSearchOutsideClick, - border = BorderStroke(1.dp, colorResource(id = R.color.colorPrimary)), - colors = ButtonDefaults.buttonColors( - backgroundColor = colorResource(id = R.color.white), - ), - ) { - Icon( - painter = painterResource(id = R.drawable.ic_search), - contentDescription = "", - tint = colorResource(id = R.color.colorPrimary), - ) - Spacer(modifier = Modifier.size(16.dp)) - Text(text = buttonText, color = colorResource(id = R.color.colorPrimary)) - } - } -} - -@Composable -fun NoMoreResults() { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = stringResource(R.string.string_no_more_results), - fontSize = 14.sp, - color = Color.Black.copy(alpha = 0.38f), - style = LocalTextStyle.current.copy( - lineHeight = 10.sp, - fontFamily = FontFamily(Font(R.font.rubik_regular)), - ), - ) - } -} - -@Composable -fun NoResults() { - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Image( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_empty_folder), - contentDescription = "", - ) - Spacer(modifier = Modifier.size(16.dp)) - Text( - text = stringResource(R.string.search_no_results), - fontSize = 17.sp, - color = Color.Black.copy(alpha = 0.38f), - style = LocalTextStyle.current.copy( - lineHeight = 24.sp, - fontFamily = FontFamily(Font(R.font.rubik_regular)), - ), - ) - } -} - -@Composable -fun TooManyResults() { - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Image( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_too_many), - contentDescription = "", - ) - Spacer(modifier = Modifier.size(16.dp)) - Text( - text = stringResource(R.string.search_too_many_results), - fontSize = 17.sp, - color = colorResource(id = R.color.pink_500), - style = LocalTextStyle.current.copy( - lineHeight = 24.sp, - fontFamily = FontFamily(Font(R.font.rubik_regular)), - ), - ) - Text( - text = stringResource(R.string.search_too_many_results_message), - fontSize = 17.sp, - color = Color.Black.copy(alpha = 0.38f), - style = LocalTextStyle.current.copy( - lineHeight = 24.sp, - fontFamily = FontFamily(Font(R.font.rubik_regular)), - ), - ) - } -} - -@Composable -fun SearchOrCreate() { - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Image( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_searchvscreate), - contentDescription = "", - ) - Spacer(modifier = Modifier.size(16.dp)) - Text( - text = stringResource(R.string.search_or_create), - fontSize = 17.sp, - color = Color.Black.copy(alpha = 0.38f), - style = LocalTextStyle.current.copy( - lineHeight = 24.sp, - fontFamily = FontFamily(Font(R.font.rubik_regular)), - ), - ) - } -} - -@ExperimentalAnimationApi -@Composable -fun CreateNewButton(modifier: Modifier, extended: Boolean = true, onClick: () -> Unit) { - Button( - modifier = modifier - .wrapContentWidth() - .height(56.dp), - contentPadding = PaddingValues(16.dp), - onClick = onClick, - colors = ButtonDefaults.buttonColors(backgroundColor = Color.White), - shape = RoundedCornerShape(16.dp), - elevation = ButtonDefaults.elevation(), - ) { - Icon( - modifier = Modifier.size(24.dp), - painter = painterResource(id = R.drawable.ic_add_accent), - contentDescription = "", - tint = colorResource(id = R.color.colorPrimary), - ) - AnimatedVisibility(visible = extended) { - Row { - Spacer(modifier = Modifier.size(12.dp)) - Text( - text = stringResource(R.string.search_create_new), - color = colorResource(id = R.color.colorPrimary), - ) - } - } - } -} - -@ExperimentalAnimationApi -@Preview(showBackground = true, backgroundColor = 0x2C98F0) -@Composable -fun SearchFullWidthPreview() { - FullSearchButton(modifier = Modifier) { - } -} - -@Preview(showBackground = true, backgroundColor = 0x2C98F0) -@Composable -fun SearchWrapWidthPreview() { - WrappedSearchButton { - } -} - -@ExperimentalAnimationApi -@Preview -@Composable -fun ExtendedCreateNewButtonPreview() { - CreateNewButton(modifier = Modifier) {} -} - -@ExperimentalAnimationApi -@Preview -@Composable -fun CreateNewButtonPreview() { - CreateNewButton(modifier = Modifier, extended = false) {} -} - -@Preview(showBackground = true) -@Composable -fun LoadingMoreResultsPreview() { - LoadingContent(loadingDescription = "Loading more results...") -} - -@Preview(showBackground = true) -@Composable -fun SearchOutsidePreview() { - SearchResult(searchResultType = SearchResult.SearchResultType.SEARCH_OUTSIDE) {} -} - -@Preview(showBackground = true) -@Composable -fun NoMoreResultsPreview() { - SearchResult(searchResultType = SearchResult.SearchResultType.NO_MORE_RESULTS) {} -} - -@Preview(showBackground = true) -@Composable -fun TooManyResultsPreview() { - SearchResult(searchResultType = SearchResult.SearchResultType.TOO_MANY_RESULTS) {} -} - -@Preview(showBackground = true) -@Composable -fun NoResultsPreview() { - SearchResult(searchResultType = SearchResult.SearchResultType.NO_RESULTS) {} -} - -@Preview(showBackground = true) -@Composable -fun SearchOrCreatePreview() { - SearchResult(searchResultType = SearchResult.SearchResultType.SEARCH_OR_CREATE) {} -} diff --git a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt new file mode 100644 index 0000000000..7374d434d9 --- /dev/null +++ b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt @@ -0,0 +1,90 @@ +package org.dhis2.usescases.searchTrackEntity + +import androidx.paging.PagingData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.dhis2.commons.resources.MetadataIconProvider +import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.data.search.SearchParametersModel +import org.dhis2.form.ui.FieldViewModelFactory +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchCollectionRepository +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.verify +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@OptIn(ExperimentalCoroutinesApi::class) +class SearchRepositoryTest { + private val searchRepositoryJava: SearchRepository = mock() + + private val d2: D2 = mock(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) + + private val dispatcher: DispatcherProvider = mock() + + private val fieldViewModelFactory: FieldViewModelFactory = mock() + + private val metadataIconProvider: MetadataIconProvider = mock() + + private lateinit var searchRepositoryImplKt: SearchRepositoryImplKt + + private val testDispatcher = UnconfinedTestDispatcher() + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + searchRepositoryImplKt = SearchRepositoryImplKt( + searchRepositoryJava, + d2, + dispatcher, + fieldViewModelFactory, + metadataIconProvider, + ) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `test searchTrackedEntities returns PagingData flow`() = runTest { + val searchParametersModel: SearchParametersModel = SearchParametersModel( + selectedProgram = null, + queryData = null, + ) + val trackedEntitySearchCollectionRepository: TrackedEntitySearchCollectionRepository = + mock() + val pagingDataFlow = flowOf(PagingData.empty()) + + whenever(searchRepositoryJava.getFilteredRepository(searchParametersModel)) doReturn trackedEntitySearchCollectionRepository + whenever(trackedEntitySearchCollectionRepository.allowOnlineCache()) doReturn mock() + whenever(trackedEntitySearchCollectionRepository.allowOnlineCache().eq(false)) doReturn mock() + whenever(trackedEntitySearchCollectionRepository.allowOnlineCache().eq(false).offlineFirst()) doReturn mock() + whenever(trackedEntitySearchCollectionRepository.allowOnlineCache().eq(false).offlineOnly()) doReturn mock() + whenever(trackedEntitySearchCollectionRepository.allowOnlineCache().eq(false).offlineOnly().getPagingData(10)) doReturn mock() + whenever(trackedEntitySearchCollectionRepository.allowOnlineCache().eq(false).offlineFirst().getPagingData(10)) doReturn mock() + whenever(trackedEntitySearchCollectionRepository.getPagingData(10)) doReturn pagingDataFlow + + val result = + searchRepositoryImplKt.searchTrackedEntities(searchParametersModel, isOnline = true) + + result.collect { pagingData -> + assertTrue(pagingData is PagingData) + } + + verify(searchRepositoryJava).getFilteredRepository(searchParametersModel) +// verify(trackedEntitySearchCollectionRepository).getPagingData(10) + } +}