Add overflow options to the listing screen and the search screen (#734)

This commit is contained in:
David Perez
2024-01-23 16:28:01 -06:00
committed by GitHub
parent b3453b62fe
commit 336e8a96cd
20 changed files with 904 additions and 47 deletions

View File

@@ -56,6 +56,14 @@ fun SearchContent(
is ListingItemOverflowAction.SendAction.EditClick,
is ListingItemOverflowAction.SendAction.RemovePasswordClick,
is ListingItemOverflowAction.SendAction.ShareUrlClick,
is ListingItemOverflowAction.VaultAction.CopyNoteClick,
is ListingItemOverflowAction.VaultAction.CopyNumberClick,
is ListingItemOverflowAction.VaultAction.CopyPasswordClick,
is ListingItemOverflowAction.VaultAction.CopySecurityCodeClick,
is ListingItemOverflowAction.VaultAction.CopyUsernameClick,
is ListingItemOverflowAction.VaultAction.EditClick,
is ListingItemOverflowAction.VaultAction.LaunchClick,
is ListingItemOverflowAction.VaultAction.ViewClick,
null,
-> Unit
}

View File

@@ -18,6 +18,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
@@ -61,6 +62,7 @@ fun SearchScreen(
is SearchEvent.NavigateToEditSend -> onNavigateToEditSend(event.sendId)
is SearchEvent.NavigateToEditCipher -> onNavigateToEditCipher(event.cipherId)
is SearchEvent.NavigateToViewCipher -> onNavigateToViewCipher(event.cipherId)
is SearchEvent.NavigateToUrl -> intentManager.launchUri(event.url.toUri())
is SearchEvent.ShowShareSheet -> intentManager.shareText(event.content)
is SearchEvent.ShowToast -> {
Toast

View File

@@ -158,6 +158,38 @@ class SearchViewModel @Inject constructor(
is ListingItemOverflowAction.SendAction.ShareUrlClick -> {
handleShareUrlClick(overflowAction)
}
is ListingItemOverflowAction.VaultAction.CopyNoteClick -> {
handleCopyNoteClick(overflowAction)
}
is ListingItemOverflowAction.VaultAction.CopyNumberClick -> {
handleCopyNumberClick(overflowAction)
}
is ListingItemOverflowAction.VaultAction.CopyPasswordClick -> {
handleCopyPasswordClick(overflowAction)
}
is ListingItemOverflowAction.VaultAction.CopySecurityCodeClick -> {
handleCopySecurityCodeClick(overflowAction)
}
is ListingItemOverflowAction.VaultAction.CopyUsernameClick -> {
handleCopyUsernameClick(overflowAction)
}
is ListingItemOverflowAction.VaultAction.EditClick -> {
handleEditCipherClick(overflowAction)
}
is ListingItemOverflowAction.VaultAction.LaunchClick -> {
handleLaunchCipherUrlClick(overflowAction)
}
is ListingItemOverflowAction.VaultAction.ViewClick -> {
handleViewCipherClick(overflowAction)
}
}
}
@@ -199,6 +231,48 @@ class SearchViewModel @Inject constructor(
sendEvent(SearchEvent.ShowShareSheet(action.sendUrl))
}
private fun handleCopyNoteClick(action: ListingItemOverflowAction.VaultAction.CopyNoteClick) {
clipboardManager.setText(action.notes)
}
private fun handleCopyNumberClick(
action: ListingItemOverflowAction.VaultAction.CopyNumberClick,
) {
clipboardManager.setText(action.number)
}
private fun handleCopyPasswordClick(
action: ListingItemOverflowAction.VaultAction.CopyPasswordClick,
) {
clipboardManager.setText(action.password)
}
private fun handleCopySecurityCodeClick(
action: ListingItemOverflowAction.VaultAction.CopySecurityCodeClick,
) {
clipboardManager.setText(action.securityCode)
}
private fun handleCopyUsernameClick(
action: ListingItemOverflowAction.VaultAction.CopyUsernameClick,
) {
clipboardManager.setText(action.username)
}
private fun handleEditCipherClick(action: ListingItemOverflowAction.VaultAction.EditClick) {
sendEvent(SearchEvent.NavigateToEditCipher(action.cipherId))
}
private fun handleLaunchCipherUrlClick(
action: ListingItemOverflowAction.VaultAction.LaunchClick,
) {
sendEvent(SearchEvent.NavigateToUrl(action.url))
}
private fun handleViewCipherClick(action: ListingItemOverflowAction.VaultAction.ViewClick) {
sendEvent(SearchEvent.NavigateToViewCipher(action.cipherId))
}
private fun handleInternalAction(action: SearchAction.Internal) {
when (action) {
is SearchAction.Internal.IconLoadingSettingReceive -> {
@@ -719,6 +793,13 @@ sealed class SearchEvent {
val cipherId: String,
) : SearchEvent()
/**
* Navigates to the given [url].
*/
data class NavigateToUrl(
val url: String,
) : SearchEvent()
/**
* Shares the [content] with share sheet.
*/

View File

@@ -19,6 +19,7 @@ import com.x8bit.bitwarden.ui.platform.feature.search.SearchTypeData
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
import com.x8bit.bitwarden.ui.tools.feature.send.util.toLabelIcons
import com.x8bit.bitwarden.ui.tools.feature.send.util.toOverflowActions
import com.x8bit.bitwarden.ui.vault.feature.util.toOverflowActions
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
import java.time.Clock
@@ -169,7 +170,7 @@ private fun CipherView.toDisplayItem(
isIconLoadingDisabled = isIconLoadingDisabled,
),
extraIconList = emptyList(),
overflowOptions = emptyList(),
overflowOptions = toOverflowActions(),
)
private fun CipherView.toIconData(

View File

@@ -35,6 +35,41 @@ fun VaultItemListingContent(
onOverflowItemClick: (action: ListingItemOverflowAction) -> Unit,
modifier: Modifier = Modifier,
) {
var showConfirmationDialog: ListingItemOverflowAction? by rememberSaveable {
mutableStateOf(null)
}
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,
is ListingItemOverflowAction.VaultAction.CopyNoteClick,
is ListingItemOverflowAction.VaultAction.CopyNumberClick,
is ListingItemOverflowAction.VaultAction.CopyPasswordClick,
is ListingItemOverflowAction.VaultAction.CopySecurityCodeClick,
is ListingItemOverflowAction.VaultAction.CopyUsernameClick,
is ListingItemOverflowAction.VaultAction.EditClick,
is ListingItemOverflowAction.VaultAction.LaunchClick,
is ListingItemOverflowAction.VaultAction.ViewClick,
null,
-> Unit
}
LazyColumn(
modifier = modifier,
) {
@@ -48,9 +83,6 @@ fun VaultItemListingContent(
)
}
items(state.displayItemList) {
var showConfirmationDialog: ListingItemOverflowAction? by rememberSaveable {
mutableStateOf(null)
}
BitwardenListItem(
startIcon = it.iconData,
label = it.title,
@@ -86,29 +118,6 @@ 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
}
}
item {

View File

@@ -54,6 +54,7 @@ data class VaultItemListingArgs(
fun NavGraphBuilder.vaultItemListingDestination(
onNavigateBack: () -> Unit,
onNavigateToVaultItemScreen: (id: String) -> Unit,
onNavigateToVaultEditItemScreen: (cipherId: String) -> Unit,
onNavigateToVaultAddItemScreen: () -> Unit,
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
) {
@@ -64,6 +65,7 @@ fun NavGraphBuilder.vaultItemListingDestination(
onNavigateToEditSendItem = { },
onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen,
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
onNavigateToSearch = { onNavigateToSearchVault(it as SearchType.Vault) },
)
}
@@ -84,6 +86,7 @@ fun NavGraphBuilder.sendItemListingDestination(
onNavigateToEditSendItem = onNavigateToEditSendItem,
onNavigateToVaultAddItemScreen = { },
onNavigateToVaultItemScreen = { },
onNavigateToVaultEditItemScreen = { },
onNavigateToSearch = { onNavigateToSearchSend(it as SearchType.Sends) },
)
}
@@ -96,6 +99,7 @@ private fun NavGraphBuilder.internalVaultItemListingDestination(
route: String,
onNavigateBack: () -> Unit,
onNavigateToVaultItemScreen: (id: String) -> Unit,
onNavigateToVaultEditItemScreen: (cipherId: String) -> Unit,
onNavigateToVaultAddItemScreen: () -> Unit,
onNavigateToAddSendItem: () -> Unit,
onNavigateToEditSendItem: (sendId: String) -> Unit,
@@ -120,6 +124,7 @@ private fun NavGraphBuilder.internalVaultItemListingDestination(
VaultItemListingScreen(
onNavigateBack = onNavigateBack,
onNavigateToVaultItem = onNavigateToVaultItemScreen,
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen,
onNavigateToAddSendItem = onNavigateToAddSendItem,
onNavigateToEditSendItem = onNavigateToEditSendItem,

View File

@@ -21,6 +21,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
@@ -49,6 +50,7 @@ import kotlinx.collections.immutable.persistentListOf
fun VaultItemListingScreen(
onNavigateBack: () -> Unit,
onNavigateToVaultItem: (id: String) -> Unit,
onNavigateToVaultEditItemScreen: (cipherId: String) -> Unit,
onNavigateToVaultAddItemScreen: () -> Unit,
onNavigateToAddSendItem: () -> Unit,
onNavigateToEditSendItem: (sendId: String) -> Unit,
@@ -88,6 +90,14 @@ fun VaultItemListingScreen(
onNavigateToVaultAddItemScreen()
}
is VaultItemListingEvent.NavigateToEditCipher -> {
onNavigateToVaultEditItemScreen(event.cipherId)
}
is VaultItemListingEvent.NavigateToUrl -> {
intentManager.launchUri(event.url.toUri())
}
is VaultItemListingEvent.NavigateToAddSendItem -> {
onNavigateToAddSendItem()
}

View File

@@ -182,6 +182,48 @@ class VaultItemListingViewModel @Inject constructor(
sendEvent(event)
}
private fun handleCopyNoteClick(action: ListingItemOverflowAction.VaultAction.CopyNoteClick) {
clipboardManager.setText(action.notes)
}
private fun handleCopyNumberClick(
action: ListingItemOverflowAction.VaultAction.CopyNumberClick,
) {
clipboardManager.setText(action.number)
}
private fun handleCopyPasswordClick(
action: ListingItemOverflowAction.VaultAction.CopyPasswordClick,
) {
clipboardManager.setText(action.password)
}
private fun handleCopySecurityCodeClick(
action: ListingItemOverflowAction.VaultAction.CopySecurityCodeClick,
) {
clipboardManager.setText(action.securityCode)
}
private fun handleCopyUsernameClick(
action: ListingItemOverflowAction.VaultAction.CopyUsernameClick,
) {
clipboardManager.setText(action.username)
}
private fun handleEditCipherClick(action: ListingItemOverflowAction.VaultAction.EditClick) {
sendEvent(VaultItemListingEvent.NavigateToEditCipher(action.cipherId))
}
private fun handleLaunchCipherUrlClick(
action: ListingItemOverflowAction.VaultAction.LaunchClick,
) {
sendEvent(VaultItemListingEvent.NavigateToUrl(action.url))
}
private fun handleViewCipherClick(action: ListingItemOverflowAction.VaultAction.ViewClick) {
sendEvent(VaultItemListingEvent.NavigateToVaultItem(action.cipherId))
}
private fun handleDismissDialogClick() {
mutableStateFlow.update { it.copy(dialogState = null) }
}
@@ -236,6 +278,38 @@ class VaultItemListingViewModel @Inject constructor(
is ListingItemOverflowAction.SendAction.ShareUrlClick -> {
handleShareSendUrlClick(overflowAction)
}
is ListingItemOverflowAction.VaultAction.CopyNoteClick -> {
handleCopyNoteClick(overflowAction)
}
is ListingItemOverflowAction.VaultAction.CopyNumberClick -> {
handleCopyNumberClick(overflowAction)
}
is ListingItemOverflowAction.VaultAction.CopyPasswordClick -> {
handleCopyPasswordClick(overflowAction)
}
is ListingItemOverflowAction.VaultAction.CopySecurityCodeClick -> {
handleCopySecurityCodeClick(overflowAction)
}
is ListingItemOverflowAction.VaultAction.CopyUsernameClick -> {
handleCopyUsernameClick(overflowAction)
}
is ListingItemOverflowAction.VaultAction.EditClick -> {
handleEditCipherClick(overflowAction)
}
is ListingItemOverflowAction.VaultAction.LaunchClick -> {
handleLaunchCipherUrlClick(overflowAction)
}
is ListingItemOverflowAction.VaultAction.ViewClick -> {
handleViewCipherClick(overflowAction)
}
}
}
@@ -696,6 +770,20 @@ sealed class VaultItemListingEvent {
*/
data class NavigateToVaultItem(val id: String) : VaultItemListingEvent()
/**
* Navigates to view a cipher.
*/
data class NavigateToEditCipher(
val cipherId: String,
) : VaultItemListingEvent()
/**
* Navigates to the given [url].
*/
data class NavigateToUrl(
val url: String,
) : VaultItemListingEvent()
/**
* Navigates to the SearchScreen with the given type filter.
*/

View File

@@ -60,4 +60,73 @@ sealed class ListingItemOverflowAction : Parcelable {
override val title: Text get() = R.string.delete.asText()
}
}
/**
* Represents the vault actions.
*/
sealed class VaultAction : ListingItemOverflowAction() {
/**
* Click on the view cipher overflow option.
*/
@Parcelize
data class ViewClick(val cipherId: String) : VaultAction() {
override val title: Text get() = R.string.view.asText()
}
/**
* Click on the edit cipher overflow option.
*/
@Parcelize
data class EditClick(val cipherId: String) : VaultAction() {
override val title: Text get() = R.string.edit.asText()
}
/**
* Click on the copy username overflow option.
*/
@Parcelize
data class CopyUsernameClick(val username: String) : VaultAction() {
override val title: Text get() = R.string.copy_username.asText()
}
/**
* Click on the copy password overflow option.
*/
@Parcelize
data class CopyPasswordClick(val password: String) : VaultAction() {
override val title: Text get() = R.string.copy_password.asText()
}
/**
* Click on the copy number overflow option.
*/
@Parcelize
data class CopyNumberClick(val number: String) : VaultAction() {
override val title: Text get() = R.string.copy_number.asText()
}
/**
* Click on the copy security code overflow option.
*/
@Parcelize
data class CopySecurityCodeClick(val securityCode: String) : VaultAction() {
override val title: Text get() = R.string.copy_security_code.asText()
}
/**
* Click on the copy secure note overflow option.
*/
@Parcelize
data class CopyNoteClick(val notes: String) : VaultAction() {
override val title: Text get() = R.string.copy_notes.asText()
}
/**
* Click on the launch overflow option.
*/
@Parcelize
data class LaunchClick(val url: String) : VaultAction() {
override val title: Text get() = R.string.launch.asText()
}
}
}

View File

@@ -14,6 +14,7 @@ import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
import com.x8bit.bitwarden.ui.tools.feature.send.util.toLabelIcons
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.util.toOverflowActions
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
import java.time.Clock
@@ -173,7 +174,7 @@ private fun CipherView.toDisplayItem(
isIconLoadingDisabled = isIconLoadingDisabled,
),
extraIconList = emptyList(),
overflowOptions = emptyList(),
overflowOptions = toOverflowActions(),
)
private fun CipherView.toIconData(

View File

@@ -0,0 +1,38 @@
package com.x8bit.bitwarden.ui.vault.feature.util
import com.bitwarden.core.CipherType
import com.bitwarden.core.CipherView
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
/**
* Creates the list of overflow actions to be displayed for a [CipherView].
*/
fun CipherView.toOverflowActions(): List<ListingItemOverflowAction.VaultAction> =
this
.id
?.let { cipherId ->
listOfNotNull(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = cipherId),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = cipherId)
.takeUnless { this.deletedDate != null },
this.login?.username?.let {
ListingItemOverflowAction.VaultAction.CopyUsernameClick(username = it)
},
this.login?.password?.let {
ListingItemOverflowAction.VaultAction.CopyPasswordClick(password = it)
},
this.card?.number?.let {
ListingItemOverflowAction.VaultAction.CopyNumberClick(number = it)
},
this.card?.code?.let {
ListingItemOverflowAction.VaultAction.CopySecurityCodeClick(securityCode = it)
},
this.notes
?.let { ListingItemOverflowAction.VaultAction.CopyNoteClick(notes = it) }
.takeIf { this.type == CipherType.SECURE_NOTE },
this.login?.uris?.firstOrNull { it.uri != null }?.uri?.let {
ListingItemOverflowAction.VaultAction.LaunchClick(url = it)
},
)
}
.orEmpty()

View File

@@ -44,6 +44,7 @@ fun NavGraphBuilder.vaultGraph(
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen,
onNavigateToSearchVault = onNavigateToSearchVault,
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
)
vaultVerificationCodeDestination(

View File

@@ -37,7 +37,7 @@ fun createMockCipherView(
name = "mockName-$number",
notes = "mockNotes-$number",
type = cipherType,
login = createMockLoginView(number = number),
login = createMockLoginView(number = number).takeIf { cipherType == CipherType.LOGIN },
creationDate = ZonedDateTime
.parse("2023-10-27T12:00:00Z")
.toInstant(),
@@ -52,13 +52,15 @@ fun createMockCipherView(
.parse("2023-10-27T12:00:00Z")
.toInstant(),
attachments = listOf(createMockAttachmentView(number = number)),
card = createMockCardView(number = number),
card = createMockCardView(number = number).takeIf { cipherType == CipherType.CARD },
fields = listOf(createMockFieldView(number = number)),
identity = createMockIdentityView(number = number),
identity = createMockIdentityView(number = number).takeIf {
cipherType == CipherType.IDENTITY
},
favorite = false,
passwordHistory = listOf(createMockPasswordHistoryView(number = number)),
reprompt = CipherRepromptType.NONE,
secureNote = createMockSecureNoteView(),
secureNote = createMockSecureNoteView().takeIf { cipherType == CipherType.SECURE_NOTE },
edit = false,
organizationUseTotp = false,
viewPassword = false,

View File

@@ -13,6 +13,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode
import androidx.core.net.toUri
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
@@ -43,6 +44,7 @@ class SearchScreenTest : BaseComposeTest() {
}
private val intentManager: IntentManager = mockk {
every { shareText(any()) } just runs
every { launchUri(any()) } just runs
}
private var onNavigateBackCalled = false
@@ -91,6 +93,15 @@ class SearchScreenTest : BaseComposeTest() {
assertEquals(cipherId, onNavigateToViewCipherId)
}
@Test
fun `NavigateToUrl should call launchUri on the IntentManager`() {
val url = "www.test.com"
mutableEventFlow.tryEmit(SearchEvent.NavigateToUrl(url))
verify(exactly = 1) {
intentManager.launchUri(url.toUri())
}
}
@Test
fun `ShowShareSheet should call onNavigateBack`() {
val sendUrl = "www.test.com"

View File

@@ -301,6 +301,130 @@ class SearchViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `OverflowOptionClick Vault CopyNoteClick should call setText on the ClipboardManager`() =
runTest {
val notes = "notes"
val viewModel = createViewModel()
viewModel.actionChannel.trySend(
SearchAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.CopyNoteClick(notes = notes),
),
)
verify(exactly = 1) {
clipboardManager.setText(notes)
}
}
@Test
fun `OverflowOptionClick Vault CopyNumberClick should call setText on the ClipboardManager`() =
runTest {
val number = "12345-4321-9876-6789"
val viewModel = createViewModel()
viewModel.actionChannel.trySend(
SearchAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.CopyNumberClick(number = number),
),
)
verify(exactly = 1) {
clipboardManager.setText(number)
}
}
@Suppress("MaxLineLength")
@Test
fun `OverflowOptionClick Vault CopyPasswordClick should call setText on the ClipboardManager`() =
runTest {
val password = "passTheWord"
val viewModel = createViewModel()
viewModel.actionChannel.trySend(
SearchAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.CopyPasswordClick(password = password),
),
)
verify(exactly = 1) {
clipboardManager.setText(password)
}
}
@Suppress("MaxLineLength")
@Test
fun `OverflowOptionClick Vault CopySecurityCodeClick should call setText on the ClipboardManager`() =
runTest {
val securityCode = "234"
val viewModel = createViewModel()
viewModel.actionChannel.trySend(
SearchAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.CopySecurityCodeClick(
securityCode = securityCode,
),
),
)
verify(exactly = 1) {
clipboardManager.setText(securityCode)
}
}
@Suppress("MaxLineLength")
@Test
fun `OverflowOptionClick Vault CopyUsernameClick should call setText on the ClipboardManager`() =
runTest {
val username = "bitwarden"
val viewModel = createViewModel()
viewModel.actionChannel.trySend(
SearchAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.CopyUsernameClick(
username = username,
),
),
)
verify(exactly = 1) {
clipboardManager.setText(username)
}
}
@Test
fun `OverflowOptionClick Vault EditClick should emit NavigateToEditCipher`() = runTest {
val cipherId = "cipherId-1234"
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(
SearchAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.EditClick(cipherId = cipherId),
),
)
assertEquals(SearchEvent.NavigateToEditCipher(cipherId), awaitItem())
}
}
@Test
fun `OverflowOptionClick Vault LaunchClick should emit NavigateToUrl`() = runTest {
val url = "www.test.com"
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(
SearchAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.LaunchClick(url = url),
),
)
assertEquals(SearchEvent.NavigateToUrl(url), awaitItem())
}
}
@Test
fun `OverflowOptionClick Vault ViewClick should emit NavigateToUrl`() = runTest {
val cipherId = "cipherId-9876"
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(
SearchAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = cipherId),
),
)
assertEquals(SearchEvent.NavigateToViewCipher(cipherId), awaitItem())
}
}
@Test
fun `vaultDataStateFlow Loaded with items should update ViewState to Content`() = runTest {
setupMockUri()

View File

@@ -27,7 +27,19 @@ fun createMockDisplayItemForCipher(
fallbackIconRes = R.drawable.ic_login_item,
),
extraIconList = emptyList(),
overflowOptions = emptyList(),
overflowOptions = listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.CopyUsernameClick(
username = "mockUsername-$number",
),
ListingItemOverflowAction.VaultAction.CopyPasswordClick(
password = "mockPassword-$number",
),
ListingItemOverflowAction.VaultAction.LaunchClick(
url = "www.mockuri$number.com",
),
),
)
}
@@ -38,7 +50,13 @@ fun createMockDisplayItemForCipher(
subtitle = null,
iconData = IconData.Local(R.drawable.ic_secure_note_item),
extraIconList = emptyList(),
overflowOptions = emptyList(),
overflowOptions = listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.CopyNoteClick(
notes = "mockNotes-$number",
),
),
)
}
@@ -49,7 +67,16 @@ fun createMockDisplayItemForCipher(
subtitle = "er-$number",
iconData = IconData.Local(R.drawable.ic_card_item),
extraIconList = emptyList(),
overflowOptions = emptyList(),
overflowOptions = listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.CopyNumberClick(
number = "mockNumber-$number",
),
ListingItemOverflowAction.VaultAction.CopySecurityCodeClick(
securityCode = "mockCode-$number",
),
),
)
}
@@ -60,7 +87,10 @@ fun createMockDisplayItemForCipher(
subtitle = "mockFirstName-${number}mockLastName-$number",
iconData = IconData.Local(R.drawable.ic_identity_item),
extraIconList = emptyList(),
overflowOptions = emptyList(),
overflowOptions = listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = "mockId-$number"),
),
)
}
}

