Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Debounce performing search depending on searchquery length #3576

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.smartregister.fhircore.quest.ui.register

import android.graphics.Bitmap
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
Expand All @@ -33,10 +34,13 @@
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlin.math.ceil
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -78,10 +82,12 @@
import org.smartregister.fhircore.engine.util.extension.encodeJson
import org.smartregister.fhircore.quest.data.register.RegisterPagingSource
import org.smartregister.fhircore.quest.data.register.model.RegisterPagingSourceState
import org.smartregister.fhircore.quest.ui.shared.models.SearchQuery
import org.smartregister.fhircore.quest.util.extensions.referenceToBitmap
import org.smartregister.fhircore.quest.util.extensions.toParamDataMap
import timber.log.Timber

@OptIn(FlowPreview::class)
@HiltViewModel
class RegisterViewModel
@Inject
Expand Down Expand Up @@ -112,6 +118,29 @@
}
private val decodedImageMap = mutableStateMapOf<String, Bitmap>()

private val _searchQueryFlow: MutableSharedFlow<SearchQuery> = MutableSharedFlow()

Check warning on line 121 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt#L121

Added line #L121 was not covered by tests

@VisibleForTesting
val debouncedSearchQueryFlow =
_searchQueryFlow.debounce {

Check warning on line 125 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt#L124-L125

Added lines #L124 - L125 were not covered by tests
val searchText = it.query
when (searchText.length) {
0 -> 2.milliseconds // when search is cleared
1,
2, -> 1000.milliseconds
else -> 500.milliseconds
}
}

init {
viewModelScope.launch {

Check warning on line 136 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt#L135-L136

Added lines #L135 - L136 were not covered by tests
debouncedSearchQueryFlow.collect {
val registerId = registerUiState.value.registerId
performSearch(registerId, it)
}
}
}

