fix: avoid redundant card activity reload and assert ordering
This commit is contained in:
@@ -180,8 +180,9 @@ class CardDetailFlowTest {
|
|||||||
onView(withText(R.string.add)).perform(click())
|
onView(withText(R.string.add)).perform(click())
|
||||||
|
|
||||||
awaitCondition {
|
awaitCondition {
|
||||||
fakeDataSource.addCommentCalls.get() == 1 && fakeDataSource.listActivitiesCalls.get() >= 2
|
fakeDataSource.addCommentCalls.get() == 1
|
||||||
}
|
}
|
||||||
|
assertEquals(1, fakeDataSource.listActivitiesCalls.get())
|
||||||
assertTrue(observedEvents.any { it is CardDetailUiEvent.ShowSnackbar })
|
assertTrue(observedEvents.any { it is CardDetailUiEvent.ShowSnackbar })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +197,7 @@ class CardDetailFlowTest {
|
|||||||
onView(withText(containsString("Someone commented"))).check(matches(isDisplayed()))
|
onView(withText(containsString("Someone commented"))).check(matches(isDisplayed()))
|
||||||
onView(withText(containsString("Someone updated this card"))).check(matches(isDisplayed()))
|
onView(withText(containsString("Someone updated this card"))).check(matches(isDisplayed()))
|
||||||
onView(withText(containsString("body"))).check(matches(isDisplayed()))
|
onView(withText(containsString("body"))).check(matches(isDisplayed()))
|
||||||
|
assertEquals(listOf("a-new", "a-old"), observedStates.last().activities.map { it.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -360,15 +362,20 @@ class CardDetailFlowTest {
|
|||||||
return if (listActivitiesResults.isNotEmpty()) listActivitiesResults.removeFirst() else DataSourceResult.Success(activities)
|
return if (listActivitiesResults.isNotEmpty()) listActivitiesResults.removeFirst() else DataSourceResult.Success(activities)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun addComment(cardId: String, comment: String): DataSourceResult<Unit> {
|
override suspend fun addComment(cardId: String, comment: String): DataSourceResult<List<CardActivity>> {
|
||||||
addCommentCalls.incrementAndGet()
|
addCommentCalls.incrementAndGet()
|
||||||
val result = if (addCommentResults.isNotEmpty()) addCommentResults.removeFirst() else DataSourceResult.Success(Unit)
|
val result = if (addCommentResults.isNotEmpty()) addCommentResults.removeFirst() else DataSourceResult.Success(Unit)
|
||||||
if (result is DataSourceResult.Success) {
|
if (result is DataSourceResult.Success) {
|
||||||
activities = listOf(
|
activities = listOf(
|
||||||
CardActivity("comment-${addCommentCalls.get()}", "comment", comment, System.currentTimeMillis()),
|
CardActivity("comment-${addCommentCalls.get()}", "comment", comment, System.currentTimeMillis()),
|
||||||
) + activities
|
) + activities
|
||||||
|
return DataSourceResult.Success(activities)
|
||||||
|
}
|
||||||
|
return when (result) {
|
||||||
|
is DataSourceResult.GenericError -> result
|
||||||
|
DataSourceResult.SessionExpired -> DataSourceResult.SessionExpired
|
||||||
|
is DataSourceResult.Success -> DataSourceResult.Success(activities)
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,13 @@ class CardDetailActivity : AppCompatActivity() {
|
|||||||
val id = cardId
|
val id = cardId
|
||||||
val fakeFactory = testDataSourceFactory
|
val fakeFactory = testDataSourceFactory
|
||||||
if (fakeFactory != null) {
|
if (fakeFactory != null) {
|
||||||
CardDetailViewModel.Factory(id, fakeFactory.invoke(id))
|
CardDetailViewModel.Factory(
|
||||||
|
cardId = id,
|
||||||
|
dataSource = fakeFactory.invoke(id),
|
||||||
|
titleRequiredMessage = getString(R.string.card_title_required),
|
||||||
|
commentRequiredMessage = getString(R.string.card_detail_comment_required),
|
||||||
|
commentAddedMessage = getString(R.string.card_detail_comment_added),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
val repository = CardDetailRepository(
|
val repository = CardDetailRepository(
|
||||||
sessionStore = sessionStore,
|
sessionStore = sessionStore,
|
||||||
@@ -107,6 +113,9 @@ class CardDetailActivity : AppCompatActivity() {
|
|||||||
CardDetailViewModel.Factory(
|
CardDetailViewModel.Factory(
|
||||||
cardId = id,
|
cardId = id,
|
||||||
dataSource = CardDetailRepositoryDataSource(repository, repository::loadCard),
|
dataSource = CardDetailRepositoryDataSource(repository, repository::loadCard),
|
||||||
|
titleRequiredMessage = getString(R.string.card_title_required),
|
||||||
|
commentRequiredMessage = getString(R.string.card_detail_comment_required),
|
||||||
|
commentAddedMessage = getString(R.string.card_detail_comment_added),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ interface CardDetailDataSource {
|
|||||||
suspend fun updateDescription(cardId: String, description: String): DataSourceResult<Unit>
|
suspend fun updateDescription(cardId: String, description: String): DataSourceResult<Unit>
|
||||||
suspend fun updateDueDate(cardId: String, dueDate: LocalDate?): DataSourceResult<Unit>
|
suspend fun updateDueDate(cardId: String, dueDate: LocalDate?): DataSourceResult<Unit>
|
||||||
suspend fun listActivities(cardId: String): DataSourceResult<List<CardActivity>>
|
suspend fun listActivities(cardId: String): DataSourceResult<List<CardActivity>>
|
||||||
suspend fun addComment(cardId: String, comment: String): DataSourceResult<Unit>
|
suspend fun addComment(cardId: String, comment: String): DataSourceResult<List<CardActivity>>
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class CardDetailRepositoryDataSource(
|
internal class CardDetailRepositoryDataSource(
|
||||||
@@ -85,10 +85,10 @@ internal class CardDetailRepositoryDataSource(
|
|||||||
return repository.listActivities(cardId).toDataSourceResult()
|
return repository.listActivities(cardId).toDataSourceResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun addComment(cardId: String, comment: String): DataSourceResult<Unit> {
|
override suspend fun addComment(cardId: String, comment: String): DataSourceResult<List<CardActivity>> {
|
||||||
val result = repository.addComment(cardId, comment)
|
val result = repository.addComment(cardId, comment)
|
||||||
return when (result) {
|
return when (result) {
|
||||||
is CardDetailRepository.Result.Success -> DataSourceResult.Success(Unit)
|
is CardDetailRepository.Result.Success -> DataSourceResult.Success(result.value)
|
||||||
is CardDetailRepository.Result.Failure.SessionExpired -> DataSourceResult.SessionExpired
|
is CardDetailRepository.Result.Failure.SessionExpired -> DataSourceResult.SessionExpired
|
||||||
is CardDetailRepository.Result.Failure.Generic -> {
|
is CardDetailRepository.Result.Failure.Generic -> {
|
||||||
DataSourceResult.GenericError(result.message)
|
DataSourceResult.GenericError(result.message)
|
||||||
@@ -101,6 +101,9 @@ class CardDetailViewModel(
|
|||||||
private val cardId: String,
|
private val cardId: String,
|
||||||
private val repository: CardDetailDataSource,
|
private val repository: CardDetailDataSource,
|
||||||
private val descriptionDebounceMillis: Long = 800L,
|
private val descriptionDebounceMillis: Long = 800L,
|
||||||
|
private val titleRequiredMessage: String = "Card title is required",
|
||||||
|
private val commentRequiredMessage: String = "Comment is required",
|
||||||
|
private val commentAddedMessage: String = "Comment added",
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _uiState = MutableStateFlow(CardDetailUiState())
|
private val _uiState = MutableStateFlow(CardDetailUiState())
|
||||||
val uiState: StateFlow<CardDetailUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<CardDetailUiState> = _uiState.asStateFlow()
|
||||||
@@ -284,7 +287,7 @@ class CardDetailViewModel(
|
|||||||
|
|
||||||
val payload = _uiState.value.commentDraft.trim()
|
val payload = _uiState.value.commentDraft.trim()
|
||||||
if (payload.isBlank()) {
|
if (payload.isBlank()) {
|
||||||
_uiState.update { it.copy(commentErrorMessage = "Comment is required") }
|
_uiState.update { it.copy(commentErrorMessage = commentRequiredMessage) }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,10 +302,12 @@ class CardDetailViewModel(
|
|||||||
isCommentDialogOpen = false,
|
isCommentDialogOpen = false,
|
||||||
commentDraft = "",
|
commentDraft = "",
|
||||||
commentDialogMode = CommentDialogMode.EDIT,
|
commentDialogMode = CommentDialogMode.EDIT,
|
||||||
|
activities = result.value,
|
||||||
|
activitiesErrorMessage = null,
|
||||||
|
isActivitiesLoading = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_events.emit(CardDetailUiEvent.ShowSnackbar("Comment added"))
|
_events.emit(CardDetailUiEvent.ShowSnackbar(commentAddedMessage))
|
||||||
loadActivities()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is DataSourceResult.SessionExpired -> {
|
is DataSourceResult.SessionExpired -> {
|
||||||
@@ -342,7 +347,7 @@ class CardDetailViewModel(
|
|||||||
}
|
}
|
||||||
val normalized = rawTitle.trim()
|
val normalized = rawTitle.trim()
|
||||||
if (normalized.isBlank()) {
|
if (normalized.isBlank()) {
|
||||||
_uiState.update { it.copy(titleErrorMessage = "Card title is required") }
|
_uiState.update { it.copy(titleErrorMessage = titleRequiredMessage) }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!fromRetry && normalized == persistedTitle) {
|
if (!fromRetry && normalized == persistedTitle) {
|
||||||
@@ -567,11 +572,20 @@ class CardDetailViewModel(
|
|||||||
class Factory(
|
class Factory(
|
||||||
private val cardId: String,
|
private val cardId: String,
|
||||||
private val dataSource: CardDetailDataSource,
|
private val dataSource: CardDetailDataSource,
|
||||||
|
private val titleRequiredMessage: String = "Card title is required",
|
||||||
|
private val commentRequiredMessage: String = "Comment is required",
|
||||||
|
private val commentAddedMessage: String = "Comment added",
|
||||||
) : ViewModelProvider.Factory {
|
) : ViewModelProvider.Factory {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
if (modelClass.isAssignableFrom(CardDetailViewModel::class.java)) {
|
if (modelClass.isAssignableFrom(CardDetailViewModel::class.java)) {
|
||||||
return CardDetailViewModel(cardId = cardId, repository = dataSource) as T
|
return CardDetailViewModel(
|
||||||
|
cardId = cardId,
|
||||||
|
repository = dataSource,
|
||||||
|
titleRequiredMessage = titleRequiredMessage,
|
||||||
|
commentRequiredMessage = commentRequiredMessage,
|
||||||
|
commentAddedMessage = commentAddedMessage,
|
||||||
|
) as T
|
||||||
}
|
}
|
||||||
throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
|
throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,4 +85,6 @@
|
|||||||
<string name="card_detail_timeline_action_commented">commented</string>
|
<string name="card_detail_timeline_action_commented">commented</string>
|
||||||
<string name="card_detail_timeline_action_updated">updated this card</string>
|
<string name="card_detail_timeline_action_updated">updated this card</string>
|
||||||
<string name="add">Add</string>
|
<string name="add">Add</string>
|
||||||
|
<string name="card_detail_comment_required">Comment is required</string>
|
||||||
|
<string name="card_detail_comment_added">Comment added</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -227,10 +227,9 @@ class CardDetailViewModelTest {
|
|||||||
listActivitiesResults = ArrayDeque(
|
listActivitiesResults = ArrayDeque(
|
||||||
listOf(
|
listOf(
|
||||||
DataSourceResult.Success(emptyList()),
|
DataSourceResult.Success(emptyList()),
|
||||||
DataSourceResult.Success(sampleActivitiesShuffled()),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
addCommentResult = DataSourceResult.Success(Unit),
|
addCommentResult = DataSourceResult.Success(sampleActivitiesShuffled()),
|
||||||
)
|
)
|
||||||
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 } }
|
||||||
@@ -243,7 +242,7 @@ class CardDetailViewModelTest {
|
|||||||
assertFalse(viewModel.uiState.value.isCommentDialogOpen)
|
assertFalse(viewModel.uiState.value.isCommentDialogOpen)
|
||||||
assertEquals("", viewModel.uiState.value.commentDraft)
|
assertEquals("", viewModel.uiState.value.commentDraft)
|
||||||
assertEquals(1, repository.addCommentCalls)
|
assertEquals(1, repository.addCommentCalls)
|
||||||
assertEquals(2, repository.listActivitiesCalls)
|
assertEquals(1, repository.listActivitiesCalls)
|
||||||
assertEquals(listOf("a-mid", "a-new", "a-old"), viewModel.uiState.value.activities.map { it.id })
|
assertEquals(listOf("a-mid", "a-new", "a-old"), viewModel.uiState.value.activities.map { it.id })
|
||||||
assertTrue(eventDeferred.await() is CardDetailUiEvent.ShowSnackbar)
|
assertTrue(eventDeferred.await() is CardDetailUiEvent.ShowSnackbar)
|
||||||
}
|
}
|
||||||
@@ -365,7 +364,7 @@ class CardDetailViewModelTest {
|
|||||||
var updateTitleResult: DataSourceResult<Unit> = DataSourceResult.Success(Unit),
|
var updateTitleResult: DataSourceResult<Unit> = DataSourceResult.Success(Unit),
|
||||||
var updateDescriptionResult: DataSourceResult<Unit> = DataSourceResult.Success(Unit),
|
var updateDescriptionResult: DataSourceResult<Unit> = DataSourceResult.Success(Unit),
|
||||||
var updateDueDateResult: DataSourceResult<Unit> = DataSourceResult.Success(Unit),
|
var updateDueDateResult: DataSourceResult<Unit> = DataSourceResult.Success(Unit),
|
||||||
var addCommentResult: DataSourceResult<Unit> = DataSourceResult.Success(Unit),
|
var addCommentResult: DataSourceResult<List<CardActivity>> = DataSourceResult.Success(emptyList()),
|
||||||
var updateDescriptionGate: CompletableDeferred<Unit>? = null,
|
var updateDescriptionGate: CompletableDeferred<Unit>? = null,
|
||||||
var updateDescriptionGates: ArrayDeque<CompletableDeferred<Unit>> = ArrayDeque(),
|
var updateDescriptionGates: ArrayDeque<CompletableDeferred<Unit>> = ArrayDeque(),
|
||||||
) : CardDetailDataSource {
|
) : CardDetailDataSource {
|
||||||
@@ -416,7 +415,7 @@ class CardDetailViewModelTest {
|
|||||||
return listActivitiesResult
|
return listActivitiesResult
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun addComment(cardId: String, comment: String): DataSourceResult<Unit> {
|
override suspend fun addComment(cardId: String, comment: String): DataSourceResult<List<CardActivity>> {
|
||||||
addCommentCalls += 1
|
addCommentCalls += 1
|
||||||
addCommentPayloads += comment
|
addCommentPayloads += comment
|
||||||
return addCommentResult
|
return addCommentResult
|
||||||
|
|||||||
Reference in New Issue
Block a user