@@ -12,6 +12,7 @@ import com.hoc081098.flowext.concatWith
12
12
import com.hoc081098.flowext.timer
13
13
import io.mockk.coEvery
14
14
import io.mockk.coVerify
15
+ import io.mockk.coVerifySequence
15
16
import io.mockk.confirmVerified
16
17
import io.mockk.mockk
17
18
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -226,6 +227,90 @@ class SearchVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, Se
226
227
}
227
228
}
228
229
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
+
229
314
@Test
230
315
fun test_withSearchIntent_returnsUserItemsWithProperLoadingState () {
231
316
val query = " query"
@@ -322,6 +407,7 @@ class SearchVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, Se
322
407
ViewState .initial(null ),
323
408
).mapRight(),
324
409
expectedEvents = emptyList(),
410
+ delayAfterDispatchingIntents = EXTRAS_TIMEOUT ,
325
411
)
326
412
}
327
413
@@ -365,6 +451,7 @@ class SearchVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, Se
365
451
emit(ViewIntent .Search (query))
366
452
timeout()
367
453
},
454
+ delayAfterDispatchingIntents = EXTRAS_TIMEOUT ,
368
455
) {
369
456
coVerify(exactly = 2 ) { searchUsersUseCase(query) }
370
457
}
@@ -409,11 +496,85 @@ class SearchVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, Se
409
496
emit(ViewIntent .Search (query))
410
497
timeout()
411
498
},
499
+ delayAfterDispatchingIntents = EXTRAS_TIMEOUT ,
412
500
) {
413
501
coVerify(exactly = 2 ) { searchUsersUseCase(query) }
414
502
}
415
503
}
416
504
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
+
417
578
private companion object {
418
579
private val EXTRAS_TIMEOUT = Duration .milliseconds(100 )
419
580
private val TOTAL_TIMEOUT = SEARCH_DEBOUNCE_DURATION + EXTRAS_TIMEOUT
0 commit comments