PM-8522: Vault tab bar title for organization users (#3632)

This commit is contained in:
Shannon Draeker 2024-07-29 14:19:08 -06:00 committed by GitHub
parent 58a91c15aa
commit 0e90bbb905
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 111 additions and 9 deletions

View file

@ -32,6 +32,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
@ -88,6 +89,8 @@ fun VaultUnlockedNavBarScreen(
onNavigateToPendingRequests: () -> Unit,
onNavigateToPasswordHistory: () -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel = viewModel) { event ->
navController.apply {
val navOptions = vaultUnlockedNavBarScreenNavOptions()
@ -120,6 +123,7 @@ fun VaultUnlockedNavBarScreen(
}
VaultUnlockedNavBarScaffold(
state = state,
navController = navController,
onNavigateToVaultItem = onNavigateToVaultItem,
onNavigateToVaultEditItem = onNavigateToVaultEditItem,
@ -155,6 +159,7 @@ fun VaultUnlockedNavBarScreen(
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod")
private fun VaultUnlockedNavBarScaffold(
state: VaultUnlockedNavBarState,
navController: NavHostController,
vaultTabClickedAction: () -> Unit,
sendTabClickedAction: () -> Unit,
@ -184,6 +189,7 @@ private fun VaultUnlockedNavBarScaffold(
Box {
var appBarHeightPx by remember { mutableIntStateOf(0) }
VaultBottomAppBar(
state = state,
navController = navController,
vaultTabClickedAction = vaultTabClickedAction,
sendTabClickedAction = sendTabClickedAction,
@ -254,6 +260,7 @@ private fun VaultUnlockedNavBarScaffold(
@Suppress("LongMethod")
@Composable
private fun VaultBottomAppBar(
state: VaultUnlockedNavBarState,
navController: NavHostController,
vaultTabClickedAction: () -> Unit,
sendTabClickedAction: () -> Unit,
@ -266,7 +273,10 @@ private fun VaultBottomAppBar(
modifier = modifier,
) {
val destinations = listOf(
VaultUnlockedNavBarTab.Vault,
VaultUnlockedNavBarTab.Vault(
labelRes = state.vaultNavBarLabelRes,
contentDescriptionRes = state.vaultNavBarContentDescriptionRes,
),
VaultUnlockedNavBarTab.Send,
VaultUnlockedNavBarTab.Generator,
VaultUnlockedNavBarTab.Settings,
@ -303,7 +313,7 @@ private fun VaultBottomAppBar(
selected = isSelected,
onClick = {
when (destination) {
VaultUnlockedNavBarTab.Vault -> vaultTabClickedAction()
is VaultUnlockedNavBarTab.Vault -> vaultTabClickedAction()
VaultUnlockedNavBarTab.Send -> sendTabClickedAction()
VaultUnlockedNavBarTab.Generator -> generatorTabClickedAction()
VaultUnlockedNavBarTab.Settings -> settingsTabClickedAction()
@ -396,11 +406,12 @@ private sealed class VaultUnlockedNavBarTab : Parcelable {
* Show the Vault screen.
*/
@Parcelize
data object Vault : VaultUnlockedNavBarTab() {
data class Vault(
override val labelRes: Int,
override val contentDescriptionRes: Int,
) : VaultUnlockedNavBarTab() {
override val iconResSelected get() = R.drawable.ic_vault_filled
override val iconRes get() = R.drawable.ic_vault
override val labelRes get() = R.string.my_vault
override val contentDescriptionRes get() = R.string.my_vault
override val route get() = VAULT_GRAPH_ROUTE
override val testTag get() = "VaultTab"
}

View file

@ -1,5 +1,7 @@
package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar
import androidx.annotation.StringRes
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
@ -14,8 +16,21 @@ import javax.inject.Inject
class VaultUnlockedNavBarViewModel @Inject constructor(
private val authRepository: AuthRepository,
specialCircumstancesManager: SpecialCircumstanceManager,
) : BaseViewModel<Unit, VaultUnlockedNavBarEvent, VaultUnlockedNavBarAction>(
initialState = Unit,
) : BaseViewModel<VaultUnlockedNavBarState, VaultUnlockedNavBarEvent, VaultUnlockedNavBarAction>(
initialState = run {
val hasOrganization = authRepository
.userStateFlow
.value
?.activeAccount
?.organizations
?.isNotEmpty()
?: false
val vaultRes = if (hasOrganization) R.string.vaults else R.string.my_vault
VaultUnlockedNavBarState(
vaultNavBarLabelRes = vaultRes,
vaultNavBarContentDescriptionRes = vaultRes,
)
},
) {
init {
when (specialCircumstancesManager.specialCircumstance) {
@ -77,6 +92,14 @@ class VaultUnlockedNavBarViewModel @Inject constructor(
// #endregion BottomTabViewModel Action Handlers
}
/**
* Models state for the [VaultUnlockedNavBarViewModel].
*/
data class VaultUnlockedNavBarState(
@StringRes val vaultNavBarLabelRes: Int,
@StringRes val vaultNavBarContentDescriptionRes: Int,
)
/**
* Models actions for the bottom tab of the vault unlocked portion of the app.
*/

View file

@ -31,7 +31,7 @@ sealed class VaultFilterType : Parcelable {
}
/**
* Only data from the user's personal vault shoudl be present.
* Only data from the user's personal vault should be present.
*/
@Parcelize
data object MyVault : VaultFilterType() {

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.navigation.navOptions
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.FakeNavHostController
@ -16,7 +17,7 @@ import org.junit.Test
class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
private val fakeNavHostController = FakeNavHostController()
private val mutableEventFlow = bufferedMutableSharedFlow<VaultUnlockedNavBarEvent>()
private val mutableStateFlow = MutableStateFlow(Unit)
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
val viewModel = mockk<VaultUnlockedNavBarViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow
@ -133,4 +134,25 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
}
}
}
@Test
fun `vault nav bar should update according to state`() {
composeTestRule.onNodeWithText("My vault").assertExists()
composeTestRule.onNodeWithText("Vaults").assertDoesNotExist()
mutableStateFlow.tryEmit(
VaultUnlockedNavBarState(
vaultNavBarLabelRes = R.string.vaults,
vaultNavBarContentDescriptionRes = R.string.vaults,
),
)
composeTestRule.onNodeWithText("My vault").assertDoesNotExist()
composeTestRule.onNodeWithText("Vaults").assertExists()
}
}
private val DEFAULT_STATE = VaultUnlockedNavBarState(
vaultNavBarLabelRes = R.string.my_vault,
vaultNavBarContentDescriptionRes = R.string.my_vault,
)

View file

@ -1,7 +1,9 @@
package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar
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.UserState
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
@ -10,12 +12,15 @@ import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() {
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
private val authRepository: AuthRepository = mockk {
every { userStateFlow } returns mutableUserStateFlow
every { updateLastActiveTime() } just runs
}
private val specialCircumstancesManager: SpecialCircumstanceManager = mockk {
@ -78,6 +83,47 @@ class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `on init with no organizations should set correct vault label res`() = runTest {
val viewModel = createViewModel()
val expected = VaultUnlockedNavBarState(
vaultNavBarLabelRes = R.string.my_vault,
vaultNavBarContentDescriptionRes = R.string.my_vault,
)
viewModel.stateFlow.test {
assertEquals(
expected,
awaitItem(),
)
}
}
@Test
fun `on init with organizations should set correct vault label res`() = runTest {
val activeUserId = "activeUserId"
val account: UserState.Account = mockk {
every { userId } returns activeUserId
every { organizations } returns listOf(mockk())
}
mutableUserStateFlow.value = UserState(
activeUserId = activeUserId,
accounts = listOf(account),
)
val viewModel = createViewModel()
val expected = VaultUnlockedNavBarState(
vaultNavBarLabelRes = R.string.vaults,
vaultNavBarContentDescriptionRes = R.string.vaults,
)
viewModel.stateFlow.test {
assertEquals(
expected,
awaitItem(),
)
}
}
@Test
fun `VaultTabClick should navigate to the vault screen`() = runTest {
val viewModel = createViewModel()