Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/jacoco-rules/unit/measured-composable-sources.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ conversation/screen/ConversationScreenRoute.kt
conversation/screen/ConversationSelectionTopAppBar.kt
conversation/screen/ConversationSimSheetState.kt
conversation/screen/ConversationTargetMessageEffects.kt
conversationlist/common/pane/ListPaneColors.kt
recipientselection/component/MultiDestinationContactRow.kt
recipientselection/component/RecipientSelectionContactAvatar.kt
recipientselection/component/RecipientSelectionContactRow.kt
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.android.messaging.ui.conversationlist.archived

import app.cash.turbine.test
import com.android.messaging.data.conversation.model.ConversationId
import com.android.messaging.data.conversationlist.model.ConversationListSnapshot
import com.android.messaging.domain.conversation.usecase.participant.model.ResolveContactActionResult
import com.android.messaging.testutil.MainDispatcherRule
import com.android.messaging.ui.conversationlist.archived.mapper.ArchivedConversationListUiStateMapper
import com.android.messaging.ui.conversationlist.archived.model.ArchivedConversationListAction as Action
import com.android.messaging.ui.conversationlist.archived.model.ArchivedConversationListUiState as State
import com.android.messaging.ui.conversationlist.conversationItem
import com.android.messaging.ui.conversationlist.delegate.ConversationListOptimisticSnapshotDelegate
import com.android.messaging.ui.conversationlist.delegate.ConversationListSelectionDelegate
import com.android.messaging.ui.conversationlist.snapshotOf
import io.mockk.MockKMatcherScope
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import io.mockk.verifyOrder
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test