View File

@@ -17,6 +17,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode
import androidx.core.net.toUri
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
@@ -51,10 +52,12 @@ class VaultItemListingScreenTest : BaseComposeTest() {
private var onNavigateToAddSendScreenCalled = false
private var onNavigateToEditSendItemId: String? = null
private var onNavigateToVaultItemId: String? = null
private var onNavigateToVaultEditItemScreenId: String? = null
private var onNavigateToSearchType: SearchType? = null
private val intentManager: IntentManager = mockk {
every { shareText(any()) } just runs
every { launchUri(any()) } just runs
}
private val mutableEventFlow = bufferedMutableSharedFlow<VaultItemListingEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
@@ -75,6 +78,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
onNavigateToAddSendItem = { onNavigateToAddSendScreenCalled = true },
onNavigateToEditSendItem = { onNavigateToEditSendItemId = it },
onNavigateToSearch = { onNavigateToSearchType = it },
onNavigateToVaultEditItemScreen = { onNavigateToVaultEditItemScreenId = it },
)
}
}
@@ -157,6 +161,13 @@ class VaultItemListingScreenTest : BaseComposeTest() {
assertEquals(searchType, onNavigateToSearchType)
}
@Test
fun `NavigateToEditCipher should call onNavigateToVaultEditItemScreen`() {
val cipherId = "cipherId"
mutableEventFlow.tryEmit(VaultItemListingEvent.NavigateToEditCipher(cipherId))
assertEquals(cipherId, onNavigateToVaultEditItemScreenId)
}
@Test
fun `NavigateToSendItem event should call onNavigateToEditSendItemId`() {
val sendId = "sendId"
@@ -171,6 +182,15 @@ class VaultItemListingScreenTest : BaseComposeTest() {
assertEquals(id, onNavigateToVaultItemId)
}
@Test
fun `NavigateToUrl should call launchUri on the IntentManager`() {
val url = "www.test.com"
mutableEventFlow.tryEmit(VaultItemListingEvent.NavigateToUrl(url))
verify(exactly = 1) {
intentManager.launchUri(url.toUri())
}
}
@Test
fun `progressbar should be displayed according to state`() {
mutableStateFlow.update { DEFAULT_STATE }

View File

@@ -53,7 +53,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
ZoneOffset.UTC,
)
private val clipboardManager: BitwardenClipboardManager = mockk()
private val clipboardManager: BitwardenClipboardManager = mockk {
every { setText(any<String>()) } just runs
}
private val mutableVaultDataStateFlow =
MutableStateFlow<DataState<VaultData>>(DataState.Loading)
@@ -350,18 +352,138 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `OverflowOptionClick Vault CopyNoteClick should call setText on the ClipboardManager`() =
runTest {
val notes = "notes"
val viewModel = createVaultItemListingViewModel()
viewModel.actionChannel.trySend(
VaultItemListingsAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.CopyNoteClick(notes = notes),
),
)
verify(exactly = 1) {
clipboardManager.setText(notes)
}
}
@Test
fun `OverflowOptionClick Vault CopyNumberClick should call setText on the ClipboardManager`() =
runTest {
val number = "12345-4321-9876-6789"
val viewModel = createVaultItemListingViewModel()
viewModel.actionChannel.trySend(
VaultItemListingsAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.CopyNumberClick(number = number),
),
)
verify(exactly = 1) {
clipboardManager.setText(number)
}
}
@Suppress("MaxLineLength")
@Test
fun `OverflowOptionClick Vault CopyPasswordClick should call setText on the ClipboardManager`() =
runTest {
val password = "passTheWord"
val viewModel = createVaultItemListingViewModel()
viewModel.actionChannel.trySend(
VaultItemListingsAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.CopyPasswordClick(password = password),
),
)
verify(exactly = 1) {
clipboardManager.setText(password)
}
}
@Suppress("MaxLineLength")
@Test
fun `OverflowOptionClick Vault CopySecurityCodeClick should call setText on the ClipboardManager`() =
runTest {
val securityCode = "234"
val viewModel = createVaultItemListingViewModel()
viewModel.actionChannel.trySend(
VaultItemListingsAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.CopySecurityCodeClick(
securityCode = securityCode,
),
),
)
verify(exactly = 1) {
clipboardManager.setText(securityCode)
}
}
@Suppress("MaxLineLength")
@Test
fun `OverflowOptionClick Vault CopyUsernameClick should call setText on the ClipboardManager`() =
runTest {
val username = "bitwarden"
val viewModel = createVaultItemListingViewModel()
viewModel.actionChannel.trySend(
VaultItemListingsAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.CopyUsernameClick(
username = username,
),
),
)
verify(exactly = 1) {
clipboardManager.setText(username)
}
}
@Test
fun `OverflowOptionClick Vault EditClick should emit NavigateToEditCipher`() = runTest {
val cipherId = "cipherId-1234"
val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(
VaultItemListingsAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.EditClick(cipherId = cipherId),
),
)
assertEquals(VaultItemListingEvent.NavigateToEditCipher(cipherId), awaitItem())
}
}
@Test
fun `OverflowOptionClick Vault LaunchClick should emit NavigateToUrl`() = runTest {
val url = "www.test.com"
val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(
VaultItemListingsAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.LaunchClick(url = url),
),
)
assertEquals(VaultItemListingEvent.NavigateToUrl(url), awaitItem())
}
}
@Test
fun `OverflowOptionClick Vault ViewClick should emit NavigateToUrl`() = runTest {
val cipherId = "cipherId-9876"
val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(
VaultItemListingsAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = cipherId),
),
)
assertEquals(VaultItemListingEvent.NavigateToVaultItem(cipherId), awaitItem())
}
}
@Test
fun `vaultDataStateFlow Loaded with items should update ViewState to Content`() =
runTest {
setupMockUri()
val dataState = DataState.Loaded(
data = VaultData(
cipherViewList = listOf(
createMockCipherView(
number = 1,
isDeleted = false,
),
),
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = false)),
folderViewList = listOf(createMockFolderView(number = 1)),
collectionViewList = listOf(createMockCollectionView(number = 1)),
sendViewList = listOf(createMockSendView(number = 1)),

View File

@@ -28,7 +28,19 @@ fun createMockDisplayItemForCipher(
fallbackIconRes = R.drawable.ic_login_item,
),
extraIconList = emptyList(),
overflowOptions = emptyList(),
overflowOptions = listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.CopyUsernameClick(
username = "mockUsername-$number",
),
ListingItemOverflowAction.VaultAction.CopyPasswordClick(
password = "mockPassword-$number",
),
ListingItemOverflowAction.VaultAction.LaunchClick(
url = "www.mockuri$number.com",
),
),
)
}
@@ -39,7 +51,13 @@ fun createMockDisplayItemForCipher(
subtitle = subtitle,
iconData = IconData.Local(R.drawable.ic_secure_note_item),
extraIconList = emptyList(),
overflowOptions = emptyList(),
overflowOptions = listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.CopyNoteClick(
notes = "mockNotes-$number",
),
),
)
}
@@ -50,7 +68,16 @@ fun createMockDisplayItemForCipher(
subtitle = subtitle,
iconData = IconData.Local(R.drawable.ic_card_item),
extraIconList = emptyList(),
overflowOptions = emptyList(),
overflowOptions = listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.CopyNumberClick(
number = "mockNumber-$number",
),
ListingItemOverflowAction.VaultAction.CopySecurityCodeClick(
securityCode = "mockCode-$number",
),
),
)
}
@@ -61,7 +88,10 @@ fun createMockDisplayItemForCipher(
subtitle = subtitle,
iconData = IconData.Local(R.drawable.ic_identity_item),
extraIconList = emptyList(),
overflowOptions = emptyList(),
overflowOptions = listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = "mockId-$number"),
),
)
}
}

