mirror of
https://github.com/bitwarden/android.git
synced 2026-05-31 01:22:39 -05:00
BIT-842, BIT-843: Add Vault Filter and Vault Selection menu UI (#448)
This commit is contained in:
@@ -14,6 +14,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 com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
@@ -32,6 +33,8 @@ import com.x8bit.bitwarden.ui.util.performAddAccountClick
|
||||
import com.x8bit.bitwarden.ui.util.performLockAccountClick
|
||||
import com.x8bit.bitwarden.ui.util.performLogoutAccountClick
|
||||
import com.x8bit.bitwarden.ui.util.performLogoutAccountConfirmationClick
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
@@ -80,6 +83,135 @@ class VaultScreenTest : BaseComposeTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `app bar title should update according to state`() {
|
||||
composeTestRule.onNodeWithText("My vault").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("Vaults").assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(appBarTitle = R.string.vaults.asText())
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("My vault").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText("Vaults").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vault filter should update according to state`() {
|
||||
composeTestRule.onNodeWithText("Vault: All").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText("Vault: My vault").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText("Vault: Test Organization").assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
vaultFilterData = VAULT_FILTER_DATA,
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE,
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("Vault: All").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("Vault: My vault").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText("Vault: Test Organization").assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
vaultFilterData = VAULT_FILTER_DATA.copy(
|
||||
selectedVaultFilterType = VaultFilterType.MyVault,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("Vault: All").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText("Vault: My vault").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("Vault: Test Organization").assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
vaultFilterData = VAULT_FILTER_DATA.copy(
|
||||
selectedVaultFilterType = ORGANIZATION_VAULT_FILTER,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("Vault: All").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText("Vault: My vault").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText("Vault: Test Organization").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vault filter menu click should display the filter selection dialog`() {
|
||||
// Display the vault filter
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
vaultFilterData = VAULT_FILTER_DATA,
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE,
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
composeTestRule.onNodeWithContentDescription("Filter items by vault").performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("All vaults")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("My vault")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Test Organization")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cancel click in the filter selection dialog should close the dialog`() {
|
||||
// Display the vault selection dialog
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
vaultFilterData = VAULT_FILTER_DATA,
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE,
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithContentDescription("Filter items by vault").performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `vault filter click in the filter selection dialog should send VaultFilterTypeSelect and close the dialog`() {
|
||||
// Display the vault selection dialog
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
vaultFilterData = VAULT_FILTER_DATA,
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE,
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithContentDescription("Filter items by vault").performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("All vaults")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultAction.VaultFilterTypeSelect(VaultFilterType.AllVaults))
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `account icon click should show the account switcher and trigger the nav bar dim request`() {
|
||||
@@ -858,6 +990,20 @@ private val LOCKED_ACCOUNT_SUMMARY = AccountSummary(
|
||||
isVaultUnlocked = false,
|
||||
)
|
||||
|
||||
private val ORGANIZATION_VAULT_FILTER = VaultFilterType.OrganizationVault(
|
||||
organizationId = "testOrganizationId",
|
||||
organizationName = "Test Organization",
|
||||
)
|
||||
|
||||
private val VAULT_FILTER_DATA = VaultFilterData(
|
||||
selectedVaultFilterType = VaultFilterType.AllVaults,
|
||||
vaultFilterTypes = listOf(
|
||||
VaultFilterType.AllVaults,
|
||||
VaultFilterType.MyVault,
|
||||
ORGANIZATION_VAULT_FILTER,
|
||||
),
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE: VaultState = VaultState(
|
||||
avatarColorString = "#aa00aa",
|
||||
initials = "AU",
|
||||
@@ -865,6 +1011,7 @@ private val DEFAULT_STATE: VaultState = VaultState(
|
||||
ACTIVE_ACCOUNT_SUMMARY,
|
||||
LOCKED_ACCOUNT_SUMMARY,
|
||||
),
|
||||
appBarTitle = R.string.my_vault.asText(),
|
||||
viewState = VaultState.ViewState.Loading,
|
||||
)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.vault.feature.vault
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState.SpecialCircumstance
|
||||
@@ -16,6 +17,8 @@ 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.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
@@ -126,13 +129,19 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isVaultUnlocked = true,
|
||||
organizations = emptyList(),
|
||||
organizations = listOf(
|
||||
Organization(
|
||||
id = "organiationId",
|
||||
name = "Test Organization",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
appBarTitle = R.string.vaults.asText(),
|
||||
avatarColorString = "#00aaaa",
|
||||
initials = "OU",
|
||||
accountSummaries = listOf(
|
||||
@@ -146,6 +155,17 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
isVaultUnlocked = true,
|
||||
),
|
||||
),
|
||||
vaultFilterData = VaultFilterData(
|
||||
selectedVaultFilterType = VaultFilterType.AllVaults,
|
||||
vaultFilterTypes = listOf(
|
||||
VaultFilterType.AllVaults,
|
||||
VaultFilterType.MyVault,
|
||||
VaultFilterType.OrganizationVault(
|
||||
organizationId = "organiationId",
|
||||
organizationName = "Test Organization",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
@@ -286,6 +306,46 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on VaultFilterTypeSelect should update the selected filter type`() {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
// Update to state with filters
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
appBarTitle = R.string.vaults.asText(),
|
||||
vaultFilterData = VAULT_FILTER_DATA,
|
||||
)
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE
|
||||
.copy(
|
||||
accounts = listOf(
|
||||
DEFAULT_USER_STATE.accounts[0].copy(
|
||||
organizations = listOf(
|
||||
Organization(
|
||||
id = "testOrganizationId",
|
||||
name = "Test Organization",
|
||||
),
|
||||
),
|
||||
),
|
||||
DEFAULT_USER_STATE.accounts[1],
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
initialState,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
viewModel.trySendAction(VaultAction.VaultFilterTypeSelect(VaultFilterType.MyVault))
|
||||
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
vaultFilterData = VAULT_FILTER_DATA.copy(
|
||||
selectedVaultFilterType = VaultFilterType.MyVault,
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultDataStateFlow Loaded with items should update state to Content`() = runTest {
|
||||
mutableVaultDataStateFlow.tryEmit(
|
||||
@@ -760,6 +820,20 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
private val ORGANIZATION_VAULT_FILTER = VaultFilterType.OrganizationVault(
|
||||
organizationId = "testOrganizationId",
|
||||
organizationName = "Test Organization",
|
||||
)
|
||||
|
||||
private val VAULT_FILTER_DATA = VaultFilterData(
|
||||
selectedVaultFilterType = VaultFilterType.AllVaults,
|
||||
vaultFilterTypes = listOf(
|
||||
VaultFilterType.AllVaults,
|
||||
VaultFilterType.MyVault,
|
||||
ORGANIZATION_VAULT_FILTER,
|
||||
),
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE: VaultState =
|
||||
createMockVaultState(viewState = VaultState.ViewState.Loading)
|
||||
|
||||
@@ -794,6 +868,7 @@ private fun createMockVaultState(
|
||||
dialog: VaultState.DialogState? = null,
|
||||
): VaultState =
|
||||
VaultState(
|
||||
appBarTitle = R.string.my_vault.asText(),
|
||||
avatarColorString = "#aa00aa",
|
||||
initials = "AU",
|
||||
accountSummaries = listOf(
|
||||
|
||||
@@ -5,7 +5,10 @@ import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class UserStateExtensionsTest {
|
||||
@@ -197,4 +200,63 @@ class UserStateExtensionsTest {
|
||||
.toActiveAccountSummary(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toVaultFilterData for an account with no organizations should return a null value`() {
|
||||
assertNull(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "name",
|
||||
email = "email",
|
||||
avatarColorHex = "avatarColorHex",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isVaultUnlocked = true,
|
||||
organizations = emptyList(),
|
||||
)
|
||||
.toVaultFilterData(),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toVaultFilterData for an account with organizations should return data with the available types in the correct order`() {
|
||||
assertEquals(
|
||||
VaultFilterData(
|
||||
selectedVaultFilterType = VaultFilterType.AllVaults,
|
||||
vaultFilterTypes = listOf(
|
||||
VaultFilterType.AllVaults,
|
||||
VaultFilterType.MyVault,
|
||||
VaultFilterType.OrganizationVault(
|
||||
organizationId = "organizationId-A",
|
||||
organizationName = "Organization A",
|
||||
),
|
||||
VaultFilterType.OrganizationVault(
|
||||
organizationId = "organizationId-B",
|
||||
organizationName = "Organization B",
|
||||
),
|
||||
),
|
||||
),
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "name",
|
||||
email = "email",
|
||||
avatarColorHex = "avatarColorHex",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isVaultUnlocked = true,
|
||||
organizations = listOf(
|
||||
Organization(
|
||||
id = "organizationId-B",
|
||||
name = "Organization B",
|
||||
),
|
||||
Organization(
|
||||
id = "organizationId-A",
|
||||
name = "Organization A",
|
||||
),
|
||||
),
|
||||
)
|
||||
.toVaultFilterData(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.vault.util
|
||||
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class VaultFilterDataExtensionsTest {
|
||||
@Test
|
||||
fun `toAppBarTitle for a null value should return My Vault`() {
|
||||
assertEquals(
|
||||
R.string.my_vault.asText(),
|
||||
(null as VaultFilterData?).toAppBarTitle(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAppBarTitle for a non-null value should return Vaults`() {
|
||||
assertEquals(
|
||||
R.string.vaults.asText(),
|
||||
VaultFilterData(
|
||||
selectedVaultFilterType = VaultFilterType.MyVault,
|
||||
vaultFilterTypes = listOf(
|
||||
VaultFilterType.AllVaults,
|
||||
VaultFilterType.MyVault,
|
||||
),
|
||||
)
|
||||
.toAppBarTitle(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user