11package com.hoc.flowmvi.ui.search
22
33import androidx.lifecycle.SavedStateHandle
4+ import arrow.core.left
45import arrow.core.right
56import com.flowmvi.mvi_testing.BaseMviViewModelTest
67import com.flowmvi.mvi_testing.mapRight
8+ import com.hoc.flowmvi.domain.model.UserError
79import com.hoc.flowmvi.domain.usecase.SearchUsersUseCase
10+ import com.hoc.flowmvi.ui.search.SearchVM.Companion.SEARCH_DEBOUNCE_DURATION
11+ import com.hoc081098.flowext.concatWith
12+ import com.hoc081098.flowext.timer
813import io.mockk.coEvery
914import io.mockk.coVerify
1015import io.mockk.confirmVerified
@@ -13,6 +18,10 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
1318import kotlinx.coroutines.FlowPreview
1419import kotlinx.coroutines.delay
1520import kotlinx.coroutines.flow.flow
21+ import kotlinx.coroutines.flow.flowOf
22+ import kotlinx.coroutines.flow.map
23+ import kotlinx.coroutines.flow.onCompletion
24+ import kotlinx.coroutines.flow.onEach
1625import kotlin.test.Test
1726import kotlin.time.Duration
1827import kotlin.time.ExperimentalTime
@@ -28,10 +37,7 @@ class SearchVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, Se
2837 override fun setup () {
2938 super .setup()
3039
31- searchUsersUseCase = mockk() {
32- coEvery { this @mockk(any()) } returns USERS .right()
33- }
34-
40+ searchUsersUseCase = mockk()
3541 savedStateHandle = SavedStateHandle ()
3642 vm = SearchVM (
3743 searchUsersUseCase = searchUsersUseCase,
@@ -47,36 +53,188 @@ class SearchVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, Se
4753 }
4854
4955 @Test
50- fun test_withSearchIntent_rejectBlankSearchQuery () {
51- val q = " "
56+ fun test_withSearchIntent_debounceSearchQuery () {
57+ val query = " d"
58+ coEvery { searchUsersUseCase(query) } returns USERS .right()
59+
5260 test(
5361 vmProducer = { vm },
54- intents = flow {
55- emit( ViewIntent .Search (q))
56- timeout()
57- },
62+ intents = flowOf( " a " , " b " , " c " , query)
63+ .map { ViewIntent .Search (it) }
64+ .onEach { delay( SEMI_TIMEOUT ) }
65+ .onCompletion { timeout() },
5866 expectedStates = listOf (
5967 ViewState .initial(null ),
6068 ViewState (
6169 users = emptyList(),
6270 isLoading = false ,
6371 error = null ,
6472 submittedQuery = " " ,
65- originalQuery = q, // update originalQuery
73+ originalQuery = " a" , // update originalQuery
74+ ),
75+ ViewState (
76+ users = emptyList(),
77+ isLoading = false ,
78+ error = null ,
79+ submittedQuery = " " ,
80+ originalQuery = " b" , // update originalQuery
81+ ),
82+ ViewState (
83+ users = emptyList(),
84+ isLoading = false ,
85+ error = null ,
86+ submittedQuery = " " ,
87+ originalQuery = " c" , // update originalQuery
88+ ),
89+ ViewState (
90+ users = emptyList(),
91+ isLoading = false ,
92+ error = null ,
93+ submittedQuery = " " ,
94+ originalQuery = query, // update originalQuery
95+ ),
96+ ViewState (
97+ users = emptyList(),
98+ isLoading = true , // update isLoading
99+ error = null ,
100+ submittedQuery = " " ,
101+ originalQuery = query,
102+ ),
103+ ViewState (
104+ users = USER_ITEMS , // update users
105+ isLoading = false , // update isLoading
106+ error = null ,
107+ submittedQuery = query, // update submittedQuery
108+ originalQuery = query,
66109 ),
67110 ).mapRight(),
68111 expectedEvents = emptyList(),
69- delayAfterDispatchingIntents = TIMEOUT ,
112+ delayAfterDispatchingIntents = EXTRAS_TIMEOUT ,
113+ ) {
114+ coVerify(exactly = 1 ) { searchUsersUseCase(query) }
115+ }
116+ }
117+
118+ @Test
119+ fun test_withSearchIntent_debounceSearchQueryAndRejectBlank () {
120+ val query = " "
121+
122+ test(
123+ vmProducer = { vm },
124+ intents = flowOf(" a" , " b" , " c" , query)
125+ .map { ViewIntent .Search (it) }
126+ .onEach { delay(SEMI_TIMEOUT ) }
127+ .onCompletion { timeout() },
128+ expectedStates = listOf (
129+ ViewState .initial(null ),
130+ ViewState (
131+ users = emptyList(),
132+ isLoading = false ,
133+ error = null ,
134+ submittedQuery = " " ,
135+ originalQuery = " a" , // update originalQuery
136+ ),
137+ ViewState (
138+ users = emptyList(),
139+ isLoading = false ,
140+ error = null ,
141+ submittedQuery = " " ,
142+ originalQuery = " b" , // update originalQuery
143+ ),
144+ ViewState (
145+ users = emptyList(),
146+ isLoading = false ,
147+ error = null ,
148+ submittedQuery = " " ,
149+ originalQuery = " c" , // update originalQuery
150+ ),
151+ ViewState (
152+ users = emptyList(),
153+ isLoading = false ,
154+ error = null ,
155+ submittedQuery = " " ,
156+ originalQuery = query, // update originalQuery
157+ ),
158+ ).mapRight(),
159+ expectedEvents = emptyList(),
160+ delayAfterDispatchingIntents = EXTRAS_TIMEOUT ,
70161 )
71162 }
72163
164+ @Test
165+ fun test_withSearchIntent_debounceSearchQueryThenRejectBlankAndDistinctUntilChanged () {
166+ val query = " #query"
167+ coEvery { searchUsersUseCase(query) } returns USERS .right()
168+
169+ test(
170+ vmProducer = { vm },
171+ intents = flowOf(" a" , " b" , " c" , query)
172+ .map { ViewIntent .Search (it) }
173+ .onEach { delay(SEMI_TIMEOUT ) }
174+ .onCompletion { timeout() }
175+ .concatWith(timer(ViewIntent .Search (query), TOTAL_TIMEOUT ))
176+ .onCompletion { timeout() },
177+ expectedStates = listOf (
178+ ViewState .initial(null ),
179+ ViewState (
180+ users = emptyList(),
181+ isLoading = false ,
182+ error = null ,
183+ submittedQuery = " " ,
184+ originalQuery = " a" , // update originalQuery
185+ ),
186+ ViewState (
187+ users = emptyList(),
188+ isLoading = false ,
189+ error = null ,
190+ submittedQuery = " " ,
191+ originalQuery = " b" , // update originalQuery
192+ ),
193+ ViewState (
194+ users = emptyList(),
195+ isLoading = false ,
196+ error = null ,
197+ submittedQuery = " " ,
198+ originalQuery = " c" , // update originalQuery
199+ ),
200+ ViewState (
201+ users = emptyList(),
202+ isLoading = false ,
203+ error = null ,
204+ submittedQuery = " " ,
205+ originalQuery = query, // update originalQuery
206+ ),
207+ ViewState (
208+ users = emptyList(),
209+ isLoading = true , // update isLoading
210+ error = null ,
211+ submittedQuery = " " ,
212+ originalQuery = query,
213+ ),
214+ ViewState (
215+ users = USER_ITEMS , // update users
216+ isLoading = false , // update isLoading
217+ error = null ,
218+ submittedQuery = query, // update submittedQuery
219+ originalQuery = query,
220+ ),
221+ ).mapRight(),
222+ expectedEvents = emptyList(),
223+ delayAfterDispatchingIntents = EXTRAS_TIMEOUT ,
224+ ) {
225+ coVerify(exactly = 1 ) { searchUsersUseCase(query) }
226+ }
227+ }
228+
73229 @Test
74230 fun test_withSearchIntent_returnsUserItemsWithProperLoadingState () {
75- val q = " query"
231+ val query = " query"
232+ coEvery { searchUsersUseCase(query) } returns USERS .right()
233+
76234 test(
77235 vmProducer = { vm },
78236 intents = flow {
79- emit(ViewIntent .Search (q ))
237+ emit(ViewIntent .Search (query ))
80238 timeout()
81239 },
82240 expectedStates = listOf (
@@ -86,37 +244,83 @@ class SearchVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, Se
86244 isLoading = false ,
87245 error = null ,
88246 submittedQuery = " " ,
89- originalQuery = q , // update originalQuery
247+ originalQuery = query , // update originalQuery
90248 ),
91249 ViewState (
92250 users = emptyList(),
93251 isLoading = true , // update isLoading
94252 error = null ,
95253 submittedQuery = " " ,
96- originalQuery = q ,
254+ originalQuery = query ,
97255 ),
98256 ViewState (
99257 users = USER_ITEMS , // update users
100258 isLoading = false , // update isLoading
101259 error = null ,
102- submittedQuery = q,
103- originalQuery = q ,
260+ submittedQuery = query, // update submittedQuery
261+ originalQuery = query ,
104262 ),
105263 ).mapRight(),
106264 expectedEvents = emptyList(),
107- delayAfterDispatchingIntents = TIMEOUT ,
265+ delayAfterDispatchingIntents = EXTRAS_TIMEOUT ,
266+ ) {
267+ coVerify(exactly = 1 ) { searchUsersUseCase(query) }
268+ }
269+ }
270+
271+ @Test
272+ fun test_withSearchIntent_returnsUserErrorWithProperLoadingState () {
273+ val query = " query"
274+ val networkError = UserError .NetworkError
275+ coEvery { searchUsersUseCase(query) } returns networkError.left()
276+
277+ test(
278+ vmProducer = { vm },
279+ intents = flow {
280+ emit(ViewIntent .Search (query))
281+ timeout()
282+ },
283+ expectedStates = listOf (
284+ ViewState .initial(null ),
285+ ViewState (
286+ users = emptyList(),
287+ isLoading = false ,
288+ error = null ,
289+ submittedQuery = " " ,
290+ originalQuery = query, // update originalQuery
291+ ),
292+ ViewState (
293+ users = emptyList(),
294+ isLoading = true , // update isLoading
295+ error = null ,
296+ submittedQuery = " " ,
297+ originalQuery = query,
298+ ),
299+ ViewState (
300+ users = emptyList(),
301+ isLoading = false , // update isLoading
302+ error = networkError, // update error
303+ submittedQuery = query, // update submittedQuery
304+ originalQuery = query,
305+ ),
306+ ).mapRight(),
307+ expectedEvents = listOf (
308+ SingleEvent .SearchFailure (networkError)
309+ ).mapRight(),
310+ delayAfterDispatchingIntents = EXTRAS_TIMEOUT ,
108311 ) {
109- coVerify(exactly = 1 ) { searchUsersUseCase(q ) }
312+ coVerify(exactly = 1 ) { searchUsersUseCase(query ) }
110313 }
111314 }
112315
113316 private companion object {
114- private val TIMEOUT = Duration .milliseconds(100 )
317+ private val EXTRAS_TIMEOUT = Duration .milliseconds(100 )
318+ private val TOTAL_TIMEOUT = SEARCH_DEBOUNCE_DURATION + EXTRAS_TIMEOUT
319+ private val SEMI_TIMEOUT = SEARCH_DEBOUNCE_DURATION / 10
115320
116321 /* *
117322 * Extra delay to emit search intent
118323 */
119- private suspend inline fun timeout () =
120- delay(SearchVM .SEARCH_DEBOUNCE_DURATION + TIMEOUT )
324+ private suspend inline fun timeout () = delay(TOTAL_TIMEOUT )
121325 }
122326}
0 commit comments