View File

@@ -0,0 +1,205 @@
package com.x8bit.bitwarden.ui.vault.feature.util
import com.bitwarden.core.CipherType
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCardView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockIdentityView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSecureNoteView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockUriView
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class CipherViewExtensionsTest {
@Test
fun `toOverflowActions should return all actions for a login cipher`() {
val id = "mockId-1"
val username = "Bitwarden"
val password = "password"
val uri = "www.test.com"
val cipher = createMockCipherView(number = 1, cipherType = CipherType.LOGIN).copy(
id = id,
login = createMockLoginView(number = 1).copy(
username = username,
password = password,
uris = listOf(createMockUriView(number = 1).copy(uri = uri)),
),
)
val result = cipher.toOverflowActions()
assertEquals(
listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = id),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = id),
ListingItemOverflowAction.VaultAction.CopyUsernameClick(username = username),
ListingItemOverflowAction.VaultAction.CopyPasswordClick(password = password),
ListingItemOverflowAction.VaultAction.LaunchClick(url = uri),
),
result,
)
}
@Test
fun `toOverflowActions should return minimum actions for a login cipher`() {
val id = "mockId-1"
val cipher = createMockCipherView(
number = 1,
isDeleted = true,
cipherType = CipherType.LOGIN,
)
.copy(
id = id,
login = createMockLoginView(number = 1).copy(
username = null,
password = null,
uris = null,
),
)
val result = cipher.toOverflowActions()
assertEquals(
listOf(ListingItemOverflowAction.VaultAction.ViewClick(cipherId = id)),
result,
)
}
@Test
fun `toOverflowActions should return all actions for a card cipher`() {
val id = "mockId-1"
val number = "1322-2414-7634-2354"
val securityCode = "123"
val cipher = createMockCipherView(number = 1, cipherType = CipherType.CARD).copy(
id = id,
card = createMockCardView(number = 1).copy(
number = number,
code = securityCode,
),
)
val result = cipher.toOverflowActions()
assertEquals(
listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = id),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = id),
ListingItemOverflowAction.VaultAction.CopyNumberClick(number = number),
ListingItemOverflowAction.VaultAction.CopySecurityCodeClick(
securityCode = securityCode,
),
),
result,
)
}
@Test
fun `toOverflowActions should return minimum actions for a card cipher`() {
val id = "mockId-1"
val cipher = createMockCipherView(
number = 1,
isDeleted = true,
cipherType = CipherType.CARD,
)
.copy(
id = id,
card = createMockCardView(number = 1).copy(
number = null,
code = null,
),
)
val result = cipher.toOverflowActions()
assertEquals(
listOf(ListingItemOverflowAction.VaultAction.ViewClick(cipherId = id)),
result,
)
}
@Test
fun `toOverflowActions should return all actions for a identity cipher`() {
val id = "mockId-1"
val cipher = createMockCipherView(number = 1, cipherType = CipherType.IDENTITY).copy(
id = id,
identity = createMockIdentityView(number = 1),
)
val result = cipher.toOverflowActions()
assertEquals(
listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = id),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = id),
),
result,
)
}
@Test
fun `toOverflowActions should return minimum actions for a identity cipher`() {
val id = "mockId-1"
val cipher = createMockCipherView(
number = 1,
isDeleted = true,
cipherType = CipherType.IDENTITY,
)
.copy(
id = id,
identity = createMockIdentityView(number = 1),
)
val result = cipher.toOverflowActions()
assertEquals(
listOf(ListingItemOverflowAction.VaultAction.ViewClick(cipherId = id)),
result,
)
}
@Test
fun `toOverflowActions should return all actions for a secure note cipher`() {
val id = "mockId-1"
val notes = "so secure"
val cipher = createMockCipherView(number = 1, cipherType = CipherType.SECURE_NOTE).copy(
id = id,
secureNote = createMockSecureNoteView(),
notes = notes,
)
val result = cipher.toOverflowActions()
assertEquals(
listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = id),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = id),
ListingItemOverflowAction.VaultAction.CopyNoteClick(notes = notes),
),
result,
)
}
@Test
fun `toOverflowActions should return minimum actions for a secure note cipher`() {
val id = "mockId-1"
val cipher = createMockCipherView(
number = 1,
isDeleted = true,
cipherType = CipherType.SECURE_NOTE,
)
.copy(
id = id,
secureNote = createMockSecureNoteView(),
notes = null,
)
val result = cipher.toOverflowActions()
assertEquals(
listOf(ListingItemOverflowAction.VaultAction.ViewClick(cipherId = id)),
result,
)
}
}