@OptIn(ExperimentalCoroutinesApi::class)
internal class ArchivedConversationListViewModelTest {

@get:Rule
val mainDispatcherRule = MainDispatcherRule()

private val uiStateMapper = mockk<ArchivedConversationListUiStateMapper>()
private val optimisticSnapshotDelegate =
mockk<ConversationListOptimisticSnapshotDelegate>(relaxed = true)
private val selectionDelegate = mockk<ConversationListSelectionDelegate>(relaxed = true)

private val snapshotFlow = MutableStateFlow<ConversationListSnapshot?>(
snapshotOf(conversationItem(conversationId = OPENED_ID, isArchived = true)),
)
private val selectedIdsFlow = MutableStateFlow<ImmutableList<ConversationId>>(
persistentListOf(),
)

@Test
fun uiState_noOpenedConversation_mapsWithoutOpenedConversation() {
runTest(context = mainDispatcherRule.testDispatcher) {
val viewModel = createViewModel()

viewModel.uiState.test {
advanceUntilIdle()

verify { mapWithOpenedConversation(openedConversationId = null) }
cancelAndIgnoreRemainingEvents()
}
}
}

@Test
fun openedConversationChanged_toConversation_remapsWithOpenedConversation() {
runTest(context = mainDispatcherRule.testDispatcher) {
val viewModel = createViewModel()

viewModel.uiState.test {
viewModel.onAction(Action.OpenedConversationChanged(OPENED_ID))
advanceUntilIdle()

verify { mapWithOpenedConversation(openedConversationId = OPENED_ID) }
cancelAndIgnoreRemainingEvents()
}
}
}

@Test
fun openedConversationChanged_toNull_remapsWithoutOpenedConversation() {
runTest(context = mainDispatcherRule.testDispatcher) {
val viewModel = createViewModel()

viewModel.uiState.test {
viewModel.onAction(Action.OpenedConversationChanged(OPENED_ID))
advanceUntilIdle()

viewModel.onAction(Action.OpenedConversationChanged(null))
advanceUntilIdle()

verifyOrder {
mapWithOpenedConversation(openedConversationId = OPENED_ID)
mapWithOpenedConversation(openedConversationId = null)
}
cancelAndIgnoreRemainingEvents()
}
}
}

private fun MockKMatcherScope.mapWithOpenedConversation(
openedConversationId: ConversationId?,
): State {
return uiStateMapper.map(
snapshot = any(),
selectedConversationIds = any(),
openedConversationId = openedConversationId,
isDebugEnabled = false,
)
}

private fun createViewModel(): ArchivedConversationListViewModel {
every { optimisticSnapshotDelegate.snapshot } returns snapshotFlow
every { selectionDelegate.selectedIds } returns selectedIdsFlow
every { uiStateMapper.map(any(), any(), any(), any()) } returns State()

return ArchivedConversationListViewModel(
actionsDelegate = mockk(relaxed = true),
optimisticSnapshotDelegate = optimisticSnapshotDelegate,
selectionDelegate = selectionDelegate,
uiStateMapper = uiStateMapper,
resolveContactAction = { _, _, _ -> ResolveContactActionResult.Unavailable },
debugFeaturesProvider = { false },
defaultDispatcher = mainDispatcherRule.testDispatcher,
)
}

private companion object {
private val OPENED_ID = ConversationId("opened")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.android.messaging.ui.conversationlist.archived.mapper

import android.content.Context
import com.android.messaging.data.conversation.model.ConversationId
import com.android.messaging.data.conversationlist.model.ConversationListSnapshot
import com.android.messaging.data.phone.formatter.PhoneNumberFormatter
import com.android.messaging.domain.conversation.usecase.avatar.ResolveAvatarUri
import com.android.messaging.domain.conversation.usecase.participant.CanShowOrAddContact
import com.android.messaging.domain.conversation.usecase.participant.IsContactSaved
import com.android.messaging.domain.conversation.usecase.telephony.CanPlacePhoneCall
import com.android.messaging.ui.conversationlist.archived.model.ArchivedConversationListUiState
import com.android.messaging.ui.conversationlist.conversationItem
import com.android.messaging.ui.conversationlist.mapper.ConversationListContentUiStateMapperImpl
import com.android.messaging.ui.conversationlist.mapper.ConversationListItemUiMapperImpl
import com.android.messaging.ui.conversationlist.model.ConversationListContentUiState
import com.android.messaging.ui.conversationlist.model.ConversationListItemUiModel
import com.android.messaging.ui.conversationlist.snapshotOf
import com.android.messaging.util.OsUtil
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import kotlinx.collections.immutable.persistentListOf
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test

internal class ArchivedConversationListUiStateMapperImplTest {

private val itemUiMapper = ConversationListItemUiMapperImpl(
context = mockk<Context>(relaxed = true),
canPlacePhoneCall = mockk<CanPlacePhoneCall>(relaxed = true),
canShowOrAddContact = mockk<CanShowOrAddContact>(relaxed = true),
isContactSaved = mockk<IsContactSaved>(relaxed = true),
phoneNumberFormatter = mockk<PhoneNumberFormatter>(relaxed = true),
resolveAvatarUri = mockk<ResolveAvatarUri>(relaxed = true),
)

private val mapper = ArchivedConversationListUiStateMapperImpl(
contentMapper = ConversationListContentUiStateMapperImpl(
itemUiMapper = itemUiMapper,
),
)

@Before
fun setUp() {
mockkStatic(OsUtil::class)
every { OsUtil.isSecondaryUser() } returns false
}

@After
fun tearDown() {
unmockkStatic(OsUtil::class)
}

@Test
fun map_openedConversation_marksOnlyThatItemOpened() {
val state = mapper.map(
snapshot = archivedSnapshot(),
selectedConversationIds = persistentListOf(),
openedConversationId = SECOND_ID,
isDebugEnabled = false,
)

assertEquals(listOf(SECOND_ID), openedIds(state))
}

@Test
fun map_noOpenedConversation_marksNoItemOpened() {
val state = mapper.map(
snapshot = archivedSnapshot(),
selectedConversationIds = persistentListOf(),
openedConversationId = null,
isDebugEnabled = false,
)

assertEquals(emptyList<ConversationId>(), openedIds(state))
}

@Test
fun map_openedConversationNotArchived_marksNoItemOpened() {
val state = mapper.map(
snapshot = archivedSnapshot(),
selectedConversationIds = persistentListOf(),
openedConversationId = ConversationId("inbox"),
isDebugEnabled = false,
)

assertEquals(emptyList<ConversationId>(), openedIds(state))
}

@Test
fun map_openedAndSelectedConversations_keepsBothFlagsAndCountsSelection() {
val state = mapper.map(
snapshot = archivedSnapshot(),
selectedConversationIds = persistentListOf(FIRST_ID),
openedConversationId = SECOND_ID,
isDebugEnabled = false,
)

val items = items(state)
assertEquals(listOf(FIRST_ID), items.filter { it.isSelected }.map { it.conversationId })
assertEquals(listOf(SECOND_ID), items.filter { it.isOpened }.map { it.conversationId })
assertEquals(1, state.selectedCount)
}

private fun archivedSnapshot(): ConversationListSnapshot {
return snapshotOf(
conversationItem(conversationId = FIRST_ID, isArchived = true),
conversationItem(conversationId = SECOND_ID, isArchived = true),
)
}

private fun openedIds(state: ArchivedConversationListUiState): List<ConversationId> {
return items(state)
.filter(ConversationListItemUiModel::isOpened)
.map(ConversationListItemUiModel::conversationId)
}

private fun items(state: ArchivedConversationListUiState): List<ConversationListItemUiModel> {
return (state.content as ConversationListContentUiState.Items).items
}

private companion object {
private val FIRST_ID = ConversationId("first")
private val SECOND_ID = ConversationId("second")
}
}
Loading
Loading