BIT-842, BIT-843: Add Vault Filter and Vault Selection menu UI (#448)

This commit is contained in:
Brian Yencho
2023-12-28 16:33:38 -06:00
committed by GitHub
parent 274cf3e415
commit 90c9f28e1d
11 changed files with 631 additions and 30 deletions

View File

@@ -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,
)

View File

@@ -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(

View File

@@ -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(),
)
}
}

View File

@@ -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(),
)
}
}