mirror of
https://github.com/bitwarden/android.git
synced 2025-02-17 12:30:00 +03:00
Add overflow items to sends listings (#665)
This commit is contained in:
parent
8c64d6b01b
commit
413677852b
9 changed files with 516 additions and 21 deletions
|
@ -10,7 +10,9 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderTextWithSupportLabel
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultEntryListItem
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenListItem
|
||||
import com.x8bit.bitwarden.ui.platform.components.SelectionItemData
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
/**
|
||||
* Content view for the [VaultItemListingScreen].
|
||||
|
@ -19,6 +21,7 @@ import com.x8bit.bitwarden.ui.vault.feature.vault.VaultEntryListItem
|
|||
fun VaultItemListingContent(
|
||||
state: VaultItemListingState.ViewState.Content,
|
||||
vaultItemClick: (id: String) -> Unit,
|
||||
onOverflowItemClick: (action: VaultItemListingsAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
|
@ -34,11 +37,20 @@ fun VaultItemListingContent(
|
|||
)
|
||||
}
|
||||
items(state.displayItemList) {
|
||||
VaultEntryListItem(
|
||||
BitwardenListItem(
|
||||
startIcon = it.iconData,
|
||||
label = it.title,
|
||||
supportingLabel = it.subtitle,
|
||||
onClick = { vaultItemClick(it.id) },
|
||||
selectionDataList = it
|
||||
.overflowOptions
|
||||
.map { option ->
|
||||
SelectionItemData(
|
||||
text = option.title(),
|
||||
onClick = { onOverflowItemClick(option.action) },
|
||||
)
|
||||
}
|
||||
.toPersistentList(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
|
|
|
@ -30,6 +30,8 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenSearchActionItem
|
|||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers.VaultItemListingHandlers
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
@ -43,6 +45,7 @@ fun VaultItemListingScreen(
|
|||
onNavigateToVaultAddItemScreen: () -> Unit,
|
||||
onNavigateToAddSendItem: () -> Unit,
|
||||
onNavigateToEditSendItem: (sendId: String) -> Unit,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
viewModel: VaultItemListingViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsState()
|
||||
|
@ -56,6 +59,10 @@ fun VaultItemListingScreen(
|
|||
onNavigateToVaultItem(event.id)
|
||||
}
|
||||
|
||||
is VaultItemListingEvent.ShowShareSheet -> {
|
||||
intentManager.shareText(event.content)
|
||||
}
|
||||
|
||||
is VaultItemListingEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.text(resources), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
@ -168,6 +175,7 @@ private fun VaultItemListingScaffold(
|
|||
VaultItemListingContent(
|
||||
state = state.viewState,
|
||||
vaultItemClick = vaultItemListingHandlers.itemClick,
|
||||
onOverflowItemClick = vaultItemListingHandlers.overflowItemClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,10 +4,12 @@ import android.os.Parcelable
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
|
@ -34,6 +36,7 @@ import javax.inject.Inject
|
|||
@Suppress("MagicNumber", "TooManyFunctions")
|
||||
class VaultItemListingViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val clipboardManager: BitwardenClipboardManager,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
|
@ -43,6 +46,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
.vaultItemListingType
|
||||
.toItemListingType(),
|
||||
viewState = VaultItemListingState.ViewState.Loading,
|
||||
baseWebSendUrl = environmentRepository.environment.environmentUrlData.baseWebSendUrl,
|
||||
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
|
||||
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
|
||||
dialogState = null,
|
||||
|
@ -70,9 +74,17 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
is VaultItemListingsAction.ItemClick -> handleItemClick(action)
|
||||
is VaultItemListingsAction.AddVaultItemClick -> handleAddVaultItemClick()
|
||||
is VaultItemListingsAction.RefreshClick -> handleRefreshClick()
|
||||
is VaultItemListingsAction.CopySendUrlClick -> handleCopySendUrlClick(action)
|
||||
is VaultItemListingsAction.DeleteSendClick -> handleDeleteSendClick(action)
|
||||
is VaultItemListingsAction.ShareSendUrlClick -> handleShareSendUrlClick(action)
|
||||
is VaultItemListingsAction.RemoveSendPasswordClick -> {
|
||||
handleRemoveSendPasswordClick(action)
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
|
||||
is VaultItemListingsAction.Internal.IconLoadingSettingReceive ->
|
||||
is VaultItemListingsAction.Internal.IconLoadingSettingReceive -> {
|
||||
handleIconsSettingReceived(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,6 +93,26 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
vaultRepository.sync()
|
||||
}
|
||||
|
||||
private fun handleCopySendUrlClick(action: VaultItemListingsAction.CopySendUrlClick) {
|
||||
clipboardManager.setText(text = action.sendUrl)
|
||||
}
|
||||
|
||||
private fun handleDeleteSendClick(action: VaultItemListingsAction.DeleteSendClick) {
|
||||
// TODO: Implement deletion (BIT-1411)
|
||||
sendEvent(VaultItemListingEvent.ShowToast("Not yet implemented".asText()))
|
||||
}
|
||||
|
||||
private fun handleShareSendUrlClick(action: VaultItemListingsAction.ShareSendUrlClick) {
|
||||
sendEvent(VaultItemListingEvent.ShowShareSheet(action.sendUrl))
|
||||
}
|
||||
|
||||
private fun handleRemoveSendPasswordClick(
|
||||
action: VaultItemListingsAction.RemoveSendPasswordClick,
|
||||
) {
|
||||
// TODO: Implement password removal (BIT-1411)
|
||||
sendEvent(VaultItemListingEvent.ShowToast("Not yet implemented".asText()))
|
||||
}
|
||||
|
||||
private fun handleAddVaultItemClick() {
|
||||
val event = when (state.itemListingType) {
|
||||
is VaultItemListingState.ItemListingType.Vault -> {
|
||||
|
@ -233,7 +265,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
.filter { sendView ->
|
||||
sendView.determineListingPredicate(listingType)
|
||||
}
|
||||
.toViewState()
|
||||
.toViewState(baseWebSendUrl = state.baseWebSendUrl)
|
||||
}
|
||||
},
|
||||
dialogState = currentState.dialogState.takeUnless { clearDialogState },
|
||||
|
@ -248,6 +280,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
data class VaultItemListingState(
|
||||
val itemListingType: ItemListingType,
|
||||
val viewState: ViewState,
|
||||
val baseWebSendUrl: String,
|
||||
val baseIconUrl: String,
|
||||
val isIconLoadingDisabled: Boolean,
|
||||
val dialogState: DialogState?,
|
||||
|
@ -309,13 +342,26 @@ data class VaultItemListingState(
|
|||
* @property title title of the item.
|
||||
* @property subtitle subtitle of the item (nullable).
|
||||
* @property iconData data for the icon to be displayed (nullable).
|
||||
* @property overflowOptions list of options for the item's overflow menu.
|
||||
*/
|
||||
data class DisplayItem(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val subtitle: String?,
|
||||
val iconData: IconData,
|
||||
)
|
||||
val overflowOptions: List<OverflowItem>,
|
||||
) {
|
||||
/**
|
||||
* Represents a single option to be displayed in an [DisplayItem]s overflow menu.
|
||||
*
|
||||
* @property title the display title of the option.
|
||||
* @property action the action to be sent back to the view model when the option is clicks.
|
||||
*/
|
||||
data class OverflowItem(
|
||||
val title: Text,
|
||||
val action: VaultItemListingsAction,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents different types of item listing.
|
||||
|
@ -470,6 +516,11 @@ sealed class VaultItemListingEvent {
|
|||
*/
|
||||
data object NavigateToVaultSearchScreen : VaultItemListingEvent()
|
||||
|
||||
/**
|
||||
* Show a share sheet with the given content.
|
||||
*/
|
||||
data class ShowShareSheet(val content: String) : VaultItemListingEvent()
|
||||
|
||||
/**
|
||||
* Show a toast with the given message.
|
||||
*
|
||||
|
@ -520,6 +571,26 @@ sealed class VaultItemListingsAction {
|
|||
*/
|
||||
data class ItemClick(val id: String) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* Click on the copy send URL overflow option.
|
||||
*/
|
||||
data class CopySendUrlClick(val sendUrl: String) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* Click on the share send URL overflow option.
|
||||
*/
|
||||
data class ShareSendUrlClick(val sendUrl: String) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* Click on the remove password send overflow option.
|
||||
*/
|
||||
data class RemoveSendPasswordClick(val sendId: String) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* Click on the delete send overflow option.
|
||||
*/
|
||||
data class DeleteSendClick(val sendId: String) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* Models actions that the [VaultItemListingViewModel] itself might send.
|
||||
*/
|
||||
|
|
|
@ -15,6 +15,7 @@ data class VaultItemListingHandlers(
|
|||
val refreshClick: () -> Unit,
|
||||
val syncClick: () -> Unit,
|
||||
val lockClick: () -> Unit,
|
||||
val overflowItemClick: (action: VaultItemListingsAction) -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
|
@ -36,6 +37,7 @@ data class VaultItemListingHandlers(
|
|||
refreshClick = { viewModel.trySendAction(VaultItemListingsAction.RefreshClick) },
|
||||
syncClick = { viewModel.trySendAction(VaultItemListingsAction.SyncClick) },
|
||||
lockClick = { viewModel.trySendAction(VaultItemListingsAction.LockClick) },
|
||||
overflowItemClick = { viewModel.trySendAction(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,11 @@ import com.bitwarden.core.FolderView
|
|||
import com.bitwarden.core.SendType
|
||||
import com.bitwarden.core.SendView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendUrl
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingsAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
|
||||
|
||||
/**
|
||||
|
@ -87,9 +90,13 @@ fun List<CipherView>.toViewState(
|
|||
/**
|
||||
* Transforms a list of [CipherView] into [VaultItemListingState.ViewState].
|
||||
*/
|
||||
fun List<SendView>.toViewState(): VaultItemListingState.ViewState =
|
||||
fun List<SendView>.toViewState(
|
||||
baseWebSendUrl: String,
|
||||
): VaultItemListingState.ViewState =
|
||||
if (isNotEmpty()) {
|
||||
VaultItemListingState.ViewState.Content(displayItemList = toDisplayItemList())
|
||||
VaultItemListingState.ViewState.Content(
|
||||
displayItemList = toDisplayItemList(baseWebSendUrl = baseWebSendUrl),
|
||||
)
|
||||
} else {
|
||||
VaultItemListingState.ViewState.NoItems
|
||||
}
|
||||
|
@ -134,8 +141,10 @@ private fun List<CipherView>.toDisplayItemList(
|
|||
)
|
||||
}
|
||||
|
||||
private fun List<SendView>.toDisplayItemList(): List<VaultItemListingState.DisplayItem> =
|
||||
this.map { it.toDisplayItem() }
|
||||
private fun List<SendView>.toDisplayItemList(
|
||||
baseWebSendUrl: String,
|
||||
): List<VaultItemListingState.DisplayItem> =
|
||||
this.map { it.toDisplayItem(baseWebSendUrl = baseWebSendUrl) }
|
||||
|
||||
private fun CipherView.toDisplayItem(
|
||||
baseIconUrl: String,
|
||||
|
@ -149,6 +158,7 @@ private fun CipherView.toDisplayItem(
|
|||
baseIconUrl = baseIconUrl,
|
||||
isIconLoadingDisabled = isIconLoadingDisabled,
|
||||
),
|
||||
overflowOptions = emptyList(),
|
||||
)
|
||||
|
||||
private fun CipherView.toIconData(
|
||||
|
@ -169,7 +179,9 @@ private fun CipherView.toIconData(
|
|||
}
|
||||
}
|
||||
|
||||
private fun SendView.toDisplayItem(): VaultItemListingState.DisplayItem =
|
||||
private fun SendView.toDisplayItem(
|
||||
baseWebSendUrl: String,
|
||||
): VaultItemListingState.DisplayItem =
|
||||
VaultItemListingState.DisplayItem(
|
||||
id = id.orEmpty(),
|
||||
title = name,
|
||||
|
@ -180,6 +192,33 @@ private fun SendView.toDisplayItem(): VaultItemListingState.DisplayItem =
|
|||
SendType.FILE -> R.drawable.ic_send_file
|
||||
},
|
||||
),
|
||||
overflowOptions = listOfNotNull(
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.edit.asText(),
|
||||
action = VaultItemListingsAction.ItemClick(id = id.orEmpty()),
|
||||
),
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.copy_link.asText(),
|
||||
action = VaultItemListingsAction.CopySendUrlClick(
|
||||
sendUrl = toSendUrl(baseWebSendUrl),
|
||||
),
|
||||
),
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.share_link.asText(),
|
||||
action = VaultItemListingsAction.ShareSendUrlClick(
|
||||
sendUrl = toSendUrl(baseWebSendUrl),
|
||||
),
|
||||
),
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.remove_password.asText(),
|
||||
action = VaultItemListingsAction.RemoveSendPasswordClick(sendId = id.orEmpty()),
|
||||
)
|
||||
.takeIf { hasPassword },
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.delete.asText(),
|
||||
action = VaultItemListingsAction.DeleteSendClick(sendId = id.orEmpty()),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
|
|
|
@ -12,6 +12,7 @@ import androidx.compose.ui.test.isDialog
|
|||
import androidx.compose.ui.test.isDisplayed
|
||||
import androidx.compose.ui.test.isPopup
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onChildren
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
|
@ -19,13 +20,18 @@ import androidx.compose.ui.test.performScrollToNode
|
|||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||
import com.x8bit.bitwarden.ui.util.isProgressBar
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
@ -42,6 +48,9 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
private var onNavigateToEditSendItemId: String? = null
|
||||
private var onNavigateToVaultItemId: String? = null
|
||||
|
||||
private val intentManager: IntentManager = mockk {
|
||||
every { shareText(any()) } just runs
|
||||
}
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<VaultItemListingEvent>()
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val viewModel = mockk<VaultItemListingViewModel>(relaxed = true) {
|
||||
|
@ -54,6 +63,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
composeTestRule.setContent {
|
||||
VaultItemListingScreen(
|
||||
viewModel = viewModel,
|
||||
intentManager = intentManager,
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
onNavigateToVaultItem = { onNavigateToVaultItemId = it },
|
||||
onNavigateToVaultAddItemScreen = { onNavigateToVaultAddItemScreenCalled = true },
|
||||
|
@ -113,6 +123,15 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
verify { viewModel.trySendAction(VaultItemListingsAction.RefreshClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ShowShareSheet event should call shareText in intentManager`() {
|
||||
val content = "content"
|
||||
mutableEventFlow.tryEmit(VaultItemListingEvent.ShowShareSheet(content = content))
|
||||
verify {
|
||||
intentManager.shareText(content)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToAdd VaultItem event should call NavigateToVaultAddItemScreen`() {
|
||||
mutableEventFlow.tryEmit(VaultItemListingEvent.NavigateToAddVaultItem)
|
||||
|
@ -476,6 +495,114 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on send item overflow click should display dialog`() {
|
||||
val number = 1
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(createDisplayItem(number = number)),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Options")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNode(isDialog())
|
||||
.onChildren()
|
||||
.filterToOne(hasText("mockTitle-$number"))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on send item overflow option click should emit the appropriate action`() {
|
||||
val number = 1
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(createDisplayItem(number = number)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Options")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Edit")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.ItemClick(id = "mockId-$number"),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Options")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Copy link")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.CopySendUrlClick(sendUrl = "www.test.com"),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Options")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Share link")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.ShareSendUrlClick(sendUrl = "www.test.com"),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Options")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Remove password")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.RemoveSendPasswordClick(sendId = "mockId-$number"),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Options")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Delete")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.DeleteSendClick(sendId = "mockId-$number"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loading dialog should be displayed according to state`() {
|
||||
val loadingMessage = "syncing"
|
||||
|
@ -500,6 +627,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
private val DEFAULT_STATE = VaultItemListingState(
|
||||
itemListingType = VaultItemListingState.ItemListingType.Vault.Login,
|
||||
viewState = VaultItemListingState.ViewState.Loading,
|
||||
baseWebSendUrl = Environment.Us.environmentUrlData.baseWebSendUrl,
|
||||
isIconLoadingDisabled = false,
|
||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||
dialogState = null,
|
||||
|
@ -511,4 +639,26 @@ private fun createDisplayItem(number: Int): VaultItemListingState.DisplayItem =
|
|||
title = "mockTitle-$number",
|
||||
subtitle = "mockSubtitle-$number",
|
||||
iconData = IconData.Local(R.drawable.ic_card_item),
|
||||
overflowOptions = listOf(
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.edit.asText(),
|
||||
action = VaultItemListingsAction.ItemClick(id = "mockId-$number"),
|
||||
),
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.copy_link.asText(),
|
||||
action = VaultItemListingsAction.CopySendUrlClick(sendUrl = "www.test.com"),
|
||||
),
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.share_link.asText(),
|
||||
action = VaultItemListingsAction.ShareSendUrlClick(sendUrl = "www.test.com"),
|
||||
),
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.remove_password.asText(),
|
||||
action = VaultItemListingsAction.RemoveSendPasswordClick(sendId = "mockId-$number"),
|
||||
),
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.delete.asText(),
|
||||
action = VaultItemListingsAction.DeleteSendClick(sendId = "mockId-$number"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -4,11 +4,13 @@ import android.net.Uri
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
|
||||
|
@ -18,7 +20,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
|||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.createMockItemListingDisplayItem
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.createMockDisplayItemForCipher
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
|
@ -36,6 +38,8 @@ import org.junit.jupiter.api.Test
|
|||
|
||||
class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val clipboardManager: BitwardenClipboardManager = mockk()
|
||||
|
||||
private val mutableVaultDataStateFlow =
|
||||
MutableStateFlow<DataState<VaultData>>(DataState.Loading)
|
||||
private val vaultRepository: VaultRepository = mockk {
|
||||
|
@ -48,7 +52,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
private val mutableIsIconLoadingDisabledFlow = MutableStateFlow(false)
|
||||
private val settingsRepository: SettingsRepository = mockk() {
|
||||
private val settingsRepository: SettingsRepository = mockk {
|
||||
every { isIconLoadingDisabled } returns false
|
||||
every { isIconLoadingDisabledFlow } returns mutableIsIconLoadingDisabledFlow
|
||||
}
|
||||
|
@ -163,6 +167,59 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
verify { vaultRepository.sync() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CopySendUrlClick should call setText on clipboardManager`() {
|
||||
val sendUrl = "www.test.com"
|
||||
every { clipboardManager.setText(sendUrl) } just runs
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.actionChannel.trySend(VaultItemListingsAction.CopySendUrlClick(sendUrl = sendUrl))
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(text = sendUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DeleteSendClick should emit ShowToast`() = runTest {
|
||||
val sendId = "sendId"
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(
|
||||
VaultItemListingsAction.DeleteSendClick(sendId = sendId),
|
||||
)
|
||||
assertEquals(
|
||||
VaultItemListingEvent.ShowToast("Not yet implemented".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ShareSendUrlClick should emit ShowShareSheet`() = runTest {
|
||||
val sendUrl = "www.test.com"
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(
|
||||
VaultItemListingsAction.ShareSendUrlClick(sendUrl = sendUrl),
|
||||
)
|
||||
assertEquals(VaultItemListingEvent.ShowShareSheet(sendUrl), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RemoveSendPasswordClick should emit ShowToast`() = runTest {
|
||||
val sendId = "sendId"
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(
|
||||
VaultItemListingsAction.RemoveSendPasswordClick(sendId = sendId),
|
||||
)
|
||||
assertEquals(
|
||||
VaultItemListingEvent.ShowToast("Not yet implemented".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultDataStateFlow Loaded with items should update ViewState to Content`() =
|
||||
runTest {
|
||||
|
@ -190,7 +247,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
createVaultItemListingState(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(
|
||||
createMockItemListingDisplayItem(number = 1),
|
||||
createMockDisplayItemForCipher(number = 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -273,7 +330,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
createVaultItemListingState(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(
|
||||
createMockItemListingDisplayItem(number = 1),
|
||||
createMockDisplayItemForCipher(number = 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -365,7 +422,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
createVaultItemListingState(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(
|
||||
createMockItemListingDisplayItem(number = 1),
|
||||
createMockDisplayItemForCipher(number = 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -464,7 +521,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
createVaultItemListingState(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(
|
||||
createMockItemListingDisplayItem(number = 1),
|
||||
createMockDisplayItemForCipher(number = 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -577,6 +634,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
): VaultItemListingViewModel =
|
||||
VaultItemListingViewModel(
|
||||
savedStateHandle = savedStateHandle,
|
||||
clipboardManager = clipboardManager,
|
||||
vaultRepository = vaultRepository,
|
||||
environmentRepository = environmentRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
|
@ -590,6 +648,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
VaultItemListingState(
|
||||
itemListingType = itemListingType,
|
||||
viewState = viewState,
|
||||
baseWebSendUrl = Environment.Us.environmentUrlData.baseWebSendUrl,
|
||||
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
|
||||
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
|
||||
dialogState = null,
|
||||
|
|
|
@ -2,11 +2,14 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
|
|||
|
||||
import android.net.Uri
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.bitwarden.core.SendType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
@ -249,6 +252,44 @@ class VaultItemListingDataExtensionsTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `determineListingPredicate should return the correct predicate for File sendView`() {
|
||||
val sendView = createMockSendView(number = 1, type = SendType.FILE)
|
||||
|
||||
mapOf(
|
||||
VaultItemListingState.ItemListingType.Send.SendFile to true,
|
||||
VaultItemListingState.ItemListingType.Send.SendText to false,
|
||||
)
|
||||
.forEach { (type, expected) ->
|
||||
val result = sendView.determineListingPredicate(
|
||||
itemListingType = type,
|
||||
)
|
||||
assertEquals(
|
||||
expected,
|
||||
result,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `determineListingPredicate should return the correct predicate for Text sendView`() {
|
||||
val sendView = createMockSendView(number = 1, type = SendType.TEXT)
|
||||
|
||||
mapOf(
|
||||
VaultItemListingState.ItemListingType.Send.SendFile to false,
|
||||
VaultItemListingState.ItemListingType.Send.SendText to true,
|
||||
)
|
||||
.forEach { (type, expected) ->
|
||||
val result = sendView.determineListingPredicate(
|
||||
itemListingType = type,
|
||||
)
|
||||
assertEquals(
|
||||
expected,
|
||||
result,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should transform a list of CipherViews into a ViewState`() {
|
||||
mockkStatic(Uri::class)
|
||||
|
@ -287,19 +328,19 @@ class VaultItemListingDataExtensionsTest {
|
|||
assertEquals(
|
||||
VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(
|
||||
createMockItemListingDisplayItem(
|
||||
createMockDisplayItemForCipher(
|
||||
number = 1,
|
||||
cipherType = CipherType.LOGIN,
|
||||
),
|
||||
createMockItemListingDisplayItem(
|
||||
createMockDisplayItemForCipher(
|
||||
number = 2,
|
||||
cipherType = CipherType.CARD,
|
||||
),
|
||||
createMockItemListingDisplayItem(
|
||||
createMockDisplayItemForCipher(
|
||||
number = 3,
|
||||
cipherType = CipherType.SECURE_NOTE,
|
||||
),
|
||||
createMockItemListingDisplayItem(
|
||||
createMockDisplayItemForCipher(
|
||||
number = 4,
|
||||
cipherType = CipherType.IDENTITY,
|
||||
),
|
||||
|
@ -311,6 +352,28 @@ class VaultItemListingDataExtensionsTest {
|
|||
unmockkStatic(Uri::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should transform a list of SendViews into a ViewState`() {
|
||||
val sendViewList = listOf(
|
||||
createMockSendView(number = 1, type = SendType.FILE),
|
||||
createMockSendView(number = 2, type = SendType.TEXT),
|
||||
)
|
||||
|
||||
val result = sendViewList.toViewState(
|
||||
baseWebSendUrl = Environment.Us.environmentUrlData.baseWebSendUrl,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(
|
||||
createMockDisplayItemForSend(number = 1, sendType = SendType.FILE),
|
||||
createMockDisplayItemForSend(number = 2, sendType = SendType.TEXT),
|
||||
),
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateWithAdditionalDataIfNecessary should update a folder itemListingType`() {
|
||||
val folderViewList = listOf(
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
|
||||
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.bitwarden.core.SendType
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingsAction
|
||||
|
||||
/**
|
||||
* Create a mock [VaultItemListingState.DisplayItem] with a given [number].
|
||||
*/
|
||||
fun createMockItemListingDisplayItem(
|
||||
fun createMockDisplayItemForCipher(
|
||||
number: Int,
|
||||
cipherType: CipherType = CipherType.LOGIN,
|
||||
): VaultItemListingState.DisplayItem =
|
||||
|
@ -22,6 +25,7 @@ fun createMockItemListingDisplayItem(
|
|||
"https://vault.bitwarden.com/icons/www.mockuri.com/icon.png",
|
||||
fallbackIconRes = R.drawable.ic_login_item,
|
||||
),
|
||||
overflowOptions = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -31,6 +35,7 @@ fun createMockItemListingDisplayItem(
|
|||
title = "mockName-$number",
|
||||
subtitle = null,
|
||||
iconData = IconData.Local(R.drawable.ic_secure_note_item),
|
||||
overflowOptions = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -40,6 +45,7 @@ fun createMockItemListingDisplayItem(
|
|||
title = "mockName-$number",
|
||||
subtitle = "er-$number",
|
||||
iconData = IconData.Local(R.drawable.ic_card_item),
|
||||
overflowOptions = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -49,6 +55,91 @@ fun createMockItemListingDisplayItem(
|
|||
title = "mockName-$number",
|
||||
subtitle = "mockFirstName-${number}mockLastName-$number",
|
||||
iconData = IconData.Local(R.drawable.ic_identity_item),
|
||||
overflowOptions = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock [VaultItemListingState.DisplayItem] with a given [number].
|
||||
*/
|
||||
@Suppress("MaxLineLength")
|
||||
fun createMockDisplayItemForSend(
|
||||
number: Int,
|
||||
sendType: SendType = SendType.FILE,
|
||||
): VaultItemListingState.DisplayItem =
|
||||
when (sendType) {
|
||||
SendType.FILE -> {
|
||||
VaultItemListingState.DisplayItem(
|
||||
id = "mockId-$number",
|
||||
title = "mockName-$number",
|
||||
subtitle = "2023-10-27T12:00:00Z",
|
||||
iconData = IconData.Local(R.drawable.ic_send_file),
|
||||
overflowOptions = listOfNotNull(
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.edit.asText(),
|
||||
action = VaultItemListingsAction.ItemClick(id = "mockId-$number"),
|
||||
),
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.copy_link.asText(),
|
||||
action = VaultItemListingsAction.CopySendUrlClick(
|
||||
sendUrl = "https://vault.bitwarden.com/#/send/mockAccessId-$number/mockKey-$number",
|
||||
),
|
||||
),
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.share_link.asText(),
|
||||
action = VaultItemListingsAction.ShareSendUrlClick(
|
||||
sendUrl = "https://vault.bitwarden.com/#/send/mockAccessId-$number/mockKey-$number",
|
||||
),
|
||||
),
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.remove_password.asText(),
|
||||
action = VaultItemListingsAction.RemoveSendPasswordClick(
|
||||
sendId = "mockId-$number",
|
||||
),
|
||||
),
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.delete.asText(),
|
||||
action = VaultItemListingsAction.DeleteSendClick(sendId = "mockId-$number"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
SendType.TEXT -> {
|
||||
VaultItemListingState.DisplayItem(
|
||||
id = "mockId-$number",
|
||||
title = "mockName-$number",
|
||||
subtitle = "2023-10-27T12:00:00Z",
|
||||
iconData = IconData.Local(R.drawable.ic_send_text),
|
||||
overflowOptions = listOfNotNull(
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.edit.asText(),
|
||||
action = VaultItemListingsAction.ItemClick(id = "mockId-$number"),
|
||||
),
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.copy_link.asText(),
|
||||
action = VaultItemListingsAction.CopySendUrlClick(
|
||||
sendUrl = "https://vault.bitwarden.com/#/send/mockAccessId-$number/mockKey-$number",
|
||||
),
|
||||
),
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.share_link.asText(),
|
||||
action = VaultItemListingsAction.ShareSendUrlClick(
|
||||
sendUrl = "https://vault.bitwarden.com/#/send/mockAccessId-$number/mockKey-$number",
|
||||
),
|
||||
),
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.remove_password.asText(),
|
||||
action = VaultItemListingsAction.RemoveSendPasswordClick(
|
||||
sendId = "mockId-$number",
|
||||
),
|
||||
),
|
||||
VaultItemListingState.DisplayItem.OverflowItem(
|
||||
title = R.string.delete.asText(),
|
||||
action = VaultItemListingsAction.DeleteSendClick(sendId = "mockId-$number"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue