BIT-852: Add account switcher UI (#235)

This commit is contained in:
Brian Yencho
2023-11-10 11:54:30 -06:00
committed by GitHub
parent b9d60e8a04
commit 830112c070
14 changed files with 914 additions and 82 deletions

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.vault
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasClickAction
@@ -9,14 +10,17 @@ 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.data.auth.repository.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
@@ -24,6 +28,7 @@ import org.junit.Test
class VaultScreenTest : BaseComposeTest() {
private var onNavigateToVaultAddItemScreenCalled = false
private var onDimBottomNavBarRequestCalled = false
private val mutableEventFlow = MutableSharedFlow<VaultEvent>(
extraBufferCapacity = Int.MAX_VALUE,
@@ -40,10 +45,49 @@ class VaultScreenTest : BaseComposeTest() {
VaultScreen(
viewModel = viewModel,
onNavigateToVaultAddItemScreen = { onNavigateToVaultAddItemScreenCalled = true },
onDimBottomNavBarRequest = { onDimBottomNavBarRequestCalled = true },
)
}
}
@Suppress("MaxLineLength")
@Test
fun `account icon click should show the account switcher and trigger the nav bar dim request`() {
composeTestRule.onNodeWithText("active@bitwarden.com").assertDoesNotExist()
composeTestRule.onNodeWithText("locked@bitwarden.com").assertDoesNotExist()
composeTestRule.onNodeWithText("Add account").assertDoesNotExist()
assertFalse(onDimBottomNavBarRequestCalled)
composeTestRule.onNodeWithText("AU").performClick()
composeTestRule.onNodeWithText("active@bitwarden.com").assertIsDisplayed()
composeTestRule.onNodeWithText("locked@bitwarden.com").assertIsDisplayed()
composeTestRule.onNodeWithText("Add account").assertIsDisplayed()
assertTrue(onDimBottomNavBarRequestCalled)
}
@Suppress("MaxLineLength")
@Test
fun `account click in the account switcher should send AccountSwitchClick and close switcher`() {
// Open the Account Switcher
composeTestRule.onNodeWithText("AU").performClick()
composeTestRule.onNodeWithText("locked@bitwarden.com").performClick()
verify { viewModel.trySendAction(VaultAction.AccountSwitchClick(LOCKED_ACCOUNT_SUMMARY)) }
composeTestRule.onNodeWithText("locked@bitwarden.com").assertDoesNotExist()
}
@Suppress("MaxLineLength")
@Test
fun `Add Account click in the account switcher should send AddAccountClick and close switcher`() {
// Open the Account Switcher
composeTestRule.onNodeWithText("AU").performClick()
composeTestRule.onNodeWithText("Add account").performClick()
verify { viewModel.trySendAction(VaultAction.AddAccountClick) }
composeTestRule.onNodeWithText("Add account").assertDoesNotExist()
}
@Test
fun `search icon click should send SearchIconClick action`() {
mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.NoItems) }
@@ -357,9 +401,29 @@ class VaultScreenTest : BaseComposeTest() {
}
}
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
userId = "activeUserId",
name = "Active User",
email = "active@bitwarden.com",
avatarColorHex = "#aa00aa",
status = AccountSummary.Status.ACTIVE,
)
private val LOCKED_ACCOUNT_SUMMARY = AccountSummary(
userId = "lockedUserId",
name = "Locked User",
email = "locked@bitwarden.com",
avatarColorHex = "#00aaaa",
status = AccountSummary.Status.LOCKED,
)
private val DEFAULT_STATE: VaultState = VaultState(
avatarColorString = "FF0000FF",
initials = "BW",
avatarColorString = "#aa00aa",
initials = "AU",
accountSummaries = persistentListOf(
ACTIVE_ACCOUNT_SUMMARY,
LOCKED_ACCOUNT_SUMMARY,
),
viewState = VaultState.ViewState.Loading,
)

View File

@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.vault
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.data.auth.repository.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import io.mockk.every
import io.mockk.mockk
@@ -27,6 +28,58 @@ class VaultViewModelTest : BaseViewModelTest() {
assertEquals(state, viewModel.stateFlow.value)
}
@Test
fun `on AccountSwitchClick for the active account should do nothing`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
VaultAction.AccountSwitchClick(
accountSummary = mockk {
every { status } returns AccountSummary.Status.ACTIVE
},
)
expectNoEvents()
}
}
@Test
fun `on AccountSwitchClick for a locked account emit NavigateToVaultUnlockScreen`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(
VaultAction.AccountSwitchClick(
accountSummary = mockk {
every { status } returns AccountSummary.Status.LOCKED
},
),
)
assertEquals(VaultEvent.NavigateToVaultUnlockScreen, awaitItem())
}
}
@Test
fun `on AccountSwitchClick for an unlocked account emit ShowToast`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(
VaultAction.AccountSwitchClick(
accountSummary = mockk {
every { status } returns AccountSummary.Status.UNLOCKED
},
),
)
assertEquals(VaultEvent.ShowToast("Not yet implemented."), awaitItem())
}
}
@Test
fun `on AddAccountClick should emit NavigateToLoginScreen`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(VaultAction.AddAccountClick)
assertEquals(VaultEvent.NavigateToLoginScreen, awaitItem())
}
}
@Test
fun `AddItemClick should emit NavigateToAddItemScreen`() = runTest {
val viewModel = createViewModel()
@@ -126,5 +179,6 @@ class VaultViewModelTest : BaseViewModelTest() {
private val DEFAULT_STATE: VaultState = VaultState(
avatarColorString = "FF0000FF",
initials = "BW",
accountSummaries = emptyList(),
viewState = VaultState.ViewState.Loading,
)

View File

@@ -0,0 +1,88 @@
package com.x8bit.bitwarden.ui.vault.feature.vault.util
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.model.AccountSummary
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
class AccountSummaryExtensionsTest {
@Test
fun `initials should return the starting letters of the first two words in the name`() {
assertEquals(
"FS",
mockk<AccountSummary>() {
every { name } returns "First Second Third"
}
.initials,
)
}
@Test
fun `iconRes returns a checkmark for active accounts`() {
assertEquals(
R.drawable.ic_check_mark,
mockk<AccountSummary>() {
every { status } returns AccountSummary.Status.ACTIVE
}
.iconRes,
)
}
@Test
fun `iconRes returns a locked lock for locked accounts`() {
assertEquals(
R.drawable.ic_locked,
mockk<AccountSummary>() {
every { status } returns AccountSummary.Status.LOCKED
}
.iconRes,
)
}
@Test
fun `iconRes returns an unlocked lock for unlocked accounts`() {
assertEquals(
R.drawable.ic_unlocked,
mockk<AccountSummary>() {
every { status } returns AccountSummary.Status.UNLOCKED
}
.iconRes,
)
}
@Test
fun `supportingTextResOrNull returns a null for active accounts`() {
assertNull(
mockk<AccountSummary>() {
every { status } returns AccountSummary.Status.ACTIVE
}
.supportingTextResOrNull,
)
}
@Test
fun `supportingTextResOrNull returns Locked locked accounts`() {
assertEquals(
R.string.account_locked,
mockk<AccountSummary>() {
every { status } returns AccountSummary.Status.LOCKED
}
.supportingTextResOrNull,
)
}
@Test
fun `supportingTextResOrNull returns Unlocked for unlocked accounts`() {
assertEquals(
R.string.account_unlocked,
mockk<AccountSummary>() {
every { status } returns AccountSummary.Status.UNLOCKED
}
.supportingTextResOrNull,
)
}
}