From cf930438c2b6ce669cb26e77df49e04fb42efad2 Mon Sep 17 00:00:00 2001 From: David Perez Date: Fri, 19 Jan 2024 13:03:38 -0600 Subject: [PATCH] Add sealed class to model reusable overflow actions (#682) --- .../feature/send/util/SendViewExtensions.kt | 26 +++ .../itemlisting/VaultItemListingContent.kt | 45 ++++- .../itemlisting/VaultItemListingScreen.kt | 15 -- .../itemlisting/VaultItemListingViewModel.kt | 118 +++++-------- .../handlers/VaultItemListingHandlers.kt | 7 +- .../model/ListingItemOverflowAction.kt | 63 +++++++ .../util/VaultItemListingDataExtensions.kt | 32 +--- .../send/util/SendViewExtensionsTest.kt | 57 ++++++ .../itemlisting/VaultItemListingScreenTest.kt | 94 +++++----- .../VaultItemListingViewModelTest.kt | 163 ++++++++++-------- .../util/VaultItemListingDataUtil.kt | 68 ++------ 11 files changed, 400 insertions(+), 288 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/util/SendViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/util/SendViewExtensions.kt index 5f25afeaa..ff6e4b846 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/util/SendViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/util/SendViewExtensions.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.tools.feature.send.util import com.bitwarden.core.SendView import com.x8bit.bitwarden.ui.platform.components.model.IconRes import com.x8bit.bitwarden.ui.tools.feature.send.model.SendStatusIcon +import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction import java.time.Clock /** @@ -20,6 +21,31 @@ fun SendView.toLabelIcons(clock: Clock = Clock.systemDefaultZone()): List = + this + .id + ?.let { sendId -> + listOfNotNull( + ListingItemOverflowAction.SendAction.EditClick(sendId = sendId), + ListingItemOverflowAction.SendAction.CopyUrlClick( + sendUrl = toSendUrl(baseWebSendUrl = baseWebSendUrl), + ), + ListingItemOverflowAction.SendAction.ShareUrlClick( + sendUrl = toSendUrl(baseWebSendUrl = baseWebSendUrl), + ), + ListingItemOverflowAction.SendAction.RemovePasswordClick(sendId = sendId).takeIf { + hasPassword + }, + ListingItemOverflowAction.SendAction.DeleteClick(sendId = sendId), + ) + } + .orEmpty() + /** * Creates a sharable url from a [SendView]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt index e4104ae65..de130a75e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt @@ -5,24 +5,31 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier 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.platform.components.BitwardenListItem +import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.SelectionItemData import com.x8bit.bitwarden.ui.platform.components.model.toIconResources +import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction import kotlinx.collections.immutable.toPersistentList /** * Content view for the [VaultItemListingScreen]. */ +@Suppress("LongMethod") @Composable fun VaultItemListingContent( state: VaultItemListingState.ViewState.Content, vaultItemClick: (id: String) -> Unit, - onOverflowItemClick: (action: VaultItemListingsAction) -> Unit, + onOverflowItemClick: (action: ListingItemOverflowAction) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn( @@ -38,6 +45,9 @@ fun VaultItemListingContent( ) } items(state.displayItemList) { + var showConfirmationDialog: ListingItemOverflowAction? by rememberSaveable { + mutableStateOf(null) + } BitwardenListItem( startIcon = it.iconData, label = it.title, @@ -52,7 +62,15 @@ fun VaultItemListingContent( .map { option -> SelectionItemData( text = option.title(), - onClick = { onOverflowItemClick(option.action) }, + onClick = { + when (option) { + is ListingItemOverflowAction.SendAction.DeleteClick -> { + showConfirmationDialog = option + } + + else -> onOverflowItemClick(option) + } + }, ) } .toPersistentList(), @@ -65,6 +83,29 @@ fun VaultItemListingContent( end = 12.dp, ), ) + when (val option = showConfirmationDialog) { + is ListingItemOverflowAction.SendAction.DeleteClick -> { + BitwardenTwoButtonDialog( + title = stringResource(id = R.string.delete), + message = stringResource(id = R.string.are_you_sure_delete_send), + confirmButtonText = stringResource(id = R.string.yes), + dismissButtonText = stringResource(id = R.string.cancel), + onConfirmClick = { + showConfirmationDialog = null + onOverflowItemClick(option) + }, + onDismissClick = { showConfirmationDialog = null }, + onDismissRequest = { showConfirmationDialog = null }, + ) + } + + is ListingItemOverflowAction.SendAction.CopyUrlClick, + is ListingItemOverflowAction.SendAction.EditClick, + is ListingItemOverflowAction.SendAction.RemovePasswordClick, + is ListingItemOverflowAction.SendAction.ShareUrlClick, + null, + -> Unit + } } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt index 951c47c43..c88439566 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt @@ -30,7 +30,6 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.BitwardenSearchActionItem import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar -import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog 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 @@ -93,9 +92,6 @@ fun VaultItemListingScreen( VaultItemListingDialogs( dialogState = state.dialogState, - onDeleteSendConfirm = remember(viewModel) { - { viewModel.trySendAction(VaultItemListingsAction.DeleteSendConfirmClick(it)) } - }, onDismissRequest = remember(viewModel) { { viewModel.trySendAction(VaultItemListingsAction.DismissDialogClick) } }, @@ -112,20 +108,9 @@ fun VaultItemListingScreen( @Composable private fun VaultItemListingDialogs( dialogState: VaultItemListingState.DialogState?, - onDeleteSendConfirm: (sendId: String) -> Unit, onDismissRequest: () -> Unit, ) { when (dialogState) { - is VaultItemListingState.DialogState.DeleteSendConfirmation -> BitwardenTwoButtonDialog( - title = stringResource(id = R.string.delete), - message = stringResource(id = R.string.are_you_sure_delete_send), - confirmButtonText = stringResource(id = R.string.yes), - dismissButtonText = stringResource(id = R.string.cancel), - onConfirmClick = { onDeleteSendConfirm(dialogState.sendId) }, - onDismissClick = onDismissRequest, - onDismissRequest = onDismissRequest, - ) - is VaultItemListingState.DialogState.Error -> BitwardenBasicDialog( visibilityState = BasicDialogState.Shown( title = dialogState.title, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index eabd5f9df..f71c759a7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -20,6 +20,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.concat import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.platform.components.model.IconRes +import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.determineListingPredicate import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toItemListingType import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toViewState @@ -81,20 +82,10 @@ class VaultItemListingViewModel @Inject constructor( is VaultItemListingsAction.LockClick -> handleLockClick() is VaultItemListingsAction.SyncClick -> handleSyncClick() is VaultItemListingsAction.SearchIconClick -> handleSearchIconClick() + is VaultItemListingsAction.OverflowOptionClick -> handleOverflowOptionClick(action) 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.DeleteSendConfirmClick -> { - handleDeleteSendConfirmClick(action) - } - - is VaultItemListingsAction.ShareSendUrlClick -> handleShareSendUrlClick(action) - is VaultItemListingsAction.RemoveSendPasswordClick -> { - handleRemoveSendPasswordClick(action) - } - is VaultItemListingsAction.Internal -> handleInternalAction(action) } } @@ -104,23 +95,11 @@ class VaultItemListingViewModel @Inject constructor( vaultRepository.sync() } - private fun handleCopySendUrlClick(action: VaultItemListingsAction.CopySendUrlClick) { + private fun handleCopySendUrlClick(action: ListingItemOverflowAction.SendAction.CopyUrlClick) { clipboardManager.setText(text = action.sendUrl) } - private fun handleDeleteSendClick(action: VaultItemListingsAction.DeleteSendClick) { - mutableStateFlow.update { - it.copy( - dialogState = VaultItemListingState.DialogState.DeleteSendConfirmation( - sendId = action.sendId, - ), - ) - } - } - - private fun handleDeleteSendConfirmClick( - action: VaultItemListingsAction.DeleteSendConfirmClick, - ) { + private fun handleDeleteSendClick(action: ListingItemOverflowAction.SendAction.DeleteClick) { mutableStateFlow.update { it.copy( dialogState = VaultItemListingState.DialogState.Loading( @@ -134,12 +113,14 @@ class VaultItemListingViewModel @Inject constructor( } } - private fun handleShareSendUrlClick(action: VaultItemListingsAction.ShareSendUrlClick) { + private fun handleShareSendUrlClick( + action: ListingItemOverflowAction.SendAction.ShareUrlClick, + ) { sendEvent(VaultItemListingEvent.ShowShareSheet(action.sendUrl)) } private fun handleRemoveSendPasswordClick( - action: VaultItemListingsAction.RemoveSendPasswordClick, + action: ListingItemOverflowAction.SendAction.RemovePasswordClick, ) { mutableStateFlow.update { it.copy( @@ -167,6 +148,10 @@ class VaultItemListingViewModel @Inject constructor( sendEvent(event) } + private fun handleEditSendClick(action: ListingItemOverflowAction.SendAction.EditClick) { + sendEvent(VaultItemListingEvent.NavigateToSendItem(id = action.sendId)) + } + private fun handleItemClick(action: VaultItemListingsAction.ItemClick) { val event = when (state.itemListingType) { is VaultItemListingState.ItemListingType.Vault -> { @@ -211,6 +196,30 @@ class VaultItemListingViewModel @Inject constructor( ) } + private fun handleOverflowOptionClick(action: VaultItemListingsAction.OverflowOptionClick) { + when (val overflowAction = action.action) { + is ListingItemOverflowAction.SendAction.CopyUrlClick -> { + handleCopySendUrlClick(overflowAction) + } + + is ListingItemOverflowAction.SendAction.DeleteClick -> { + handleDeleteSendClick(overflowAction) + } + + is ListingItemOverflowAction.SendAction.EditClick -> { + handleEditSendClick(overflowAction) + } + + is ListingItemOverflowAction.SendAction.RemovePasswordClick -> { + handleRemoveSendPasswordClick(overflowAction) + } + + is ListingItemOverflowAction.SendAction.ShareUrlClick -> { + handleShareSendUrlClick(overflowAction) + } + } + } + private fun handleInternalAction(action: VaultItemListingsAction.Internal) { when (action) { is VaultItemListingsAction.Internal.DeleteSendResultReceive -> { @@ -411,14 +420,6 @@ data class VaultItemListingState( */ sealed class DialogState : Parcelable { - /** - * Represents a dismissible dialog with the given error [message]. - */ - @Parcelize - data class DeleteSendConfirmation( - val sendId: String, - ) : DialogState() - /** * Represents a dismissible dialog with the given error [message]. */ @@ -487,19 +488,8 @@ data class VaultItemListingState( val subtitle: String?, val iconData: IconData, val extraIconList: List, - val overflowOptions: List, - ) { - /** - * 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, - ) - } + val overflowOptions: List, + ) /** * Represents different types of item listing. @@ -707,6 +697,13 @@ sealed class VaultItemListingsAction { */ data object AddVaultItemClick : VaultItemListingsAction() + /** + * Click on overflow option. + */ + data class OverflowOptionClick( + val action: ListingItemOverflowAction, + ) : VaultItemListingsAction() + /** * Click on an item. * @@ -714,31 +711,6 @@ 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() - - /** - * Click on the delete send confirmation button. - */ - data class DeleteSendConfirmClick(val sendId: String) : VaultItemListingsAction() - /** * Models actions that the [VaultItemListingViewModel] itself might send. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/handlers/VaultItemListingHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/handlers/VaultItemListingHandlers.kt index 57679142b..e75667315 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/handlers/VaultItemListingHandlers.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/handlers/VaultItemListingHandlers.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingViewModel import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingsAction +import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction /** * A collection of handler functions for managing actions within the context of viewing a list of @@ -15,7 +16,7 @@ data class VaultItemListingHandlers( val refreshClick: () -> Unit, val syncClick: () -> Unit, val lockClick: () -> Unit, - val overflowItemClick: (action: VaultItemListingsAction) -> Unit, + val overflowItemClick: (action: ListingItemOverflowAction) -> Unit, ) { companion object { /** @@ -37,7 +38,9 @@ data class VaultItemListingHandlers( refreshClick = { viewModel.trySendAction(VaultItemListingsAction.RefreshClick) }, syncClick = { viewModel.trySendAction(VaultItemListingsAction.SyncClick) }, lockClick = { viewModel.trySendAction(VaultItemListingsAction.LockClick) }, - overflowItemClick = { viewModel.trySendAction(it) }, + overflowItemClick = { + viewModel.trySendAction(VaultItemListingsAction.OverflowOptionClick(it)) + }, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt new file mode 100644 index 000000000..5036cc867 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt @@ -0,0 +1,63 @@ +package com.x8bit.bitwarden.ui.vault.feature.itemlisting.model + +import android.os.Parcelable +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.Text +import com.x8bit.bitwarden.ui.platform.base.util.asText +import kotlinx.parcelize.Parcelize + +/** + * Represents the actions for an individual item's overflow menu. + */ +sealed class ListingItemOverflowAction : Parcelable { + + /** + * The display title of the option. + */ + abstract val title: Text + + /** + * Represents the send actions. + */ + sealed class SendAction : ListingItemOverflowAction() { + /** + * Click on the edit send overflow option. + */ + @Parcelize + data class EditClick(val sendId: String) : SendAction() { + override val title: Text get() = R.string.edit.asText() + } + + /** + * Click on the copy send URL overflow option. + */ + @Parcelize + data class CopyUrlClick(val sendUrl: String) : SendAction() { + override val title: Text get() = R.string.copy_link.asText() + } + + /** + * Click on the share send URL overflow option. + */ + @Parcelize + data class ShareUrlClick(val sendUrl: String) : SendAction() { + override val title: Text get() = R.string.share_link.asText() + } + + /** + * Click on the remove password send overflow option. + */ + @Parcelize + data class RemovePasswordClick(val sendId: String) : SendAction() { + override val title: Text get() = R.string.remove_password.asText() + } + + /** + * Click on the delete send overflow option. + */ + @Parcelize + data class DeleteClick(val sendId: String) : SendAction() { + override val title: Text get() = R.string.delete.asText() + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt index 9afc711a5..6145324d5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt @@ -8,12 +8,10 @@ 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.toLabelIcons -import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendUrl +import com.x8bit.bitwarden.ui.tools.feature.send.util.toOverflowActions 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 import java.time.Clock @@ -207,33 +205,7 @@ private fun SendView.toDisplayItem( }, ), extraIconList = toLabelIcons(clock = clock), - 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()), - ), - ), + overflowOptions = toOverflowActions(baseWebSendUrl = baseWebSendUrl), ) @Suppress("MagicNumber") diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/util/SendViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/util/SendViewExtensionsTest.kt index dac4cd93c..92879e3a1 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/util/SendViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/util/SendViewExtensionsTest.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.tools.feature.send.util import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView import com.x8bit.bitwarden.ui.platform.components.model.IconRes import com.x8bit.bitwarden.ui.tools.feature.send.model.SendStatusIcon +import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import java.time.Clock @@ -60,6 +61,49 @@ class SendViewExtensionsTest { assertEquals(emptyList(), result) } + @Suppress("MaxLineLength") + @Test + fun `toOverflowActions should return overflow options with remove password when there is a password`() { + val baseWebSendUrl = "www.test.com" + val sendView = createMockSendView(number = 1).copy( + // Make sure there is a password for the remove password action + hasPassword = true, + ) + + val result = sendView.toOverflowActions(baseWebSendUrl = baseWebSendUrl) + + assertEquals(ALL_SEND_OVERFLOW_OPTIONS, result) + } + + @Suppress("MaxLineLength") + @Test + fun `toOverflowActions should return overflow options without Remove Password when there is no password`() { + val baseWebSendUrl = "www.test.com" + val sendView = createMockSendView(number = 1).copy( + // Make sure there is no password for the remove password action + hasPassword = false, + ) + + val result = sendView.toOverflowActions(baseWebSendUrl = baseWebSendUrl) + + assertEquals( + ALL_SEND_OVERFLOW_OPTIONS.filter { + it !is ListingItemOverflowAction.SendAction.RemovePasswordClick + }, + result, + ) + } + + @Test + fun `toOverflowActions should return no overflow options when the id is null`() { + val baseWebSendUrl = "www.test.com" + val sendView = createMockSendView(number = 1).copy(id = null) + + val result = sendView.toOverflowActions(baseWebSendUrl = baseWebSendUrl) + + assertEquals(emptyList(), result) + } + @Test fun `toSendUrl should create an appropriate url`() { val sendView = createMockSendView(number = 1) @@ -92,3 +136,16 @@ private val ALL_SEND_STATUS_ICONS: List = listOf( contentDescription = SendStatusIcon.PENDING_DELETE.contentDescription, ), ) + +private val ALL_SEND_OVERFLOW_OPTIONS: List = + listOf( + ListingItemOverflowAction.SendAction.EditClick(sendId = "mockId-1"), + ListingItemOverflowAction.SendAction.CopyUrlClick( + sendUrl = "www.test.commockAccessId-1/mockKey-1", + ), + ListingItemOverflowAction.SendAction.ShareUrlClick( + sendUrl = "www.test.commockAccessId-1/mockKey-1", + ), + ListingItemOverflowAction.SendAction.RemovePasswordClick(sendId = "mockId-1"), + ListingItemOverflowAction.SendAction.DeleteClick(sendId = "mockId-1"), + ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt index e9cc106f3..f3a32f06f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt @@ -29,6 +29,7 @@ import com.x8bit.bitwarden.ui.platform.components.model.IconRes import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.util.assertNoDialogExists import com.x8bit.bitwarden.ui.util.isProgressBar +import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType import io.mockk.every import io.mockk.just @@ -544,7 +545,11 @@ class VaultItemListingScreenTest : BaseComposeTest() { .performClick() verify(exactly = 1) { viewModel.trySendAction( - VaultItemListingsAction.ItemClick(id = "mockId-$number"), + VaultItemListingsAction.OverflowOptionClick( + action = ListingItemOverflowAction.SendAction.EditClick( + sendId = "mockId-$number", + ), + ), ) } @@ -558,7 +563,11 @@ class VaultItemListingScreenTest : BaseComposeTest() { .performClick() verify(exactly = 1) { viewModel.trySendAction( - VaultItemListingsAction.CopySendUrlClick(sendUrl = "www.test.com"), + VaultItemListingsAction.OverflowOptionClick( + action = ListingItemOverflowAction.SendAction.CopyUrlClick( + sendUrl = "www.test.com", + ), + ), ) } @@ -572,7 +581,11 @@ class VaultItemListingScreenTest : BaseComposeTest() { .performClick() verify(exactly = 1) { viewModel.trySendAction( - VaultItemListingsAction.ShareSendUrlClick(sendUrl = "www.test.com"), + VaultItemListingsAction.OverflowOptionClick( + action = ListingItemOverflowAction.SendAction.ShareUrlClick( + sendUrl = "www.test.com", + ), + ), ) } @@ -586,9 +599,29 @@ class VaultItemListingScreenTest : BaseComposeTest() { .performClick() verify(exactly = 1) { viewModel.trySendAction( - VaultItemListingsAction.RemoveSendPasswordClick(sendId = "mockId-$number"), + VaultItemListingsAction.OverflowOptionClick( + action = ListingItemOverflowAction.SendAction.RemovePasswordClick( + sendId = "mockId-$number", + ), + ), ) } + } + + @Suppress("MaxLineLength") + @Test + fun `on send item delete overflow option click should display delete confirmation dialog and emits DeleteSendConfirmClick on confirmation`() { + val sendId = "mockId-1" + val message = "Are you sure you want to delete this Send?" + mutableStateFlow.update { + it.copy( + viewState = VaultItemListingState.ViewState.Content( + displayItemList = listOf(createDisplayItem(number = 1)), + ), + ) + } + composeTestRule.onNode(isDialog()).assertDoesNotExist() + composeTestRule.onNodeWithText(message).assertDoesNotExist() composeTestRule .onNodeWithContentDescription("Options") @@ -598,28 +631,6 @@ class VaultItemListingScreenTest : BaseComposeTest() { .onNodeWithText("Delete") .assert(hasAnyAncestor(isDialog())) .performClick() - verify(exactly = 1) { - viewModel.trySendAction( - VaultItemListingsAction.DeleteSendClick(sendId = "mockId-$number"), - ) - } - } - - @Suppress("MaxLineLength") - @Test - fun `delete send confirmation dialog should be displayed according to state and emits DeleteSendConfirmClick on confirmation`() { - val sendId = "sendId" - val message = "Are you sure you want to delete this Send?" - composeTestRule.onNode(isDialog()).assertDoesNotExist() - composeTestRule.onNodeWithText(message).assertDoesNotExist() - - mutableStateFlow.update { - it.copy( - dialogState = VaultItemListingState.DialogState.DeleteSendConfirmation( - sendId = sendId, - ), - ) - } composeTestRule .onNodeWithText(message) @@ -631,7 +642,11 @@ class VaultItemListingScreenTest : BaseComposeTest() { .performClick() verify(exactly = 1) { - viewModel.trySendAction(VaultItemListingsAction.DeleteSendConfirmClick(sendId)) + viewModel.trySendAction( + VaultItemListingsAction.OverflowOptionClick( + action = ListingItemOverflowAction.SendAction.DeleteClick(sendId = sendId), + ), + ) } } @@ -716,25 +731,10 @@ private fun createDisplayItem(number: Int): VaultItemListingState.DisplayItem = ), ), 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"), - ), + ListingItemOverflowAction.SendAction.EditClick(sendId = "mockId-$number"), + ListingItemOverflowAction.SendAction.CopyUrlClick(sendUrl = "www.test.com"), + ListingItemOverflowAction.SendAction.ShareUrlClick(sendUrl = "www.test.com"), + ListingItemOverflowAction.SendAction.RemovePasswordClick(sendId = "mockId-$number"), + ListingItemOverflowAction.SendAction.DeleteClick(sendId = "mockId-$number"), ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index c15d5b273..e025734ec 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -22,6 +22,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.model.ListingItemOverflowAction import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.createMockDisplayItemForCipher import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType @@ -189,89 +190,105 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { } @Test - fun `CopySendUrlClick should call setText on clipboardManager`() { + fun `OverflowOptionClick Send EditClick should emit NavigateToSendItem`() = runTest { + val sendId = "sendId" + val viewModel = createVaultItemListingViewModel() + viewModel.eventFlow.test { + viewModel.actionChannel.trySend( + VaultItemListingsAction.OverflowOptionClick( + ListingItemOverflowAction.SendAction.EditClick(sendId = sendId), + ), + ) + assertEquals(VaultItemListingEvent.NavigateToSendItem(sendId), awaitItem()) + } + } + + @Test + fun `OverflowOptionClick Send CopyUrlClick 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)) + viewModel.actionChannel.trySend( + VaultItemListingsAction.OverflowOptionClick( + ListingItemOverflowAction.SendAction.CopyUrlClick(sendUrl = sendUrl), + ), + ) verify(exactly = 1) { clipboardManager.setText(text = sendUrl) } } @Test - fun `DeleteSendClick should display delete confirmation dialog`() { - val sendId = "sendId" - val viewModel = createVaultItemListingViewModel() - viewModel.trySendAction(VaultItemListingsAction.DeleteSendClick(sendId)) - assertEquals( - initialState.copy( - dialogState = VaultItemListingState.DialogState.DeleteSendConfirmation( - sendId = sendId, - ), - ), - viewModel.stateFlow.value, - ) - } + fun `OverflowOptionClick Send DeleteClick with deleteSend error should display error dialog`() = + runTest { + val sendId = "sendId1234" + coEvery { vaultRepository.deleteSend(sendId) } returns DeleteSendResult.Error - @Test - fun `DeleteSendConfirmClick with deleteSend error should display error dialog`() = runTest { - val sendId = "sendId1234" - coEvery { vaultRepository.deleteSend(sendId) } returns DeleteSendResult.Error - - val viewModel = createVaultItemListingViewModel() - viewModel.stateFlow.test { - assertEquals(initialState, awaitItem()) - viewModel.trySendAction(VaultItemListingsAction.DeleteSendConfirmClick(sendId)) - assertEquals( - initialState.copy( - dialogState = VaultItemListingState.DialogState.Loading( - message = R.string.deleting.asText(), + val viewModel = createVaultItemListingViewModel() + viewModel.stateFlow.test { + assertEquals(initialState, awaitItem()) + viewModel.actionChannel.trySend( + VaultItemListingsAction.OverflowOptionClick( + ListingItemOverflowAction.SendAction.DeleteClick(sendId = sendId), ), - ), - awaitItem(), - ) - assertEquals( - initialState.copy( - dialogState = VaultItemListingState.DialogState.Error( - title = R.string.an_error_has_occurred.asText(), - message = R.string.generic_error_message.asText(), + ) + assertEquals( + initialState.copy( + dialogState = VaultItemListingState.DialogState.Loading( + message = R.string.deleting.asText(), + ), ), - ), - awaitItem(), - ) + awaitItem(), + ) + assertEquals( + initialState.copy( + dialogState = VaultItemListingState.DialogState.Error( + title = R.string.an_error_has_occurred.asText(), + message = R.string.generic_error_message.asText(), + ), + ), + awaitItem(), + ) + } } - } @Test - fun `DeleteSendConfirmClick with deleteSend success should emit ShowToast`() = runTest { - val sendId = "sendId1234" - coEvery { vaultRepository.deleteSend(sendId) } returns DeleteSendResult.Success + fun `OverflowOptionClick Send DeleteClick with deleteSend success should emit ShowToast`() = + runTest { + val sendId = "sendId1234" + coEvery { vaultRepository.deleteSend(sendId) } returns DeleteSendResult.Success - val viewModel = createVaultItemListingViewModel() - viewModel.eventFlow.test { - viewModel.trySendAction(VaultItemListingsAction.DeleteSendConfirmClick(sendId)) - assertEquals( - VaultItemListingEvent.ShowToast(R.string.send_deleted.asText()), - awaitItem(), - ) + val viewModel = createVaultItemListingViewModel() + viewModel.eventFlow.test { + viewModel.actionChannel.trySend( + VaultItemListingsAction.OverflowOptionClick( + ListingItemOverflowAction.SendAction.DeleteClick(sendId = sendId), + ), + ) + assertEquals( + VaultItemListingEvent.ShowToast(R.string.send_deleted.asText()), + awaitItem(), + ) + } } - } @Test - fun `ShareSendUrlClick should emit ShowShareSheet`() = runTest { + fun `OverflowOptionClick Send ShareUrlClick should emit ShowShareSheet`() = runTest { val sendUrl = "www.test.com" val viewModel = createVaultItemListingViewModel() viewModel.eventFlow.test { viewModel.actionChannel.trySend( - VaultItemListingsAction.ShareSendUrlClick(sendUrl = sendUrl), + VaultItemListingsAction.OverflowOptionClick( + ListingItemOverflowAction.SendAction.ShareUrlClick(sendUrl = sendUrl), + ), ) assertEquals(VaultItemListingEvent.ShowShareSheet(sendUrl), awaitItem()) } } + @Suppress("MaxLineLength") @Test - fun `RemovePasswordClick with removePasswordSend error should display error dialog`() = + fun `OverflowOptionClick Send RemovePasswordClick with removePasswordSend error should display error dialog`() = runTest { val sendId = "sendId1234" coEvery { @@ -281,7 +298,11 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { val viewModel = createVaultItemListingViewModel() viewModel.stateFlow.test { assertEquals(initialState, awaitItem()) - viewModel.trySendAction(VaultItemListingsAction.RemoveSendPasswordClick(sendId)) + viewModel.actionChannel.trySend( + VaultItemListingsAction.OverflowOptionClick( + ListingItemOverflowAction.SendAction.RemovePasswordClick(sendId = sendId), + ), + ) assertEquals( initialState.copy( dialogState = VaultItemListingState.DialogState.Loading( @@ -302,22 +323,28 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") @Test - fun `RemovePasswordClick with removePasswordSend success should emit ShowToast`() = runTest { - val sendId = "sendId1234" - coEvery { - vaultRepository.removePasswordSend(sendId) - } returns RemovePasswordSendResult.Success(mockk()) + fun `OverflowOptionClick Send RemovePasswordClick with removePasswordSend success should emit ShowToast`() = + runTest { + val sendId = "sendId1234" + coEvery { + vaultRepository.removePasswordSend(sendId) + } returns RemovePasswordSendResult.Success(mockk()) - val viewModel = createVaultItemListingViewModel() - viewModel.eventFlow.test { - viewModel.trySendAction(VaultItemListingsAction.RemoveSendPasswordClick(sendId)) - assertEquals( - VaultItemListingEvent.ShowToast(R.string.send_password_removed.asText()), - awaitItem(), - ) + val viewModel = createVaultItemListingViewModel() + viewModel.eventFlow.test { + viewModel.actionChannel.trySend( + VaultItemListingsAction.OverflowOptionClick( + ListingItemOverflowAction.SendAction.RemovePasswordClick(sendId = sendId), + ), + ) + assertEquals( + VaultItemListingEvent.ShowToast(R.string.send_password_removed.asText()), + awaitItem(), + ) + } } - } @Test fun `vaultDataStateFlow Loaded with items should update ViewState to Content`() = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt index ad1093349..7a028bd3d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt @@ -7,7 +7,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.platform.components.model.IconRes 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.itemlisting.model.ListingItemOverflowAction /** * Create a mock [VaultItemListingState.DisplayItem] with a given [number]. @@ -90,33 +90,16 @@ fun createMockDisplayItemForSend( contentDescription = R.string.maximum_access_count_reached.asText(), ), ), - overflowOptions = listOfNotNull( - VaultItemListingState.DisplayItem.OverflowItem( - title = R.string.edit.asText(), - action = VaultItemListingsAction.ItemClick(id = "mockId-$number"), + overflowOptions = listOf( + ListingItemOverflowAction.SendAction.EditClick(sendId = "mockId-$number"), + ListingItemOverflowAction.SendAction.CopyUrlClick( + sendUrl = "https://vault.bitwarden.com/#/send/mockAccessId-$number/mockKey-$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"), + ListingItemOverflowAction.SendAction.ShareUrlClick( + sendUrl = "https://vault.bitwarden.com/#/send/mockAccessId-$number/mockKey-$number", ), + ListingItemOverflowAction.SendAction.RemovePasswordClick(sendId = "mockId-$number"), + ListingItemOverflowAction.SendAction.DeleteClick(sendId = "mockId-$number"), ), ) } @@ -137,33 +120,16 @@ fun createMockDisplayItemForSend( contentDescription = R.string.maximum_access_count_reached.asText(), ), ), - overflowOptions = listOfNotNull( - VaultItemListingState.DisplayItem.OverflowItem( - title = R.string.edit.asText(), - action = VaultItemListingsAction.ItemClick(id = "mockId-$number"), + overflowOptions = listOf( + ListingItemOverflowAction.SendAction.EditClick(sendId = "mockId-$number"), + ListingItemOverflowAction.SendAction.CopyUrlClick( + sendUrl = "https://vault.bitwarden.com/#/send/mockAccessId-$number/mockKey-$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"), + ListingItemOverflowAction.SendAction.ShareUrlClick( + sendUrl = "https://vault.bitwarden.com/#/send/mockAccessId-$number/mockKey-$number", ), + ListingItemOverflowAction.SendAction.RemovePasswordClick(sendId = "mockId-$number"), + ListingItemOverflowAction.SendAction.DeleteClick(sendId = "mockId-$number"), ), ) }