diff --git a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt index a48bd93f..f59257e9 100644 --- a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt +++ b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt @@ -89,13 +89,7 @@ class MainActivity : is SingleEvent.RemoveUser.Success -> toast("Removed '${event.user.fullName}'") is SingleEvent.RemoveUser.Failure -> { toast("Error when removing '${event.user.fullName}'") - userAdapter.notifyItemChanged( - vm.viewState.value - .userItems - .indexOfFirst { it.id == event.user.id } - .takeIf { it != RecyclerView.NO_POSITION } - ?: return - ) + userAdapter.notifyItemChanged(event.indexProducer() ?: return) } } } diff --git a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt index cb159cc1..b82e4f21 100644 --- a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt +++ b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt @@ -114,6 +114,10 @@ sealed interface SingleEvent : MviSingleEvent { sealed interface RemoveUser : SingleEvent { data class Success(val user: UserItem) : RemoveUser - data class Failure(val user: UserItem, val error: UserError) : RemoveUser + data class Failure( + val user: UserItem, + val error: UserError, + val indexProducer: () -> Int?, + ) : RemoveUser } } diff --git a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt index d5c07caf..c27f48a0 100644 --- a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt +++ b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt @@ -58,15 +58,21 @@ class MainVM( } private fun Flow.sendSingleEvent(): Flow { - return onEach { - val event = when (it) { - is PartialChange.GetUser.Error -> SingleEvent.GetUsersError(it.error) + return onEach { change -> + val event = when (change) { + is PartialChange.GetUser.Error -> SingleEvent.GetUsersError(change.error) is PartialChange.Refresh.Success -> SingleEvent.Refresh.Success - is PartialChange.Refresh.Failure -> SingleEvent.Refresh.Failure(it.error) - is PartialChange.RemoveUser.Success -> SingleEvent.RemoveUser.Success(it.user) + is PartialChange.Refresh.Failure -> SingleEvent.Refresh.Failure(change.error) + is PartialChange.RemoveUser.Success -> SingleEvent.RemoveUser.Success(change.user) is PartialChange.RemoveUser.Failure -> SingleEvent.RemoveUser.Failure( - user = it.user, - error = it.error, + user = change.user, + error = change.error, + indexProducer = { + viewState.value + .userItems + .indexOfFirst { it.id == change.user.id } + .takeIf { it != -1 } + } ) PartialChange.GetUser.Loading -> return@onEach is PartialChange.GetUser.Data -> return@onEach diff --git a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt index d8871384..a1a178e9 100644 --- a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt +++ b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt @@ -3,6 +3,7 @@ package com.hoc.flowmvi.ui.main import arrow.core.left import arrow.core.right import com.flowmvi.mvi_testing.BaseMviViewModelTest +import com.flowmvi.mvi_testing.mapRight import com.hoc.flowmvi.domain.entity.User import com.hoc.flowmvi.domain.repository.UserError import com.hoc.flowmvi.domain.usecase.GetUsersUseCase @@ -22,6 +23,8 @@ import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.update import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs import kotlin.time.Duration import kotlin.time.ExperimentalTime @@ -78,7 +81,7 @@ class MainVMTest : BaseMviViewModelTest< error = null, isRefreshing = false ) - ), + ).mapRight(), expectedEvents = emptyList(), ) { verify(exactly = 1) { getUserUseCase() } } @@ -100,12 +103,12 @@ class MainVMTest : BaseMviViewModelTest< error = userError, isRefreshing = false ) - ), + ).mapRight(), expectedEvents = listOf( SingleEvent.GetUsersError( error = userError, ), - ), + ).mapRight(), ) { verify(exactly = 1) { getUserUseCase() } } } @@ -137,10 +140,10 @@ class MainVMTest : BaseMviViewModelTest< error = null, isRefreshing = false ), - ), + ).mapRight(), expectedEvents = listOf( SingleEvent.Refresh.Success - ), + ).mapRight(), ) { coVerify(exactly = 1) { getUserUseCase() } coVerify(exactly = 1) { refreshGetUsersUseCase() } @@ -177,10 +180,10 @@ class MainVMTest : BaseMviViewModelTest< error = null, isRefreshing = false ), - ), + ).mapRight(), expectedEvents = listOf( SingleEvent.Refresh.Failure(userError) - ), + ).mapRight(), ) { coVerify(exactly = 1) { getUserUseCase() } coVerify(exactly = 1) { refreshGetUsersUseCase() } @@ -195,7 +198,7 @@ class MainVMTest : BaseMviViewModelTest< vm }, intents = flowOf(ViewIntent.Refresh), - expectedStates = listOf(ViewState.initial()), + expectedStates = listOf(ViewState.initial()).mapRight(), expectedEvents = emptyList(), delayAfterDispatchingIntents = Duration.milliseconds(100), ) { coVerify(exactly = 0) { refreshGetUsersUseCase() } } @@ -220,10 +223,10 @@ class MainVMTest : BaseMviViewModelTest< error = userError, isRefreshing = false, ) - ), + ).mapRight(), expectedEvents = listOf( SingleEvent.GetUsersError(userError), - ), + ).mapRight(), delayAfterDispatchingIntents = Duration.milliseconds(100), ) { coVerify(exactly = 1) { getUserUseCase() } @@ -239,7 +242,7 @@ class MainVMTest : BaseMviViewModelTest< vm }, intents = flowOf(ViewIntent.Retry), - expectedStates = listOf(ViewState.initial()), + expectedStates = listOf(ViewState.initial()).mapRight(), expectedEvents = emptyList(), delayAfterDispatchingIntents = Duration.milliseconds(100), ) { coVerify(exactly = 0) { getUserUseCase() } } @@ -278,10 +281,10 @@ class MainVMTest : BaseMviViewModelTest< error = null, isRefreshing = false, ) - ), + ).mapRight(), expectedEvents = listOf( SingleEvent.GetUsersError(userError), - ), + ).mapRight(), ) { verify(exactly = 2) { getUserUseCase() } } } @@ -319,11 +322,11 @@ class MainVMTest : BaseMviViewModelTest< error = userError2, isRefreshing = false, ) - ), + ).mapRight(), expectedEvents = listOf( SingleEvent.GetUsersError(userError1), SingleEvent.GetUsersError(userError2), - ), + ).mapRight(), ) { verify(exactly = 2) { getUserUseCase() } } } @@ -375,11 +378,11 @@ class MainVMTest : BaseMviViewModelTest< error = null, isRefreshing = false, ), - ), + ).mapRight(), expectedEvents = listOf( SingleEvent.RemoveUser.Success(item1), SingleEvent.RemoveUser.Success(item2), - ) + ).mapRight() ) { coVerify(exactly = 1) { getUserUseCase() } coVerifySequence { @@ -410,9 +413,14 @@ class MainVMTest : BaseMviViewModelTest< error = null, isRefreshing = false, ), - ), + ).mapRight(), expectedEvents = listOf( - SingleEvent.RemoveUser.Failure(item, userError), + { event: SingleEvent -> + val removed = assertIs(event) + assertEquals(item, removed.user) + assertEquals(userError, removed.error) + assertEquals(removed.indexProducer(), 0) + }.left(), ) ) { coVerify(exactly = 1) { getUserUseCase() } diff --git a/mvi/mvi-testing/build.gradle.kts b/mvi/mvi-testing/build.gradle.kts index 870df1a5..97b4be58 100644 --- a/mvi/mvi-testing/build.gradle.kts +++ b/mvi/mvi-testing/build.gradle.kts @@ -38,5 +38,7 @@ dependencies { implementation(mviBase) implementation(deps.timber) + implementation(deps.arrow.core) + addUnitTest(testImplementation = false) } diff --git a/mvi/mvi-testing/src/main/java/com/flowmvi/mvi_testing/BaseMviViewModelTest.kt b/mvi/mvi-testing/src/main/java/com/flowmvi/mvi_testing/BaseMviViewModelTest.kt index a31f0b28..fd62354a 100644 --- a/mvi/mvi-testing/src/main/java/com/flowmvi/mvi_testing/BaseMviViewModelTest.kt +++ b/mvi/mvi-testing/src/main/java/com/flowmvi/mvi_testing/BaseMviViewModelTest.kt @@ -1,6 +1,8 @@ package com.flowmvi.mvi_testing import androidx.annotation.CallSuper +import arrow.core.Either +import arrow.core.right import com.hoc.flowmvi.mvi_base.MviIntent import com.hoc.flowmvi.mvi_base.MviSingleEvent import com.hoc.flowmvi.mvi_base.MviViewModel @@ -20,7 +22,6 @@ import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.setMain import kotlin.test.AfterTest import kotlin.test.BeforeTest -import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.time.Duration import kotlin.time.ExperimentalTime @@ -44,8 +45,8 @@ abstract class BaseMviViewModelTest< protected fun test( vmProducer: () -> VM, intents: Flow, - expectedStates: List, - expectedEvents: List, + expectedStates: List Unit, S>>, + expectedEvents: List Unit, E>>, delayAfterDispatchingIntents: Duration = Duration.ZERO, logging: Boolean = true, intentsBeforeCollecting: Flow? = null, @@ -69,18 +70,34 @@ abstract class BaseMviViewModelTest< } assertEquals(expectedStates.size, states.size, "States size") - assertContentEquals( - expectedStates, - states, - "States content" - ) + expectedStates.withIndex().zip(states).forEach { (indexedValue, state) -> + val (index, exp) = indexedValue + exp.fold( + ifRight = { + assertEquals( + expected = it, + actual = state, + message = "[State index=$index]" + ) + }, + ifLeft = { it(state) } + ) + } assertEquals(expectedEvents.size, events.size, "Events size") - assertContentEquals( - expectedEvents, - events, - "Evens content", - ) + expectedEvents.withIndex().zip(events).forEach { (indexedValue, event) -> + val (index, exp) = indexedValue + exp.fold( + ifRight = { + assertEquals( + expected = it, + actual = event, + message = "[Event index=$index]" + ) + }, + ifLeft = { it(event) } + ) + } otherAssertions?.invoke() stateJob.cancel() @@ -95,3 +112,5 @@ abstract class BaseMviViewModelTest< clearAllMocks() } } + +fun Iterable.mapRight(): List Unit, T>> = map { it.right() }