refactor: decouple card detail viewmodel datasource contracts
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package space.hackenslacker.kanbn4droid.app.carddetail
|
package space.hackenslacker.kanbn4droid.app.carddetail
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -45,13 +46,55 @@ sealed interface CardDetailUiEvent {
|
|||||||
data class ShowSnackbar(val message: String) : CardDetailUiEvent
|
data class ShowSnackbar(val message: String) : CardDetailUiEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed interface DataSourceResult<out T> {
|
||||||
|
data class Success<T>(val value: T) : DataSourceResult<T>
|
||||||
|
data class GenericError(val message: String) : DataSourceResult<Nothing>
|
||||||
|
data object SessionExpired : DataSourceResult<Nothing>
|
||||||
|
}
|
||||||
|
|
||||||
interface CardDetailDataSource {
|
interface CardDetailDataSource {
|
||||||
suspend fun loadCard(cardId: String): CardDetailRepository.Result<CardDetail>
|
suspend fun loadCard(cardId: String): DataSourceResult<CardDetail>
|
||||||
suspend fun updateTitle(cardId: String, title: String): CardDetailRepository.Result<Unit>
|
suspend fun updateTitle(cardId: String, title: String): DataSourceResult<Unit>
|
||||||
suspend fun updateDescription(cardId: String, description: String): CardDetailRepository.Result<Unit>
|
suspend fun updateDescription(cardId: String, description: String): DataSourceResult<Unit>
|
||||||
suspend fun updateDueDate(cardId: String, dueDate: LocalDate?): CardDetailRepository.Result<Unit>
|
suspend fun updateDueDate(cardId: String, dueDate: LocalDate?): DataSourceResult<Unit>
|
||||||
suspend fun listActivities(cardId: String): CardDetailRepository.Result<List<CardActivity>>
|
suspend fun listActivities(cardId: String): DataSourceResult<List<CardActivity>>
|
||||||
suspend fun addComment(cardId: String, comment: String): CardDetailRepository.Result<Unit>
|
suspend fun addComment(cardId: String, comment: String): DataSourceResult<Unit>
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class CardDetailRepositoryDataSource(
|
||||||
|
private val repository: CardDetailRepository,
|
||||||
|
private val loadCardCall: suspend (String) -> CardDetailRepository.Result<CardDetail>,
|
||||||
|
) : CardDetailDataSource {
|
||||||
|
override suspend fun loadCard(cardId: String): DataSourceResult<CardDetail> {
|
||||||
|
return loadCardCall(cardId).toDataSourceResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateTitle(cardId: String, title: String): DataSourceResult<Unit> {
|
||||||
|
return repository.updateTitle(cardId, title).toDataSourceResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateDescription(cardId: String, description: String): DataSourceResult<Unit> {
|
||||||
|
return repository.updateDescription(cardId, description).toDataSourceResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateDueDate(cardId: String, dueDate: LocalDate?): DataSourceResult<Unit> {
|
||||||
|
return repository.updateDueDate(cardId, dueDate).toDataSourceResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun listActivities(cardId: String): DataSourceResult<List<CardActivity>> {
|
||||||
|
return repository.listActivities(cardId).toDataSourceResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun addComment(cardId: String, comment: String): DataSourceResult<Unit> {
|
||||||
|
val result = repository.addComment(cardId, comment)
|
||||||
|
return when (result) {
|
||||||
|
is CardDetailRepository.Result.Success -> DataSourceResult.Success(Unit)
|
||||||
|
is CardDetailRepository.Result.Failure.SessionExpired -> DataSourceResult.SessionExpired
|
||||||
|
is CardDetailRepository.Result.Failure.Generic -> {
|
||||||
|
DataSourceResult.GenericError(result.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CardDetailViewModel(
|
class CardDetailViewModel(
|
||||||
@@ -93,7 +136,7 @@ class CardDetailViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
when (val result = repository.loadCard(cardId)) {
|
when (val result = repository.loadCard(cardId)) {
|
||||||
is CardDetailRepository.Result.Success -> {
|
is DataSourceResult.Success -> {
|
||||||
val detail = result.value
|
val detail = result.value
|
||||||
persistedTitle = detail.title
|
persistedTitle = detail.title
|
||||||
persistedDescription = detail.description
|
persistedDescription = detail.description
|
||||||
@@ -114,7 +157,7 @@ class CardDetailViewModel(
|
|||||||
loadActivities()
|
loadActivities()
|
||||||
}
|
}
|
||||||
|
|
||||||
is CardDetailRepository.Result.Failure.SessionExpired -> {
|
is DataSourceResult.SessionExpired -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isInitialLoading = false,
|
isInitialLoading = false,
|
||||||
@@ -124,7 +167,7 @@ class CardDetailViewModel(
|
|||||||
_events.emit(CardDetailUiEvent.SessionExpired)
|
_events.emit(CardDetailUiEvent.SessionExpired)
|
||||||
}
|
}
|
||||||
|
|
||||||
is CardDetailRepository.Result.Failure.Generic -> {
|
is DataSourceResult.GenericError -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isInitialLoading = false,
|
isInitialLoading = false,
|
||||||
@@ -249,7 +292,7 @@ class CardDetailViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.update { it.copy(isCommentSubmitting = true, commentErrorMessage = null) }
|
_uiState.update { it.copy(isCommentSubmitting = true, commentErrorMessage = null) }
|
||||||
when (val result = repository.addComment(cardId, payload)) {
|
when (val result = repository.addComment(cardId, payload)) {
|
||||||
is CardDetailRepository.Result.Success -> {
|
is DataSourceResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isCommentSubmitting = false,
|
isCommentSubmitting = false,
|
||||||
@@ -262,7 +305,7 @@ class CardDetailViewModel(
|
|||||||
loadActivities()
|
loadActivities()
|
||||||
}
|
}
|
||||||
|
|
||||||
is CardDetailRepository.Result.Failure.SessionExpired -> {
|
is DataSourceResult.SessionExpired -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isCommentSubmitting = false,
|
isCommentSubmitting = false,
|
||||||
@@ -272,7 +315,7 @@ class CardDetailViewModel(
|
|||||||
_events.emit(CardDetailUiEvent.SessionExpired)
|
_events.emit(CardDetailUiEvent.SessionExpired)
|
||||||
}
|
}
|
||||||
|
|
||||||
is CardDetailRepository.Result.Failure.Generic -> {
|
is DataSourceResult.GenericError -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isCommentSubmitting = false,
|
isCommentSubmitting = false,
|
||||||
@@ -310,7 +353,7 @@ class CardDetailViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.update { it.copy(isTitleSaving = true, titleErrorMessage = null) }
|
_uiState.update { it.copy(isTitleSaving = true, titleErrorMessage = null) }
|
||||||
when (val result = repository.updateTitle(cardId, normalized)) {
|
when (val result = repository.updateTitle(cardId, normalized)) {
|
||||||
is CardDetailRepository.Result.Success -> {
|
is DataSourceResult.Success -> {
|
||||||
persistedTitle = normalized
|
persistedTitle = normalized
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -321,7 +364,7 @@ class CardDetailViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is CardDetailRepository.Result.Failure.SessionExpired -> {
|
is DataSourceResult.SessionExpired -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isTitleSaving = false,
|
isTitleSaving = false,
|
||||||
@@ -331,7 +374,7 @@ class CardDetailViewModel(
|
|||||||
_events.emit(CardDetailUiEvent.SessionExpired)
|
_events.emit(CardDetailUiEvent.SessionExpired)
|
||||||
}
|
}
|
||||||
|
|
||||||
is CardDetailRepository.Result.Failure.Generic -> {
|
is DataSourceResult.GenericError -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isTitleSaving = false,
|
isTitleSaving = false,
|
||||||
@@ -387,7 +430,7 @@ class CardDetailViewModel(
|
|||||||
inFlightDescriptionPayload = normalized
|
inFlightDescriptionPayload = normalized
|
||||||
_uiState.update { it.copy(isDescriptionSaving = true, descriptionErrorMessage = null) }
|
_uiState.update { it.copy(isDescriptionSaving = true, descriptionErrorMessage = null) }
|
||||||
when (val result = repository.updateDescription(cardId, normalized)) {
|
when (val result = repository.updateDescription(cardId, normalized)) {
|
||||||
is CardDetailRepository.Result.Success -> {
|
is DataSourceResult.Success -> {
|
||||||
persistedDescription = normalized
|
persistedDescription = normalized
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
val stillDirty = it.description != persistedDescription
|
val stillDirty = it.description != persistedDescription
|
||||||
@@ -399,7 +442,7 @@ class CardDetailViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is CardDetailRepository.Result.Failure.SessionExpired -> {
|
is DataSourceResult.SessionExpired -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isDescriptionSaving = false,
|
isDescriptionSaving = false,
|
||||||
@@ -409,7 +452,7 @@ class CardDetailViewModel(
|
|||||||
_events.emit(CardDetailUiEvent.SessionExpired)
|
_events.emit(CardDetailUiEvent.SessionExpired)
|
||||||
}
|
}
|
||||||
|
|
||||||
is CardDetailRepository.Result.Failure.Generic -> {
|
is DataSourceResult.GenericError -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isDescriptionSaving = false,
|
isDescriptionSaving = false,
|
||||||
@@ -450,7 +493,7 @@ class CardDetailViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
when (val result = repository.updateDueDate(cardId, dueDate)) {
|
when (val result = repository.updateDueDate(cardId, dueDate)) {
|
||||||
is CardDetailRepository.Result.Success -> {
|
is DataSourceResult.Success -> {
|
||||||
persistedDueDate = dueDate
|
persistedDueDate = dueDate
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -460,7 +503,7 @@ class CardDetailViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is CardDetailRepository.Result.Failure.SessionExpired -> {
|
is DataSourceResult.SessionExpired -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isDueDateSaving = false,
|
isDueDateSaving = false,
|
||||||
@@ -470,7 +513,7 @@ class CardDetailViewModel(
|
|||||||
_events.emit(CardDetailUiEvent.SessionExpired)
|
_events.emit(CardDetailUiEvent.SessionExpired)
|
||||||
}
|
}
|
||||||
|
|
||||||
is CardDetailRepository.Result.Failure.Generic -> {
|
is DataSourceResult.GenericError -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isDueDateSaving = false,
|
isDueDateSaving = false,
|
||||||
@@ -489,17 +532,17 @@ class CardDetailViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.update { it.copy(isActivitiesLoading = true, activitiesErrorMessage = null) }
|
_uiState.update { it.copy(isActivitiesLoading = true, activitiesErrorMessage = null) }
|
||||||
when (val result = repository.listActivities(cardId)) {
|
when (val result = repository.listActivities(cardId)) {
|
||||||
is CardDetailRepository.Result.Success -> {
|
is DataSourceResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isActivitiesLoading = false,
|
isActivitiesLoading = false,
|
||||||
activities = result.value.sortedByDescending { activity -> activity.createdAtEpochMillis },
|
activities = result.value,
|
||||||
activitiesErrorMessage = null,
|
activitiesErrorMessage = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is CardDetailRepository.Result.Failure.SessionExpired -> {
|
is DataSourceResult.SessionExpired -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isActivitiesLoading = false,
|
isActivitiesLoading = false,
|
||||||
@@ -509,7 +552,7 @@ class CardDetailViewModel(
|
|||||||
_events.emit(CardDetailUiEvent.SessionExpired)
|
_events.emit(CardDetailUiEvent.SessionExpired)
|
||||||
}
|
}
|
||||||
|
|
||||||
is CardDetailRepository.Result.Failure.Generic -> {
|
is DataSourceResult.GenericError -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isActivitiesLoading = false,
|
isActivitiesLoading = false,
|
||||||
@@ -520,4 +563,25 @@ class CardDetailViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Factory(
|
||||||
|
private val cardId: String,
|
||||||
|
private val dataSource: CardDetailDataSource,
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
if (modelClass.isAssignableFrom(CardDetailViewModel::class.java)) {
|
||||||
|
return CardDetailViewModel(cardId = cardId, repository = dataSource) as T
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> CardDetailRepository.Result<T>.toDataSourceResult(): DataSourceResult<T> {
|
||||||
|
return when (this) {
|
||||||
|
is CardDetailRepository.Result.Success -> DataSourceResult.Success(value)
|
||||||
|
is CardDetailRepository.Result.Failure.Generic -> DataSourceResult.GenericError(message)
|
||||||
|
is CardDetailRepository.Result.Failure.SessionExpired -> DataSourceResult.SessionExpired
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ class CardDetailViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun initialLoad_fillsEditableFieldsAndActivities() = runTest {
|
fun initialLoad_fillsEditableFieldsAndActivities() = runTest {
|
||||||
val repository = FakeCardDetailDataSource(
|
val repository = FakeCardDetailDataSource(
|
||||||
loadCardResult = CardDetailRepository.Result.Success(sampleCardDetail()),
|
loadCardResult = DataSourceResult.Success(sampleCardDetail()),
|
||||||
listActivitiesResult = CardDetailRepository.Result.Success(sampleActivitiesShuffled()),
|
listActivitiesResult = DataSourceResult.Success(sampleActivitiesShuffled()),
|
||||||
)
|
)
|
||||||
val viewModel = CardDetailViewModel(
|
val viewModel = CardDetailViewModel(
|
||||||
cardId = "card-1",
|
cardId = "card-1",
|
||||||
@@ -61,7 +61,7 @@ class CardDetailViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun loadSessionExpired_emitsEvent_andRetryPathsDoNotRun() = runTest {
|
fun loadSessionExpired_emitsEvent_andRetryPathsDoNotRun() = runTest {
|
||||||
val repository = FakeCardDetailDataSource(
|
val repository = FakeCardDetailDataSource(
|
||||||
loadCardResult = CardDetailRepository.Result.Failure.SessionExpired(),
|
loadCardResult = DataSourceResult.SessionExpired,
|
||||||
)
|
)
|
||||||
val viewModel = CardDetailViewModel(cardId = "card-1", repository = repository)
|
val viewModel = CardDetailViewModel(cardId = "card-1", repository = repository)
|
||||||
val eventDeferred = async { viewModel.events.first() }
|
val eventDeferred = async { viewModel.events.first() }
|
||||||
@@ -90,8 +90,8 @@ class CardDetailViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun titleTrimRejectsBlank_andIsolatedFromDescription() = runTest {
|
fun titleTrimRejectsBlank_andIsolatedFromDescription() = runTest {
|
||||||
val repository = FakeCardDetailDataSource(
|
val repository = FakeCardDetailDataSource(
|
||||||
loadCardResult = CardDetailRepository.Result.Success(sampleCardDetail()),
|
loadCardResult = DataSourceResult.Success(sampleCardDetail()),
|
||||||
listActivitiesResult = CardDetailRepository.Result.Success(emptyList()),
|
listActivitiesResult = DataSourceResult.Success(emptyList()),
|
||||||
)
|
)
|
||||||
val viewModel = loadedViewModel(this, repository)
|
val viewModel = loadedViewModel(this, repository)
|
||||||
|
|
||||||
@@ -109,8 +109,8 @@ class CardDetailViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun dueDateSetAndClear_areIndependentAndSaved() = runTest {
|
fun dueDateSetAndClear_areIndependentAndSaved() = runTest {
|
||||||
val repository = FakeCardDetailDataSource(
|
val repository = FakeCardDetailDataSource(
|
||||||
loadCardResult = CardDetailRepository.Result.Success(sampleCardDetail()),
|
loadCardResult = DataSourceResult.Success(sampleCardDetail()),
|
||||||
listActivitiesResult = CardDetailRepository.Result.Success(emptyList()),
|
listActivitiesResult = DataSourceResult.Success(emptyList()),
|
||||||
)
|
)
|
||||||
val viewModel = loadedViewModel(this, repository)
|
val viewModel = loadedViewModel(this, repository)
|
||||||
|
|
||||||
@@ -130,8 +130,8 @@ class CardDetailViewModelTest {
|
|||||||
fun descriptionDebounce_savesLatestOnly_andSuppressesDuplicateInflight() = runTest {
|
fun descriptionDebounce_savesLatestOnly_andSuppressesDuplicateInflight() = runTest {
|
||||||
val gate = CompletableDeferred<Unit>()
|
val gate = CompletableDeferred<Unit>()
|
||||||
val repository = FakeCardDetailDataSource(
|
val repository = FakeCardDetailDataSource(
|
||||||
loadCardResult = CardDetailRepository.Result.Success(sampleCardDetail()),
|
loadCardResult = DataSourceResult.Success(sampleCardDetail()),
|
||||||
listActivitiesResult = CardDetailRepository.Result.Success(emptyList()),
|
listActivitiesResult = DataSourceResult.Success(emptyList()),
|
||||||
updateDescriptionGate = gate,
|
updateDescriptionGate = gate,
|
||||||
)
|
)
|
||||||
val viewModel = loadedViewModel(this, repository)
|
val viewModel = loadedViewModel(this, repository)
|
||||||
@@ -164,8 +164,8 @@ class CardDetailViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun descriptionFocusLossAndOnStop_flushLatestDirtyImmediately() = runTest {
|
fun descriptionFocusLossAndOnStop_flushLatestDirtyImmediately() = runTest {
|
||||||
val repository = FakeCardDetailDataSource(
|
val repository = FakeCardDetailDataSource(
|
||||||
loadCardResult = CardDetailRepository.Result.Success(sampleCardDetail()),
|
loadCardResult = DataSourceResult.Success(sampleCardDetail()),
|
||||||
listActivitiesResult = CardDetailRepository.Result.Success(emptyList()),
|
listActivitiesResult = DataSourceResult.Success(emptyList()),
|
||||||
)
|
)
|
||||||
val viewModel = loadedViewModel(this, repository)
|
val viewModel = loadedViewModel(this, repository)
|
||||||
|
|
||||||
@@ -186,8 +186,8 @@ class CardDetailViewModelTest {
|
|||||||
val firstGate = CompletableDeferred<Unit>()
|
val firstGate = CompletableDeferred<Unit>()
|
||||||
val secondGate = CompletableDeferred<Unit>()
|
val secondGate = CompletableDeferred<Unit>()
|
||||||
val repository = FakeCardDetailDataSource(
|
val repository = FakeCardDetailDataSource(
|
||||||
loadCardResult = CardDetailRepository.Result.Success(sampleCardDetail()),
|
loadCardResult = DataSourceResult.Success(sampleCardDetail()),
|
||||||
listActivitiesResult = CardDetailRepository.Result.Success(emptyList()),
|
listActivitiesResult = DataSourceResult.Success(emptyList()),
|
||||||
updateDescriptionGates = ArrayDeque(listOf(firstGate, secondGate)),
|
updateDescriptionGates = ArrayDeque(listOf(firstGate, secondGate)),
|
||||||
)
|
)
|
||||||
val viewModel = loadedViewModel(this, repository)
|
val viewModel = loadedViewModel(this, repository)
|
||||||
@@ -222,14 +222,14 @@ class CardDetailViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun addComment_success_closesDialog_showsSnackbar_refreshesActivities() = runTest {
|
fun addComment_success_closesDialog_showsSnackbar_refreshesActivities() = runTest {
|
||||||
val repository = FakeCardDetailDataSource(
|
val repository = FakeCardDetailDataSource(
|
||||||
loadCardResult = CardDetailRepository.Result.Success(sampleCardDetail()),
|
loadCardResult = DataSourceResult.Success(sampleCardDetail()),
|
||||||
listActivitiesResults = ArrayDeque(
|
listActivitiesResults = ArrayDeque(
|
||||||
listOf(
|
listOf(
|
||||||
CardDetailRepository.Result.Success(emptyList()),
|
DataSourceResult.Success(emptyList()),
|
||||||
CardDetailRepository.Result.Success(sampleActivitiesShuffled()),
|
DataSourceResult.Success(sampleActivitiesShuffled()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
addCommentResult = CardDetailRepository.Result.Success(Unit),
|
addCommentResult = DataSourceResult.Success(Unit),
|
||||||
)
|
)
|
||||||
val viewModel = loadedViewModel(this, repository)
|
val viewModel = loadedViewModel(this, repository)
|
||||||
val eventDeferred = async { viewModel.events.first { it is CardDetailUiEvent.ShowSnackbar } }
|
val eventDeferred = async { viewModel.events.first { it is CardDetailUiEvent.ShowSnackbar } }
|
||||||
@@ -250,9 +250,9 @@ class CardDetailViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun addComment_failure_keepsDialogOpen_andStoresRetryPayload() = runTest {
|
fun addComment_failure_keepsDialogOpen_andStoresRetryPayload() = runTest {
|
||||||
val repository = FakeCardDetailDataSource(
|
val repository = FakeCardDetailDataSource(
|
||||||
loadCardResult = CardDetailRepository.Result.Success(sampleCardDetail()),
|
loadCardResult = DataSourceResult.Success(sampleCardDetail()),
|
||||||
listActivitiesResult = CardDetailRepository.Result.Success(emptyList()),
|
listActivitiesResult = DataSourceResult.Success(emptyList()),
|
||||||
addCommentResult = CardDetailRepository.Result.Failure.Generic("Cannot add comment"),
|
addCommentResult = DataSourceResult.GenericError("Cannot add comment"),
|
||||||
)
|
)
|
||||||
val viewModel = loadedViewModel(this, repository)
|
val viewModel = loadedViewModel(this, repository)
|
||||||
|
|
||||||
@@ -272,11 +272,11 @@ class CardDetailViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun activitiesRetry_recoversFromFailure() = runTest {
|
fun activitiesRetry_recoversFromFailure() = runTest {
|
||||||
val repository = FakeCardDetailDataSource(
|
val repository = FakeCardDetailDataSource(
|
||||||
loadCardResult = CardDetailRepository.Result.Success(sampleCardDetail()),
|
loadCardResult = DataSourceResult.Success(sampleCardDetail()),
|
||||||
listActivitiesResults = ArrayDeque(
|
listActivitiesResults = ArrayDeque(
|
||||||
listOf(
|
listOf(
|
||||||
CardDetailRepository.Result.Failure.Generic("Network error"),
|
DataSourceResult.GenericError("Network error"),
|
||||||
CardDetailRepository.Result.Success(sampleActivitiesShuffled()),
|
DataSourceResult.Success(sampleActivitiesShuffled()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -296,8 +296,8 @@ class CardDetailViewModelTest {
|
|||||||
val viewModel = loadedViewModel(
|
val viewModel = loadedViewModel(
|
||||||
this,
|
this,
|
||||||
FakeCardDetailDataSource(
|
FakeCardDetailDataSource(
|
||||||
loadCardResult = CardDetailRepository.Result.Success(sampleCardDetail()),
|
loadCardResult = DataSourceResult.Success(sampleCardDetail()),
|
||||||
listActivitiesResult = CardDetailRepository.Result.Success(emptyList()),
|
listActivitiesResult = DataSourceResult.Success(emptyList()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -312,11 +312,11 @@ class CardDetailViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun fieldSpecificRetry_usesLastAttemptedPayload() = runTest {
|
fun fieldSpecificRetry_usesLastAttemptedPayload() = runTest {
|
||||||
val repository = FakeCardDetailDataSource(
|
val repository = FakeCardDetailDataSource(
|
||||||
loadCardResult = CardDetailRepository.Result.Success(sampleCardDetail()),
|
loadCardResult = DataSourceResult.Success(sampleCardDetail()),
|
||||||
listActivitiesResult = CardDetailRepository.Result.Success(emptyList()),
|
listActivitiesResult = DataSourceResult.Success(emptyList()),
|
||||||
updateTitleResult = CardDetailRepository.Result.Failure.Generic("title failed"),
|
updateTitleResult = DataSourceResult.GenericError("title failed"),
|
||||||
updateDueDateResult = CardDetailRepository.Result.Failure.Generic("due failed"),
|
updateDueDateResult = DataSourceResult.GenericError("due failed"),
|
||||||
updateDescriptionResult = CardDetailRepository.Result.Failure.Generic("desc failed"),
|
updateDescriptionResult = DataSourceResult.GenericError("desc failed"),
|
||||||
)
|
)
|
||||||
val viewModel = loadedViewModel(this, repository)
|
val viewModel = loadedViewModel(this, repository)
|
||||||
|
|
||||||
@@ -358,13 +358,13 @@ class CardDetailViewModelTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class FakeCardDetailDataSource(
|
private class FakeCardDetailDataSource(
|
||||||
var loadCardResult: CardDetailRepository.Result<CardDetail> = CardDetailRepository.Result.Success(sampleCardDetail()),
|
var loadCardResult: DataSourceResult<CardDetail> = DataSourceResult.Success(sampleCardDetail()),
|
||||||
var listActivitiesResult: CardDetailRepository.Result<List<CardActivity>> = CardDetailRepository.Result.Success(emptyList()),
|
var listActivitiesResult: DataSourceResult<List<CardActivity>> = DataSourceResult.Success(emptyList()),
|
||||||
var listActivitiesResults: ArrayDeque<CardDetailRepository.Result<List<CardActivity>>> = ArrayDeque(),
|
var listActivitiesResults: ArrayDeque<DataSourceResult<List<CardActivity>>> = ArrayDeque(),
|
||||||
var updateTitleResult: CardDetailRepository.Result<Unit> = CardDetailRepository.Result.Success(Unit),
|
var updateTitleResult: DataSourceResult<Unit> = DataSourceResult.Success(Unit),
|
||||||
var updateDescriptionResult: CardDetailRepository.Result<Unit> = CardDetailRepository.Result.Success(Unit),
|
var updateDescriptionResult: DataSourceResult<Unit> = DataSourceResult.Success(Unit),
|
||||||
var updateDueDateResult: CardDetailRepository.Result<Unit> = CardDetailRepository.Result.Success(Unit),
|
var updateDueDateResult: DataSourceResult<Unit> = DataSourceResult.Success(Unit),
|
||||||
var addCommentResult: CardDetailRepository.Result<Unit> = CardDetailRepository.Result.Success(Unit),
|
var addCommentResult: DataSourceResult<Unit> = DataSourceResult.Success(Unit),
|
||||||
var updateDescriptionGate: CompletableDeferred<Unit>? = null,
|
var updateDescriptionGate: CompletableDeferred<Unit>? = null,
|
||||||
var updateDescriptionGates: ArrayDeque<CompletableDeferred<Unit>> = ArrayDeque(),
|
var updateDescriptionGates: ArrayDeque<CompletableDeferred<Unit>> = ArrayDeque(),
|
||||||
) : CardDetailDataSource {
|
) : CardDetailDataSource {
|
||||||
@@ -380,18 +380,18 @@ class CardDetailViewModelTest {
|
|||||||
val updateDueDatePayloads: MutableList<LocalDate?> = mutableListOf()
|
val updateDueDatePayloads: MutableList<LocalDate?> = mutableListOf()
|
||||||
val addCommentPayloads: MutableList<String> = mutableListOf()
|
val addCommentPayloads: MutableList<String> = mutableListOf()
|
||||||
|
|
||||||
override suspend fun loadCard(cardId: String): CardDetailRepository.Result<CardDetail> {
|
override suspend fun loadCard(cardId: String): DataSourceResult<CardDetail> {
|
||||||
loadCalls += 1
|
loadCalls += 1
|
||||||
return loadCardResult
|
return loadCardResult
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateTitle(cardId: String, title: String): CardDetailRepository.Result<Unit> {
|
override suspend fun updateTitle(cardId: String, title: String): DataSourceResult<Unit> {
|
||||||
updateTitleCalls += 1
|
updateTitleCalls += 1
|
||||||
updateTitlePayloads += title
|
updateTitlePayloads += title
|
||||||
return updateTitleResult
|
return updateTitleResult
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateDescription(cardId: String, description: String): CardDetailRepository.Result<Unit> {
|
override suspend fun updateDescription(cardId: String, description: String): DataSourceResult<Unit> {
|
||||||
updateDescriptionCalls += 1
|
updateDescriptionCalls += 1
|
||||||
updateDescriptionPayloads += description
|
updateDescriptionPayloads += description
|
||||||
if (updateDescriptionGates.isNotEmpty()) {
|
if (updateDescriptionGates.isNotEmpty()) {
|
||||||
@@ -401,13 +401,13 @@ class CardDetailViewModelTest {
|
|||||||
return updateDescriptionResult
|
return updateDescriptionResult
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateDueDate(cardId: String, dueDate: LocalDate?): CardDetailRepository.Result<Unit> {
|
override suspend fun updateDueDate(cardId: String, dueDate: LocalDate?): DataSourceResult<Unit> {
|
||||||
updateDueDateCalls += 1
|
updateDueDateCalls += 1
|
||||||
updateDueDatePayloads += dueDate
|
updateDueDatePayloads += dueDate
|
||||||
return updateDueDateResult
|
return updateDueDateResult
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun listActivities(cardId: String): CardDetailRepository.Result<List<CardActivity>> {
|
override suspend fun listActivities(cardId: String): DataSourceResult<List<CardActivity>> {
|
||||||
listActivitiesCalls += 1
|
listActivitiesCalls += 1
|
||||||
if (listActivitiesResults.isNotEmpty()) {
|
if (listActivitiesResults.isNotEmpty()) {
|
||||||
return listActivitiesResults.removeFirst()
|
return listActivitiesResults.removeFirst()
|
||||||
@@ -415,7 +415,7 @@ class CardDetailViewModelTest {
|
|||||||
return listActivitiesResult
|
return listActivitiesResult
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun addComment(cardId: String, comment: String): CardDetailRepository.Result<Unit> {
|
override suspend fun addComment(cardId: String, comment: String): DataSourceResult<Unit> {
|
||||||
addCommentCalls += 1
|
addCommentCalls += 1
|
||||||
addCommentPayloads += comment
|
addCommentPayloads += comment
|
||||||
return addCommentResult
|
return addCommentResult
|
||||||
@@ -440,9 +440,9 @@ class CardDetailViewModelTest {
|
|||||||
|
|
||||||
fun sampleActivitiesShuffled(): List<CardActivity> {
|
fun sampleActivitiesShuffled(): List<CardActivity> {
|
||||||
return listOf(
|
return listOf(
|
||||||
|
CardActivity(id = "a-new", type = "comment", text = "new", createdAtEpochMillis = 3L),
|
||||||
CardActivity(id = "a-mid", type = "comment", text = "mid", createdAtEpochMillis = 2L),
|
CardActivity(id = "a-mid", type = "comment", text = "mid", createdAtEpochMillis = 2L),
|
||||||
CardActivity(id = "a-old", type = "comment", text = "old", createdAtEpochMillis = 1L),
|
CardActivity(id = "a-old", type = "comment", text = "old", createdAtEpochMillis = 1L),
|
||||||
CardActivity(id = "a-new", type = "comment", text = "new", createdAtEpochMillis = 3L),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user