BIT-1054, BIT-1055: Adding modal generator UI and navigation from Add/Edit item (#643)

This commit is contained in:
Joshua Queen
2024-01-17 13:20:38 -05:00
committed by GitHub
parent 5d388bffe7
commit 59732a4ba1
10 changed files with 379 additions and 54 deletions

View File

@@ -5,6 +5,7 @@ import com.bitwarden.core.PasswordGeneratorRequest
import com.bitwarden.core.PasswordHistoryView
import com.bitwarden.core.UsernameGeneratorRequest
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedCatchAllUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedForwardedServiceUsernameResult
@@ -12,10 +13,13 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassph
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPlusAddressedUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
/**
* A fake implementation of [GeneratorRepository] for testing purposes.
@@ -36,6 +40,8 @@ class FakeGeneratorRepository : GeneratorRepository {
private val mutablePasswordHistoryStateFlow =
MutableStateFlow<LocalDataState<List<PasswordHistoryView>>>(LocalDataState.Loading)
private val mutableGeneratorResultFlow = bufferedMutableSharedFlow<GeneratorResult>()
private var generatePlusAddressedEmailResult: GeneratedPlusAddressedUsernameResult =
GeneratedPlusAddressedUsernameResult.Success(
generatedEmailAddress = "email+abcd1234@address.com",
@@ -59,6 +65,13 @@ class FakeGeneratorRepository : GeneratorRepository {
override val passwordHistoryStateFlow: StateFlow<LocalDataState<List<PasswordHistoryView>>>
get() = mutablePasswordHistoryStateFlow
override val generatorResultFlow: Flow<GeneratorResult>
get() = mutableGeneratorResultFlow.asSharedFlow()
override fun emitGeneratorResult(generatorResult: GeneratorResult) {
mutableGeneratorResultFlow.tryEmit(generatorResult)
}
override suspend fun generatePassword(
passwordGeneratorRequest: PasswordGeneratorRequest,
shouldSave: Boolean,

View File

@@ -27,6 +27,7 @@ import androidx.compose.ui.text.AnnotatedString
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
@@ -58,6 +59,85 @@ class GeneratorScreenTest : BaseComposeTest() {
}
}
@Test
fun `ModalAppBar should be displayed for Password Mode`() {
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Modal.Password))
composeTestRule
.onNodeWithContentDescription(label = "Close")
.assertIsDisplayed()
composeTestRule
.onNodeWithText(text = "Select")
.assertIsDisplayed()
}
@Test
fun `ModalAppBar should be displayed for Username Mode`() {
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Modal.Username))
composeTestRule
.onNodeWithContentDescription(label = "Close")
.assertIsDisplayed()
composeTestRule
.onNodeWithText(text = "Select")
.assertIsDisplayed()
}
@Test
fun `on close click should send CloseClick`() {
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Modal.Username))
composeTestRule
.onNodeWithContentDescription(label = "Close")
.performClick()
verify {
viewModel.trySendAction(GeneratorAction.CloseClick)
}
}
@Test
fun `on select click should send SelectClick`() {
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Modal.Username))
composeTestRule
.onNodeWithText(text = "Select")
.performClick()
verify {
viewModel.trySendAction(GeneratorAction.SelectClick)
}
}
@Test
fun `DefaultAppBar should be displayed for Default Mode`() {
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Default))
composeTestRule
.onNodeWithContentDescription(label = "More")
.assertIsDisplayed()
}
@Test
fun `MainTypeOption select control should be hidden for password mode`() {
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Modal.Password))
composeTestRule
.onNodeWithContentDescription(label = "What would you like to generate?, Password")
.assertDoesNotExist()
}
@Test
fun `MainTypeOption select control should be hidden for username mode`() {
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Modal.Username))
composeTestRule
.onNodeWithContentDescription(label = "What would you like to generate?, Password")
.assertDoesNotExist()
}
@Test
fun `NavigateToPasswordHistory event should call onNavigateToPasswordHistoryScreen`() {
mutableEventFlow.tryEmit(GeneratorEvent.NavigateToPasswordHistory)

View File

@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.tools.feature.generator
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import app.cash.turbine.turbineScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
@@ -12,10 +13,12 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedForwar
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassphraseResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import com.x8bit.bitwarden.data.tools.generator.repository.util.FakeGeneratorRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
@@ -28,12 +31,15 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
@Suppress("LargeClass")
class GeneratorViewModelTest : BaseViewModelTest() {
private val initialPasscodeState = createPasswordState()
private val initialPasscodeSavedStateHandle =
createSavedStateHandleWithState(initialPasscodeState)
private val initialUsernameModeState = createUsernameModeState()
private val initialPassphraseState = createPassphraseState()
private val passphraseSavedStateHandle = createSavedStateHandleWithState(initialPassphraseState)
@@ -90,6 +96,35 @@ class GeneratorViewModelTest : BaseViewModelTest() {
assertEquals(initialPasscodeState, viewModel.stateFlow.value)
}
@Test
fun `CloseClick should emit NavigateBack event`() = runTest {
val viewModel = createViewModel()
viewModel.actionChannel.trySend(GeneratorAction.CloseClick)
viewModel.eventFlow.test {
val event = awaitItem()
assertEquals(GeneratorEvent.NavigateBack, event)
}
}
@Test
fun `SelectClick should emit the NavigateBack event with GeneratorResult`() = runTest {
turbineScope {
val viewModel = createViewModel(state = initialUsernameModeState)
val eventTurbine = viewModel
.eventFlow
.testIn(backgroundScope)
val generatorResultTurbine = fakeGeneratorRepository
.generatorResultFlow
.testIn(backgroundScope)
viewModel.actionChannel.trySend(GeneratorAction.SelectClick)
assertEquals(GeneratorEvent.NavigateBack, eventTurbine.awaitItem())
assertEquals(GeneratorResult.Username("username"), generatorResultTurbine.awaitItem())
}
}
@Suppress("MaxLineLength")
@Test
fun `RegenerateClick action for password state updates generatedText and saves password generation options on successful password generation`() =
@@ -1550,6 +1585,21 @@ class GeneratorViewModelTest : BaseViewModelTest() {
currentEmailAddress = "currentEmail",
)
private fun createUsernameModeState(
generatedText: String = "username",
email: String = "currentEmail",
): GeneratorState =
GeneratorState(
generatedText = generatedText,
generatorMode = GeneratorMode.Modal.Username,
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail(
email = email,
),
),
currentEmailAddress = "currentEmail",
)
private fun createForwardedEmailAliasState(
generatedText: String = "defaultForwardedEmailAlias",
obfuscatedText: String = "defaultObfuscatedText",

View File

@@ -7,6 +7,8 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
import com.x8bit.bitwarden.data.tools.generator.repository.util.FakeGeneratorRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
@@ -14,6 +16,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState
@@ -62,6 +65,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
every { totpCodeFlow } returns totpTestCodeFlow
}
private val generatorRepository: GeneratorRepository = FakeGeneratorRepository()
@BeforeEach
fun setup() {
mockkStatic(CIPHER_VIEW_EXTENSIONS_PATH)
@@ -570,7 +575,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `OpenUsernameGeneratorClick should emit ShowToast with 'Open Username Generator' message`() =
fun `OpenUsernameGeneratorClick should emit NavigateToGeneratorModal with username GeneratorMode`() =
runTest {
val viewModel = createAddVaultItemViewModel()
@@ -579,7 +584,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
VaultAddEditAction.ItemType.LoginType.OpenUsernameGeneratorClick,
)
assertEquals(
VaultAddEditEvent.ShowToast("Open Username Generator".asText()),
VaultAddEditEvent.NavigateToGeneratorModal(GeneratorMode.Modal.Username),
awaitItem(),
)
}
@@ -606,7 +611,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `OpenPasswordGeneratorClick should emit ShowToast with 'Open Password Generator' message`() =
fun `OpenPasswordGeneratorClick should emit NavigateToGeneratorModal with with password GeneratorMode`() =
runTest {
val viewModel = createAddVaultItemViewModel()
@@ -616,7 +621,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
.trySend(VaultAddEditAction.ItemType.LoginType.OpenPasswordGeneratorClick)
assertEquals(
VaultAddEditEvent.ShowToast("Open Password Generator".asText()),
VaultAddEditEvent.NavigateToGeneratorModal(GeneratorMode.Modal.Password),
awaitItem(),
)
}
@@ -1186,6 +1191,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
savedStateHandle = secureNotesInitialSavedStateHandle,
clipboardManager = clipboardManager,
vaultRepository = vaultRepository,
generatorRepository = generatorRepository,
)
}
@@ -1487,11 +1493,13 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
savedStateHandle: SavedStateHandle = loginInitialSavedStateHandle,
bitwardenClipboardManager: BitwardenClipboardManager = clipboardManager,
vaultRepo: VaultRepository = vaultRepository,
generatorRepo: GeneratorRepository = generatorRepository,
): VaultAddEditViewModel =
VaultAddEditViewModel(
savedStateHandle = savedStateHandle,
clipboardManager = bitwardenClipboardManager,
vaultRepository = vaultRepo,
generatorRepository = generatorRepo,
)
/**