/**
* This function paginates the register data. An optional [clearCache] resets the data in the
* cache (this is necessary after a questionnaire has been submitted to refresh the register with
Expand Down Expand Up @@ -191,26 +220,7 @@
when (event) {
// Search using name or patient logicalId or identifier. Modify to add more search params
is RegisterEvent.SearchRegister -> {
if (event.searchQuery.isBlank()) {
val regConfig = retrieveRegisterConfiguration(registerId)
val searchByDynamicQueries = !regConfig.searchBar?.dataFilterFields.isNullOrEmpty()
if (searchByDynamicQueries) {
registerFilterState.value = RegisterFilterState() // Reset queries
}
when {
regConfig.infiniteScroll ->
registerData.value = retrieveCompleteRegisterData(registerId, searchByDynamicQueries)
else ->
retrieveRegisterUiState(
registerId = registerId,
screenTitle = registerUiState.value.screenTitle,
params = registerUiState.value.params.toTypedArray(),
clearCache = searchByDynamicQueries,
)
}
} else {
filterRegisterData(event.searchQuery.query)
}
viewModelScope.launch { _searchQueryFlow.emit(event.searchQuery) }
}
is RegisterEvent.MoveToNextPage -> {
currentPage.value = currentPage.value.plus(1)
Expand All @@ -224,6 +234,30 @@
}
}

@VisibleForTesting
fun performSearch(registerId: String, searchQuery: SearchQuery) {
if (searchQuery.isBlank()) {
val regConfig = retrieveRegisterConfiguration(registerId)

Check warning on line 240 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt#L240

Added line #L240 was not covered by tests
val searchByDynamicQueries = !regConfig.searchBar?.dataFilterFields.isNullOrEmpty()
if (searchByDynamicQueries) {
registerFilterState.value = RegisterFilterState() // Reset queries

Check warning on line 243 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt#L243

Added line #L243 was not covered by tests
}
when {

Check warning on line 245 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt#L245

Added line #L245 was not covered by tests
regConfig.infiniteScroll ->
registerData.value = retrieveCompleteRegisterData(registerId, searchByDynamicQueries)

Check warning on line 247 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt#L247

Added line #L247 was not covered by tests
else ->
retrieveRegisterUiState(
registerId = registerId,
screenTitle = registerUiState.value.screenTitle,
params = registerUiState.value.params.toTypedArray(),
clearCache = searchByDynamicQueries,

Check warning on line 253 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt#L249-L253

Added lines #L249 - L253 were not covered by tests
)
}
} else {
filterRegisterData(searchQuery.query)

Check warning on line 257 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt#L257

Added line #L257 was not covered by tests
}
}

fun filterRegisterData(searchText: String) {
val searchBar = registerUiState.value.registerConfiguration?.searchBar
val registerId = registerUiState.value.registerId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import io.mockk.runs
import io.mockk.spyk
import io.mockk.verify
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.DateType
Expand Down Expand Up @@ -114,14 +118,18 @@ class RegisterViewModelTest : RobolectricTest() {
@Test
@kotlinx.coroutines.ExperimentalCoroutinesApi
fun testRetrieveRegisterUiState() = runTest {
every { registerViewModel.retrieveRegisterConfiguration(any()) } returns
val registerConfig =
RegisterConfiguration(
appId = "app",
id = registerId,
fhirResource =
FhirResourceConfig(baseResource = ResourceConfig(resource = ResourceType.Patient)),
pageSize = 10,
)
configurationRegistry.configCacheMap[registerId] = registerConfig
registerViewModel.registerUiState.value =
registerViewModel.registerUiState.value.copy(registerId = registerId)

every { registerViewModel.paginateRegisterData(any(), any()) } just runs
coEvery { registerRepository.countRegisterData(any()) } returns 200
registerViewModel.retrieveRegisterUiState(
Expand All @@ -143,23 +151,78 @@ class RegisterViewModelTest : RobolectricTest() {
}

@Test
fun testOnEventSearchRegister() {
every { registerViewModel.retrieveRegisterConfiguration(any()) } returns
@kotlinx.coroutines.ExperimentalCoroutinesApi
fun testDebounceSearchQueryFlow() = runTest {
val registerConfig =
RegisterConfiguration(
appId = "app",
id = registerId,
fhirResource =
FhirResourceConfig(baseResource = ResourceConfig(resource = ResourceType.Patient)),
pageSize = 10,
)
every { registerViewModel.registerUiState } returns
mutableStateOf(RegisterUiState(registerId = registerId))
configurationRegistry.configCacheMap[registerId] = registerConfig
registerViewModel.registerUiState.value =
registerViewModel.registerUiState.value.copy(registerId = registerId)
coEvery { registerRepository.countRegisterData(any()) } returns 0L

val results = mutableListOf<String>()
val debounceJob = launch {
registerViewModel.debouncedSearchQueryFlow.collect { results.add(it.query) }
}
advanceUntilIdle()

// Search with empty string should paginate the data
registerViewModel.onEvent(RegisterEvent.SearchRegister(SearchQuery.emptyText))

advanceTimeBy(3.milliseconds)
Assert.assertTrue(results.isNotEmpty())
Assert.assertTrue(results.last().isBlank())

registerViewModel.onEvent(RegisterEvent.SearchRegister(SearchQuery("K")))
registerViewModel.onEvent(RegisterEvent.SearchRegister(SearchQuery("Kh")))
registerViewModel.onEvent(RegisterEvent.SearchRegister(SearchQuery("Kha")))
registerViewModel.onEvent(RegisterEvent.SearchRegister(SearchQuery("Khan")))

advanceTimeBy(1010.milliseconds)
Assert.assertEquals(2, results.size)
Assert.assertEquals("Khan", results.last())
debounceJob.cancel()
}

@Test
fun testPerformSearchWithEmptyQuery() = runTest {
val registerConfig =
RegisterConfiguration(
appId = "app",
id = registerId,
fhirResource =
FhirResourceConfig(baseResource = ResourceConfig(resource = ResourceType.Patient)),
pageSize = 10,
)
configurationRegistry.configCacheMap[registerId] = registerConfig
coEvery { registerRepository.countRegisterData(any()) } returns 0L

// Search with empty string should paginate the data
registerViewModel.performSearch(registerId, SearchQuery.emptyText)
verify { registerViewModel.retrieveRegisterUiState(any(), any(), any(), any()) }
}

@Test
fun testPerformSearchWithNonEmptyQuery() = runTest {
val registerConfig =
RegisterConfiguration(
appId = "app",
id = registerId,
fhirResource =
FhirResourceConfig(baseResource = ResourceConfig(resource = ResourceType.Patient)),
pageSize = 10,
)
configurationRegistry.configCacheMap[registerId] = registerConfig
coEvery { registerRepository.countRegisterData(any()) } returns 0L

// Search for the word 'Khan' should call the filterRegisterData function
registerViewModel.onEvent(RegisterEvent.SearchRegister(SearchQuery("Khan")))
registerViewModel.performSearch(registerId, SearchQuery("Khan"))
verify { registerViewModel.filterRegisterData(any()) }
}

Expand Down
Loading