fix: preserve pending description flush during in-flight save
This commit is contained in:
@@ -76,6 +76,7 @@ class CardDetailViewModel(
|
||||
private var lastAttemptedComment: String? = null
|
||||
|
||||
private var inFlightDescriptionPayload: String? = null
|
||||
private var pendingDescriptionPayload: String? = null
|
||||
private var descriptionDebounceJob: Job? = null
|
||||
|
||||
fun load() {
|
||||
@@ -367,16 +368,20 @@ class CardDetailViewModel(
|
||||
}
|
||||
val normalized = rawDescription
|
||||
if (!fromRetry && normalized == persistedDescription) {
|
||||
_uiState.update { it.copy(isDescriptionDirty = false) }
|
||||
_uiState.update { it.copy(isDescriptionDirty = it.description != persistedDescription) }
|
||||
return
|
||||
}
|
||||
if (!fromRetry && normalized == inFlightDescriptionPayload) {
|
||||
return
|
||||
}
|
||||
if (_uiState.value.isDescriptionSaving) {
|
||||
if (normalized != inFlightDescriptionPayload) {
|
||||
pendingDescriptionPayload = normalized
|
||||
}
|
||||
return
|
||||
}
|
||||
lastAttemptedDescription = normalized
|
||||
pendingDescriptionPayload = null
|
||||
|
||||
viewModelScope.launch {
|
||||
inFlightDescriptionPayload = normalized
|
||||
@@ -385,10 +390,10 @@ class CardDetailViewModel(
|
||||
is CardDetailRepository.Result.Success -> {
|
||||
persistedDescription = normalized
|
||||
_uiState.update {
|
||||
val stillDirty = it.description != persistedDescription
|
||||
it.copy(
|
||||
description = normalized,
|
||||
isDescriptionSaving = false,
|
||||
isDescriptionDirty = false,
|
||||
isDescriptionDirty = stillDirty,
|
||||
descriptionErrorMessage = null,
|
||||
)
|
||||
}
|
||||
@@ -408,13 +413,18 @@ class CardDetailViewModel(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isDescriptionSaving = false,
|
||||
isDescriptionDirty = true,
|
||||
isDescriptionDirty = it.description != persistedDescription,
|
||||
descriptionErrorMessage = result.message,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
inFlightDescriptionPayload = null
|
||||
|
||||
val pending = pendingDescriptionPayload
|
||||
if (!_uiState.value.isSessionExpired && pending != null && pending != persistedDescription) {
|
||||
saveDescription(pending)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -181,6 +181,44 @@ class CardDetailViewModelTest {
|
||||
assertFalse(viewModel.uiState.value.isDescriptionDirty)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onStopDuringInflightSave_preservesPendingLatest_andDirtyUntilLatestCompletes() = runTest {
|
||||
val firstGate = CompletableDeferred<Unit>()
|
||||
val secondGate = CompletableDeferred<Unit>()
|
||||
val repository = FakeCardDetailDataSource(
|
||||
loadCardResult = CardDetailRepository.Result.Success(sampleCardDetail()),
|
||||
listActivitiesResult = CardDetailRepository.Result.Success(emptyList()),
|
||||
updateDescriptionGates = ArrayDeque(listOf(firstGate, secondGate)),
|
||||
)
|
||||
val viewModel = loadedViewModel(this, repository)
|
||||
|
||||
viewModel.onDescriptionChanged("first")
|
||||
advanceTimeBy(800)
|
||||
runCurrent()
|
||||
assertEquals(listOf("first"), repository.updateDescriptionPayloads)
|
||||
|
||||
viewModel.onDescriptionChanged("second")
|
||||
viewModel.onStop()
|
||||
runCurrent()
|
||||
assertEquals(1, repository.updateDescriptionCalls)
|
||||
|
||||
firstGate.complete(Unit)
|
||||
runCurrent()
|
||||
|
||||
assertEquals(listOf("first", "second"), repository.updateDescriptionPayloads)
|
||||
assertTrue(viewModel.uiState.value.isDescriptionDirty)
|
||||
assertTrue(viewModel.uiState.value.isDescriptionSaving)
|
||||
assertEquals("second", viewModel.uiState.value.description)
|
||||
|
||||
secondGate.complete(Unit)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(listOf("first", "second"), repository.updateDescriptionPayloads)
|
||||
assertFalse(viewModel.uiState.value.isDescriptionDirty)
|
||||
assertFalse(viewModel.uiState.value.isDescriptionSaving)
|
||||
assertEquals("second", viewModel.uiState.value.description)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addComment_success_closesDialog_showsSnackbar_refreshesActivities() = runTest {
|
||||
val repository = FakeCardDetailDataSource(
|
||||
@@ -328,6 +366,7 @@ class CardDetailViewModelTest {
|
||||
var updateDueDateResult: CardDetailRepository.Result<Unit> = CardDetailRepository.Result.Success(Unit),
|
||||
var addCommentResult: CardDetailRepository.Result<Unit> = CardDetailRepository.Result.Success(Unit),
|
||||
var updateDescriptionGate: CompletableDeferred<Unit>? = null,
|
||||
var updateDescriptionGates: ArrayDeque<CompletableDeferred<Unit>> = ArrayDeque(),
|
||||
) : CardDetailDataSource {
|
||||
var loadCalls: Int = 0
|
||||
var updateTitleCalls: Int = 0
|
||||
@@ -355,6 +394,9 @@ class CardDetailViewModelTest {
|
||||
override suspend fun updateDescription(cardId: String, description: String): CardDetailRepository.Result<Unit> {
|
||||
updateDescriptionCalls += 1
|
||||
updateDescriptionPayloads += description
|
||||
if (updateDescriptionGates.isNotEmpty()) {
|
||||
updateDescriptionGates.removeFirst().await()
|
||||
}
|
||||
updateDescriptionGate?.await()
|
||||
return updateDescriptionResult
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user