@@ -12,6 +12,7 @@ import com.hoc081098.flowext.concatWith
1212import com.hoc081098.flowext.timer
1313import io.mockk.coEvery
1414import io.mockk.coVerify
15+ import io.mockk.coVerifySequence
1516import io.mockk.confirmVerified
1617import io.mockk.mockk
1718import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -226,6 +227,90 @@ class SearchVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, Se
226227 }
227228 }
228229
230+ @Test
231+ fun test_withSearchIntent_debounceSearchQueryThenRejectBlankThenDistinctUntilChangedAndCancelledPreviousExecution () {
232+ val query1 = " #query#1"
233+ val query2 = " #query#2"
234+ coEvery { searchUsersUseCase(query1) } coAnswers {
235+ repeat(10 ) { timeout() } // (1) very long... -> cancelled by (2)
236+ USERS .right()
237+ }
238+ coEvery { searchUsersUseCase(query2) } returns USERS .right()
239+
240+ test(
241+ vmProducer = { vm },
242+ intents = flowOf(" a" , " b" , " c" , query1)
243+ .map { ViewIntent .Search (it) }
244+ .onEach { delay(SEMI_TIMEOUT ) }
245+ .onCompletion { timeout() }
246+ .concatWith(
247+ timer(
248+ ViewIntent .Search (query2),
249+ TOTAL_TIMEOUT ,
250+ ).onCompletion { timeout() }, // (2)
251+ ),
252+ expectedStates = listOf (
253+ ViewState .initial(null ),
254+ ViewState (
255+ users = emptyList(),
256+ isLoading = false ,
257+ error = null ,
258+ submittedQuery = " " ,
259+ originalQuery = " a" , // update originalQuery
260+ ),
261+ ViewState (
262+ users = emptyList(),
263+ isLoading = false ,
264+ error = null ,
265+ submittedQuery = " " ,
266+ originalQuery = " b" , // update originalQuery
267+ ),
268+ ViewState (
269+ users = emptyList(),
270+ isLoading = false ,
271+ error = null ,
272+ submittedQuery = " " ,
273+ originalQuery = " c" , // update originalQuery
274+ ),
275+ ViewState (
276+ users = emptyList(),
277+ isLoading = false ,
278+ error = null ,
279+ submittedQuery = " " ,
280+ originalQuery = query1, // update originalQuery
281+ ),
282+ ViewState (
283+ users = emptyList(),
284+ isLoading = true , // update isLoading
285+ error = null ,
286+ submittedQuery = " " ,
287+ originalQuery = query1,
288+ ),
289+ ViewState (
290+ users = emptyList(),
291+ isLoading = true ,
292+ error = null ,
293+ submittedQuery = " " ,
294+ originalQuery = query2, // update originalQuery
295+ ),
296+ ViewState (
297+ users = USER_ITEMS , // update users
298+ isLoading = false , // update isLoading
299+ error = null ,
300+ submittedQuery = query2, // update submittedQuery
301+ originalQuery = query2,
302+ ),
303+ ).mapRight(),
304+ expectedEvents = emptyList(),
305+ delayAfterDispatchingIntents = EXTRAS_TIMEOUT ,
306+ ) {
307+ coVerifySequence {
308+ searchUsersUseCase(query1)
309+ searchUsersUseCase(query2)
310+ }
311+ }
312+ }
313+
229314 @Test
230315 fun test_withSearchIntent_returnsUserItemsWithProperLoadingState () {
231316 val query = " query"
@@ -322,6 +407,7 @@ class SearchVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, Se
322407 ViewState .initial(null ),
323408 ).mapRight(),
324409 expectedEvents = emptyList(),
410+ delayAfterDispatchingIntents = EXTRAS_TIMEOUT ,
325411 )
326412 }
327413
@@ -365,6 +451,7 @@ class SearchVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, Se
365451 emit(ViewIntent .Search (query))
366452 timeout()
367453 },
454+ delayAfterDispatchingIntents = EXTRAS_TIMEOUT ,
368455 ) {
369456 coVerify(exactly = 2 ) { searchUsersUseCase(query) }
370457 }
@@ -409,11 +496,85 @@ class SearchVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, Se
409496 emit(ViewIntent .Search (query))
410497 timeout()
411498 },
499+ delayAfterDispatchingIntents = EXTRAS_TIMEOUT ,
412500 ) {
413501 coVerify(exactly = 2 ) { searchUsersUseCase(query) }
414502 }
415503 }
416504
505+ @Test
506+ fun test_withRetryIntentWhenError_cancelledBySearchIntent () {
507+ val query1 = " #hoc081098#1"
508+ val query2 = " #hoc081098#2"
509+ val networkError = UserError .NetworkError
510+
511+ var count = 0
512+ coEvery { searchUsersUseCase(query1) } coAnswers {
513+ when (count++ ) {
514+ 0 -> networkError.left()
515+ 1 -> {
516+ repeat(3 ) { timeout() } // (1) very long ... -> cancelled by (2)
517+ USERS .right()
518+ }
519+ else -> error(" Should not reach here!" )
520+ }
521+ }
522+ coEvery { searchUsersUseCase(query2) } returns USERS .right()
523+
524+ test(
525+ vmProducer = { vm },
526+ intents = flowOf(ViewIntent .Retry ).concatWith(
527+ flow {
528+ delay(SEMI_TIMEOUT ) // (2) very short ...
529+ emit(ViewIntent .Search (query2))
530+ timeout()
531+ }
532+ ),
533+ expectedStates = listOf (
534+ ViewState (
535+ users = emptyList(),
536+ isLoading = false ,
537+ error = networkError,
538+ submittedQuery = query1,
539+ originalQuery = query1,
540+ ),
541+ ViewState (
542+ users = emptyList(),
543+ isLoading = true , // update isLoading
544+ error = null , // update error
545+ submittedQuery = query1,
546+ originalQuery = query1,
547+ ),
548+ ViewState (
549+ users = emptyList(),
550+ isLoading = true ,
551+ error = null ,
552+ submittedQuery = query1,
553+ originalQuery = query2, // update originalQuery
554+ ),
555+ ViewState (
556+ users = USER_ITEMS , // update users
557+ isLoading = false , // update isLoading
558+ error = null ,
559+ submittedQuery = query2, // update submittedQuery
560+ originalQuery = query2,
561+ ),
562+ ).mapRight(),
563+ expectedEvents = emptyList(),
564+ intentsBeforeCollecting = flow {
565+ emit(ViewIntent .Search (query1))
566+ timeout()
567+ },
568+ delayAfterDispatchingIntents = EXTRAS_TIMEOUT
569+ ) {
570+ coVerifySequence {
571+ searchUsersUseCase(query1)
572+ searchUsersUseCase(query1)
573+ searchUsersUseCase(query2)
574+ }
575+ }
576+ }
577+
417578 private companion object {
418579 private val EXTRAS_TIMEOUT = Duration .milliseconds(100 )
419580 private val TOTAL_TIMEOUT = SEARCH_DEBOUNCE_DURATION + EXTRAS_TIMEOUT
0 commit comments