mirror of
https://github.com/bitwarden/android.git
synced 2024-11-29 06:18:52 +03:00
BIT-2009 Add support for nested collections (#1111)
This commit is contained in:
parent
5e1328eecb
commit
3b33360e58
13 changed files with 506 additions and 44 deletions
|
@ -39,6 +39,7 @@ import kotlinx.collections.immutable.toPersistentList
|
|||
fun VaultItemListingContent(
|
||||
state: VaultItemListingState.ViewState.Content,
|
||||
policyDisablesSend: Boolean,
|
||||
collectionClick: (id: String) -> Unit,
|
||||
folderClick: (id: String) -> Unit,
|
||||
vaultItemClick: (id: String) -> Unit,
|
||||
masterPasswordRepromptSubmit: (password: String, data: MasterPasswordRepromptData) -> Unit,
|
||||
|
@ -111,6 +112,35 @@ fun VaultItemListingContent(
|
|||
}
|
||||
}
|
||||
|
||||
if (state.displayCollectionList.isNotEmpty()) {
|
||||
item {
|
||||
BitwardenListHeaderTextWithSupportLabel(
|
||||
label = stringResource(id = R.string.collections),
|
||||
supportingLabel = state.displayCollectionList.count().toString(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
items(state.displayCollectionList) { collection ->
|
||||
BitwardenGroupItem(
|
||||
startIcon = painterResource(id = R.drawable.ic_collection),
|
||||
label = collection.name,
|
||||
supportingLabel = collection.count.toString(),
|
||||
onClick = { collectionClick(collection.id) },
|
||||
showDivider = false,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.displayFolderList.isNotEmpty()) {
|
||||
item {
|
||||
BitwardenListHeaderTextWithSupportLabel(
|
||||
|
@ -140,7 +170,7 @@ fun VaultItemListingContent(
|
|||
}
|
||||
}
|
||||
|
||||
if (state.displayItemList.isNotEmpty() && state.displayFolderList.isNotEmpty()) {
|
||||
if (state.shouldShowDivider) {
|
||||
item {
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
|
|
|
@ -56,7 +56,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
fun VaultItemListingScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToVaultItem: (id: String) -> Unit,
|
||||
|
@ -124,6 +124,10 @@ fun VaultItemListingScreen(
|
|||
is VaultItemListingEvent.NavigateToFolderItem -> {
|
||||
onNavigateToVaultItemListing(VaultItemListingType.Folder(event.folderId))
|
||||
}
|
||||
|
||||
is VaultItemListingEvent.NavigateToCollectionItem -> {
|
||||
onNavigateToVaultItemListing(VaultItemListingType.Collection(event.collectionId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -244,6 +248,7 @@ private fun VaultItemListingScaffold(
|
|||
policyDisablesSend = state.policyDisablesSend &&
|
||||
state.itemListingType is VaultItemListingState.ItemListingType.Send,
|
||||
vaultItemClick = vaultItemListingHandlers.itemClick,
|
||||
collectionClick = vaultItemListingHandlers.collectionClick,
|
||||
folderClick = vaultItemListingHandlers.folderClick,
|
||||
masterPasswordRepromptSubmit =
|
||||
vaultItemListingHandlers.masterPasswordRepromptSubmit,
|
||||
|
|
|
@ -139,6 +139,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
is VaultItemListingsAction.DismissDialogClick -> handleDismissDialogClick()
|
||||
is VaultItemListingsAction.BackClick -> handleBackClick()
|
||||
is VaultItemListingsAction.FolderClick -> handleFolderClick(action)
|
||||
is VaultItemListingsAction.CollectionClick -> handleCollectionClick(action)
|
||||
is VaultItemListingsAction.LockClick -> handleLockClick()
|
||||
is VaultItemListingsAction.SyncClick -> handleSyncClick()
|
||||
is VaultItemListingsAction.SearchIconClick -> handleSearchIconClick()
|
||||
|
@ -168,6 +169,10 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
authRepository.switchAccount(userId = action.accountSummary.userId)
|
||||
}
|
||||
|
||||
private fun handleCollectionClick(action: VaultItemListingsAction.CollectionClick) {
|
||||
sendEvent(VaultItemListingEvent.NavigateToCollectionItem(action.id))
|
||||
}
|
||||
|
||||
private fun handleFolderClick(action: VaultItemListingsAction.FolderClick) {
|
||||
sendEvent(VaultItemListingEvent.NavigateToFolderItem(action.id))
|
||||
}
|
||||
|
@ -839,12 +844,18 @@ data class VaultItemListingState(
|
|||
* Content state for the [VaultItemListingScreen] showing the actual content or items.
|
||||
*
|
||||
* @property displayItemList List of items to display.
|
||||
* @property displayFolderList list of folders to display.
|
||||
* @property displayCollectionList list of collections to display.
|
||||
*/
|
||||
data class Content(
|
||||
val displayItemList: List<DisplayItem>,
|
||||
val displayFolderList: List<FolderDisplayItem>,
|
||||
val displayCollectionList: List<CollectionDisplayItem>,
|
||||
) : ViewState() {
|
||||
override val isPullToRefreshEnabled: Boolean get() = true
|
||||
val shouldShowDivider: Boolean
|
||||
get() = displayItemList.isNotEmpty() &&
|
||||
(displayFolderList.isNotEmpty() || displayCollectionList.isNotEmpty())
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -895,6 +906,19 @@ data class VaultItemListingState(
|
|||
val count: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* The collection that is displayed to the user on the ItemListingScreen.
|
||||
*
|
||||
* @property id the id of the collection.
|
||||
* @property name the name of the collection.
|
||||
* @property count the amount of ciphers in the collection.
|
||||
*/
|
||||
data class CollectionDisplayItem(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val count: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents different types of item listing.
|
||||
*/
|
||||
|
@ -1031,6 +1055,11 @@ sealed class VaultItemListingEvent {
|
|||
*/
|
||||
data object NavigateToAddVaultItem : VaultItemListingEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the collection.
|
||||
*/
|
||||
data class NavigateToCollectionItem(val collectionId: String) : VaultItemListingEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the folder.
|
||||
*/
|
||||
|
@ -1165,6 +1194,13 @@ sealed class VaultItemListingsAction {
|
|||
*/
|
||||
data class ItemClick(val id: String) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* Click on the collection.
|
||||
*
|
||||
* @property id the id of the collection that has been clicked
|
||||
*/
|
||||
data class CollectionClick(val id: String) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* Click on the folder.
|
||||
*
|
||||
|
|
|
@ -19,6 +19,7 @@ data class VaultItemListingHandlers(
|
|||
val addVaultItemClick: () -> Unit,
|
||||
val itemClick: (id: String) -> Unit,
|
||||
val folderClick: (id: String) -> Unit,
|
||||
val collectionClick: (id: String) -> Unit,
|
||||
val masterPasswordRepromptSubmit: (password: String, MasterPasswordRepromptData) -> Unit,
|
||||
val refreshClick: () -> Unit,
|
||||
val syncClick: () -> Unit,
|
||||
|
@ -50,6 +51,9 @@ data class VaultItemListingHandlers(
|
|||
addVaultItemClick = {
|
||||
viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick)
|
||||
},
|
||||
collectionClick = {
|
||||
viewModel.trySendAction(VaultItemListingsAction.CollectionClick(it))
|
||||
},
|
||||
itemClick = { viewModel.trySendAction(VaultItemListingsAction.ItemClick(it)) },
|
||||
folderClick = { viewModel.trySendAction(VaultItemListingsAction.FolderClick(it)) },
|
||||
masterPasswordRepromptSubmit = { password, data ->
|
||||
|
|
|
@ -19,7 +19,9 @@ 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.getCollections
|
||||
import com.x8bit.bitwarden.ui.vault.feature.util.getFolders
|
||||
import com.x8bit.bitwarden.ui.vault.feature.util.toCollectionDisplayName
|
||||
import com.x8bit.bitwarden.ui.vault.feature.util.toFolderDisplayName
|
||||
import com.x8bit.bitwarden.ui.vault.feature.util.toLabelIcons
|
||||
import com.x8bit.bitwarden.ui.vault.feature.util.toOverflowActions
|
||||
|
@ -101,15 +103,20 @@ fun VaultData.toViewState(
|
|||
}
|
||||
.toFilteredList(vaultFilterType)
|
||||
|
||||
val folderList = if (itemListingType is VaultItemListingState.ItemListingType.Vault.Folder &&
|
||||
!itemListingType.folderId.isNullOrBlank()
|
||||
) {
|
||||
folderViewList.getFolders(itemListingType.folderId)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val folderList =
|
||||
(itemListingType as? VaultItemListingState.ItemListingType.Vault.Folder)
|
||||
?.folderId
|
||||
?.let { folderViewList.getFolders(it) }
|
||||
.orEmpty()
|
||||
|
||||
return if (folderList.isNotEmpty() || filteredCipherViewList.isNotEmpty()) {
|
||||
val collectionList =
|
||||
(itemListingType as? VaultItemListingState.ItemListingType.Vault.Collection)
|
||||
?.let { collectionViewList.getCollections(it.collectionId) }
|
||||
.orEmpty()
|
||||
|
||||
return if (folderList.isNotEmpty() || filteredCipherViewList.isNotEmpty() ||
|
||||
collectionList.isNotEmpty()
|
||||
) {
|
||||
VaultItemListingState.ViewState.Content(
|
||||
displayItemList = filteredCipherViewList.toDisplayItemList(
|
||||
baseIconUrl = baseIconUrl,
|
||||
|
@ -128,6 +135,18 @@ fun VaultData.toViewState(
|
|||
},
|
||||
)
|
||||
},
|
||||
displayCollectionList = collectionList.map { collectionView ->
|
||||
VaultItemListingState.CollectionDisplayItem(
|
||||
id = requireNotNull(collectionView.id),
|
||||
name = collectionView.name,
|
||||
count = this.cipherViewList
|
||||
.count {
|
||||
!it.id.isNullOrBlank() &&
|
||||
it.deletedDate == null &&
|
||||
collectionView.id in it.collectionIds
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
// Use the autofill empty message if necessary, otherwise use normal type-specific message
|
||||
|
@ -141,6 +160,10 @@ fun VaultData.toViewState(
|
|||
R.string.no_items_folder
|
||||
}
|
||||
|
||||
is VaultItemListingState.ItemListingType.Vault.Collection -> {
|
||||
R.string.no_items_collection
|
||||
}
|
||||
|
||||
VaultItemListingState.ItemListingType.Vault.Trash -> {
|
||||
R.string.no_items_trash
|
||||
}
|
||||
|
@ -177,6 +200,7 @@ fun List<SendView>.toViewState(
|
|||
clock = clock,
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
displayCollectionList = emptyList(),
|
||||
)
|
||||
} else {
|
||||
VaultItemListingState.ViewState.NoItems(
|
||||
|
@ -196,6 +220,7 @@ fun VaultItemListingState.ItemListingType.updateWithAdditionalDataIfNecessary(
|
|||
collectionName = collectionList
|
||||
.find { it.id == collectionId }
|
||||
?.name
|
||||
?.toCollectionDisplayName(collectionList)
|
||||
.orEmpty(),
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.util
|
||||
|
||||
import com.bitwarden.core.CollectionView
|
||||
|
||||
private const val COLLECTION_DIVIDER: String = "/"
|
||||
|
||||
/**
|
||||
* Retrieves the nested collections of a given [collectionId] and updates their names to proper
|
||||
* display names. This function is necessary if we want to show the nested collections for a
|
||||
* specific collection.
|
||||
*/
|
||||
@Suppress("ReturnCount")
|
||||
fun List<CollectionView>.getCollections(collectionId: String): List<CollectionView> {
|
||||
val currentCollection = this.find { it.id == collectionId } ?: return emptyList()
|
||||
|
||||
// If two collections have the same name the second collection should have no nested collections
|
||||
val firstCollectionWithName = this.first { it.name == currentCollection.name }
|
||||
if (firstCollectionWithName.id != collectionId) return emptyList()
|
||||
|
||||
val collectionList = this
|
||||
.getFilteredCollections(currentCollection.name)
|
||||
.map {
|
||||
it.copy(name = it.name.substringAfter(currentCollection.name + COLLECTION_DIVIDER))
|
||||
}
|
||||
|
||||
return collectionList
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out the nest collections of the nest collections from the given list. If a
|
||||
* [collectionName] is provided, collections that are not nested of the specified [collectionName]
|
||||
* will be filtered out.
|
||||
*/
|
||||
fun List<CollectionView>.getFilteredCollections(
|
||||
collectionName: String? = null,
|
||||
): List<CollectionView> =
|
||||
this.filter { collectionView ->
|
||||
// If the collection name is not null we filter out collections that are not nested
|
||||
// collections.
|
||||
if (collectionName != null &&
|
||||
!collectionView.name.startsWith(collectionName + COLLECTION_DIVIDER)
|
||||
) {
|
||||
return@filter false
|
||||
}
|
||||
|
||||
this.forEach {
|
||||
val firstCollection = collectionName
|
||||
?.let { name -> collectionView.name.substringAfter(name + COLLECTION_DIVIDER) }
|
||||
?: collectionView.name
|
||||
|
||||
val secondCollection = collectionName
|
||||
?.let { name -> it.name.substringAfter(name + COLLECTION_DIVIDER) }
|
||||
?: it.name
|
||||
|
||||
// We don't want to compare the collection to itself or itself plus a slash.
|
||||
if (firstCollection == secondCollection) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
// If the first collection name is blank or the first collection is a nested collection
|
||||
// of the second collection, we want to filter it out.
|
||||
if (firstCollection.isEmpty() ||
|
||||
firstCollection.startsWith(secondCollection + COLLECTION_DIVIDER)
|
||||
) {
|
||||
return@filter false
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a collection name to a user-friendly display name. This function is necessary because
|
||||
* the collection name we receive is often nested, and we want to extract just the relevant name for
|
||||
* display to the user.
|
||||
*/
|
||||
fun String.toCollectionDisplayName(list: List<CollectionView>): String {
|
||||
var collectionName = this
|
||||
|
||||
// cycle through the list and determine the correct display name of the collection.
|
||||
list.forEach { collection ->
|
||||
if (this.startsWith(collection.name + COLLECTION_DIVIDER)) {
|
||||
val newName = this.substringAfter(collection.name + COLLECTION_DIVIDER)
|
||||
if (newName.length < collectionName.length) {
|
||||
collectionName = newName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return collectionName
|
||||
}
|
|
@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
|||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.util.getFilteredCollections
|
||||
import com.x8bit.bitwarden.ui.vault.feature.util.getFilteredFolders
|
||||
import com.x8bit.bitwarden.ui.vault.feature.util.toLabelIcons
|
||||
import com.x8bit.bitwarden.ui.vault.feature.util.toOverflowActions
|
||||
|
@ -47,7 +48,10 @@ fun VaultData.toViewState(
|
|||
|
||||
val filteredFolderViewList = folderViewList.toFilteredList(vaultFilterType).getFilteredFolders()
|
||||
|
||||
val filteredCollectionViewList = collectionViewList.toFilteredList(vaultFilterType)
|
||||
val filteredCollectionViewList = collectionViewList
|
||||
.toFilteredList(vaultFilterType)
|
||||
.getFilteredCollections()
|
||||
|
||||
val noFolderItems = filteredCipherViewList
|
||||
.filter { it.folderId.isNullOrBlank() }
|
||||
|
||||
|
|
|
@ -5,12 +5,12 @@ import com.bitwarden.core.CollectionView
|
|||
/**
|
||||
* Create a mock [CollectionView] with a given [number].
|
||||
*/
|
||||
fun createMockCollectionView(number: Int): CollectionView =
|
||||
fun createMockCollectionView(number: Int, name: String? = null): CollectionView =
|
||||
CollectionView(
|
||||
id = "mockId-$number",
|
||||
organizationId = "mockOrganizationId-$number",
|
||||
hidePasswords = false,
|
||||
name = "mockName-$number",
|
||||
name = name ?: "mockName-$number",
|
||||
externalId = "mockExternalId-$number",
|
||||
readOnly = false,
|
||||
)
|
||||
|
|
|
@ -6,8 +6,6 @@ import androidx.compose.ui.test.assertIsNotDisplayed
|
|||
import androidx.compose.ui.test.assertTextEquals
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.hasScrollToNodeAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.isDisplayed
|
||||
import androidx.compose.ui.test.isPopup
|
||||
|
@ -15,7 +13,6 @@ import androidx.compose.ui.test.onAllNodesWithText
|
|||
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.compose.ui.test.performTextInput
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.R
|
||||
|
@ -39,6 +36,7 @@ import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
|||
import com.x8bit.bitwarden.ui.util.assertSwitcherIsDisplayed
|
||||
import com.x8bit.bitwarden.ui.util.assertSwitcherIsNotDisplayed
|
||||
import com.x8bit.bitwarden.ui.util.isProgressBar
|
||||
import com.x8bit.bitwarden.ui.util.onNodeWithTextAfterScroll
|
||||
import com.x8bit.bitwarden.ui.util.performAccountClick
|
||||
import com.x8bit.bitwarden.ui.util.performAccountIconClick
|
||||
import com.x8bit.bitwarden.ui.util.performAccountLongClick
|
||||
|
@ -566,14 +564,12 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
name = "test", id = "1", count = 0,
|
||||
),
|
||||
),
|
||||
displayCollectionList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(hasScrollToNodeAction())
|
||||
.performScrollToNode(hasText(folders))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = folders)
|
||||
.onNodeWithTextAfterScroll(text = folders)
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
|
@ -592,14 +588,12 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
displayFolderList = listOf(
|
||||
VaultItemListingState.FolderDisplayItem(name = "test", id = "1", count = 0),
|
||||
),
|
||||
displayCollectionList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(hasScrollToNodeAction())
|
||||
.performScrollToNode(hasText(folders))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = folders)
|
||||
.onNodeWithTextAfterScroll(text = folders)
|
||||
.assertIsDisplayed()
|
||||
.assertTextEquals(folders, 1.toString())
|
||||
|
||||
|
@ -624,15 +618,13 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
count = 0,
|
||||
),
|
||||
),
|
||||
displayCollectionList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNode(hasScrollToNodeAction())
|
||||
.performScrollToNode(hasText(folders))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = folders)
|
||||
.onNodeWithTextAfterScroll(text = folders)
|
||||
.assertIsDisplayed()
|
||||
.assertTextEquals(folders, 3.toString())
|
||||
}
|
||||
|
@ -648,6 +640,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = emptyList(),
|
||||
displayFolderList = listOf(
|
||||
VaultItemListingState.FolderDisplayItem(
|
||||
|
@ -665,6 +658,123 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Collections text should be displayed according to state`() {
|
||||
val collectionName = "Collections"
|
||||
mutableStateFlow.update { DEFAULT_STATE }
|
||||
composeTestRule
|
||||
.onNodeWithText(text = collectionName)
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = emptyList(),
|
||||
displayFolderList = emptyList(),
|
||||
displayCollectionList = listOf(
|
||||
VaultItemListingState.CollectionDisplayItem(
|
||||
name = "Collection",
|
||||
id = "1",
|
||||
count = 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(collectionName)
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Collection text count should be displayed according to state`() {
|
||||
val collections = "Collections"
|
||||
mutableStateFlow.update { DEFAULT_STATE }
|
||||
composeTestRule
|
||||
.onNodeWithText(text = collections)
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = emptyList(),
|
||||
displayFolderList = emptyList(),
|
||||
displayCollectionList = listOf(
|
||||
VaultItemListingState.CollectionDisplayItem(
|
||||
name = "Collection",
|
||||
id = "1",
|
||||
count = 0,
|
||||
),
|
||||
VaultItemListingState.CollectionDisplayItem(
|
||||
name = "Collection2",
|
||||
id = "2",
|
||||
count = 0,
|
||||
),
|
||||
VaultItemListingState.CollectionDisplayItem(
|
||||
name = "Collection3",
|
||||
id = "3",
|
||||
count = 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = collections)
|
||||
.assertIsDisplayed()
|
||||
.assertTextEquals(collections, 3.toString())
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = emptyList(),
|
||||
displayFolderList = emptyList(),
|
||||
displayCollectionList = listOf(
|
||||
VaultItemListingState.CollectionDisplayItem(
|
||||
name = "Collection",
|
||||
id = "1",
|
||||
count = 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = collections)
|
||||
.assertIsDisplayed()
|
||||
.assertTextEquals(collections, 1.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `collectionDisplayItems should be displayed according to state`() {
|
||||
val collectionName = "TestCollection"
|
||||
mutableStateFlow.update { DEFAULT_STATE }
|
||||
composeTestRule
|
||||
.onNodeWithText(text = collectionName)
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = emptyList(),
|
||||
displayFolderList = emptyList(),
|
||||
displayCollectionList = listOf(
|
||||
VaultItemListingState.CollectionDisplayItem(
|
||||
name = collectionName,
|
||||
id = "1",
|
||||
count = 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = collectionName)
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Items text should be displayed according to state`() {
|
||||
val items = "Items"
|
||||
|
@ -676,6 +786,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1),
|
||||
),
|
||||
|
@ -684,10 +795,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(hasScrollToNodeAction())
|
||||
.performScrollToNode(hasText(items))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = items)
|
||||
.onNodeWithTextAfterScroll(text = items)
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
|
@ -702,6 +810,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1),
|
||||
),
|
||||
|
@ -710,16 +819,14 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(hasScrollToNodeAction())
|
||||
.performScrollToNode(hasText(items))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = items)
|
||||
.onNodeWithTextAfterScroll(text = items)
|
||||
.assertIsDisplayed()
|
||||
.assertTextEquals(items, 1.toString())
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1),
|
||||
createDisplayItem(number = 2),
|
||||
|
@ -732,10 +839,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
composeTestRule
|
||||
.onNode(hasScrollToNodeAction())
|
||||
.performScrollToNode(hasText(items))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = items)
|
||||
.onNodeWithTextAfterScroll(text = items)
|
||||
.assertIsDisplayed()
|
||||
.assertTextEquals(items, 4.toString())
|
||||
}
|
||||
|
@ -745,6 +849,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1),
|
||||
),
|
||||
|
@ -767,6 +872,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1)
|
||||
.copy(
|
||||
|
@ -794,6 +900,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1)
|
||||
.copy(
|
||||
|
@ -845,6 +952,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1)
|
||||
.copy(
|
||||
|
@ -875,6 +983,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1)
|
||||
.copy(
|
||||
|
@ -1015,6 +1124,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
it.copy(
|
||||
itemListingType = VaultItemListingState.ItemListingType.Vault.Login,
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(createCipherDisplayItem(number = 1)),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
|
@ -1052,6 +1162,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
it.copy(
|
||||
itemListingType = VaultItemListingState.ItemListingType.Vault.Login,
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createCipherDisplayItem(number = 1)
|
||||
.copy(shouldShowMasterPasswordReprompt = true),
|
||||
|
@ -1083,6 +1194,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
it.copy(
|
||||
itemListingType = VaultItemListingState.ItemListingType.Send.SendFile,
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(createDisplayItem(number = 1)),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
|
@ -1109,6 +1221,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(createDisplayItem(number = number)),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
|
@ -1133,6 +1246,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(createDisplayItem(number = number)),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
|
@ -1222,6 +1336,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(createDisplayItem(number = 1)),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
|
|
|
@ -305,6 +305,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
} returns ValidatePasswordResult.Error
|
||||
val initialState = createVaultItemListingState(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(number = 1),
|
||||
),
|
||||
|
@ -354,6 +355,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
val initialState = createVaultItemListingState(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(number = 1),
|
||||
),
|
||||
|
@ -485,6 +487,17 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CollectionClick for vault item should emit NavigateToCollectionItem`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
val testId = "1"
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(VaultItemListingsAction.CollectionClick(testId))
|
||||
assertEquals(VaultItemListingEvent.NavigateToCollectionItem(testId), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RefreshClick should sync`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
@ -842,6 +855,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(
|
||||
createVaultItemListingState(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(number = 1),
|
||||
),
|
||||
|
@ -889,6 +903,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(
|
||||
createVaultItemListingState(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(number = 1).copy(isAutofill = true),
|
||||
),
|
||||
|
@ -990,6 +1005,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(
|
||||
createVaultItemListingState(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(number = 1),
|
||||
),
|
||||
|
@ -1097,6 +1113,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(
|
||||
createVaultItemListingState(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(number = 1),
|
||||
),
|
||||
|
@ -1211,6 +1228,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(
|
||||
createVaultItemListingState(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(number = 1),
|
||||
),
|
||||
|
|
|
@ -397,6 +397,7 @@ class VaultItemListingDataExtensionsTest {
|
|||
|
||||
assertEquals(
|
||||
VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(
|
||||
number = 1,
|
||||
|
@ -469,6 +470,7 @@ class VaultItemListingDataExtensionsTest {
|
|||
|
||||
assertEquals(
|
||||
VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(
|
||||
number = 1,
|
||||
|
@ -582,6 +584,7 @@ class VaultItemListingDataExtensionsTest {
|
|||
|
||||
assertEquals(
|
||||
VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createMockDisplayItemForSend(number = 1, sendType = SendType.FILE),
|
||||
createMockDisplayItemForSend(number = 2, sendType = SendType.TEXT),
|
||||
|
@ -683,7 +686,7 @@ class VaultItemListingDataExtensionsTest {
|
|||
@Test
|
||||
fun `toViewState should properly filter and return the correct folders`() {
|
||||
val vaultData = VaultData(
|
||||
listOf(createMockCipherView(number = 1)),
|
||||
cipherViewList = listOf(createMockCipherView(number = 1)),
|
||||
collectionViewList = emptyList(),
|
||||
folderViewList = listOf(
|
||||
FolderView("1", "test", clock.instant()),
|
||||
|
@ -705,6 +708,7 @@ class VaultItemListingDataExtensionsTest {
|
|||
|
||||
assertEquals(
|
||||
VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(),
|
||||
displayFolderList = listOf(
|
||||
VaultItemListingState.FolderDisplayItem(
|
||||
|
@ -717,4 +721,48 @@ class VaultItemListingDataExtensionsTest {
|
|||
actual,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should properly filter and return the correct collections`() {
|
||||
val vaultData = VaultData(
|
||||
cipherViewList = emptyList(),
|
||||
collectionViewList = listOf(
|
||||
createMockCollectionView(1, "test"),
|
||||
createMockCollectionView(2, "test/test"),
|
||||
createMockCollectionView(3, "Collection/test"),
|
||||
createMockCollectionView(4, "test/Collection"),
|
||||
createMockCollectionView(5, "Collection"),
|
||||
),
|
||||
folderViewList = emptyList(),
|
||||
sendViewList = emptyList(),
|
||||
)
|
||||
|
||||
val actual = vaultData.toViewState(
|
||||
isIconLoadingDisabled = false,
|
||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||
autofillSelectionData = null,
|
||||
itemListingType = VaultItemListingState.ItemListingType.Vault.Collection("mockId-1"),
|
||||
vaultFilterType = VaultFilterType.AllVaults,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = listOf(
|
||||
VaultItemListingState.CollectionDisplayItem(
|
||||
id = "mockId-2",
|
||||
name = "test",
|
||||
count = 0,
|
||||
),
|
||||
VaultItemListingState.CollectionDisplayItem(
|
||||
id = "mockId-4",
|
||||
name = "Collection",
|
||||
count = 0,
|
||||
),
|
||||
),
|
||||
displayItemList = emptyList(),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
actual,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.util
|
||||
|
||||
import com.bitwarden.core.CollectionView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class CollectionViewExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `getCollections should get the collections for a collectionId with the correct names`() {
|
||||
val collectionList: List<CollectionView> = listOf(
|
||||
createMockCollectionView(number = 1, name = "test"),
|
||||
createMockCollectionView(number = 2, name = "test/test"),
|
||||
createMockCollectionView(number = 3, name = "test/Collection"),
|
||||
createMockCollectionView(number = 4, name = "test/test/test"),
|
||||
createMockCollectionView(number = 5, name = "Collection"),
|
||||
)
|
||||
|
||||
val expected = listOf(
|
||||
createMockCollectionView(number = 2, name = "test"),
|
||||
createMockCollectionView(number = 3, name = "Collection"),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected,
|
||||
collectionList.getCollections("mockId-1"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFilteredCollections should properly filter out sub collections in a list`() {
|
||||
val collectionList: List<CollectionView> = listOf(
|
||||
createMockCollectionView(number = 1, name = "test"),
|
||||
createMockCollectionView(number = 2, name = "test/test"),
|
||||
createMockCollectionView(number = 3, name = "test/Collection"),
|
||||
createMockCollectionView(number = 4, name = "test/test/test"),
|
||||
createMockCollectionView(number = 5, name = "Collection"),
|
||||
)
|
||||
|
||||
val expected = listOf(
|
||||
createMockCollectionView(number = 1, name = "test"),
|
||||
createMockCollectionView(number = 5, name = "Collection"),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected,
|
||||
collectionList.getFilteredCollections(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toCollectionDisplayName should return the correct name`() {
|
||||
val collectionName = "Collection/test/2"
|
||||
|
||||
val collectionList: List<CollectionView> = listOf(
|
||||
createMockCollectionView(number = 1, name = "Collection/test"),
|
||||
createMockCollectionView(number = 2, name = "test/test"),
|
||||
createMockCollectionView(number = 3, name = "test/Collection"),
|
||||
createMockCollectionView(number = 4, name = collectionName),
|
||||
createMockCollectionView(number = 5, name = "Collection"),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"2",
|
||||
collectionName.toCollectionDisplayName(collectionList),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -494,10 +494,16 @@ class VaultDataExtensionsTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should properly filter nested folders out`() {
|
||||
fun `toViewState should properly filter nested items out`() {
|
||||
val vaultData = VaultData(
|
||||
listOf(createMockCipherView(number = 1)),
|
||||
collectionViewList = emptyList(),
|
||||
collectionViewList = listOf(
|
||||
createMockCollectionView(1, "test"),
|
||||
createMockCollectionView(2, "test/test"),
|
||||
createMockCollectionView(3, "Collection/test"),
|
||||
createMockCollectionView(4, "test/Collection"),
|
||||
createMockCollectionView(5, "Collection"),
|
||||
),
|
||||
folderViewList = listOf(
|
||||
FolderView("1", "test", clock.instant()),
|
||||
FolderView("2", "test/test", clock.instant()),
|
||||
|
@ -522,6 +528,18 @@ class VaultDataExtensionsTest {
|
|||
identityItemsCount = 0,
|
||||
secureNoteItemsCount = 0,
|
||||
favoriteItems = listOf(),
|
||||
collectionItems = listOf(
|
||||
VaultState.ViewState.CollectionItem(
|
||||
id = "mockId-1",
|
||||
name = "test",
|
||||
itemCount = 1,
|
||||
),
|
||||
VaultState.ViewState.CollectionItem(
|
||||
id = "mockId-5",
|
||||
name = "Collection",
|
||||
itemCount = 0,
|
||||
),
|
||||
),
|
||||
folderItems = listOf(
|
||||
VaultState.ViewState.FolderItem(
|
||||
id = "1",
|
||||
|
@ -540,7 +558,6 @@ class VaultDataExtensionsTest {
|
|||
),
|
||||
|
||||
),
|
||||
collectionItems = listOf(),
|
||||
noFolderItems = listOf(),
|
||||
trashItemsCount = 0,
|
||||
totpItemsCount = 1,
|
||||
|
|
Loading…
Reference in a new issue