mirror of
https://github.com/bitwarden/android.git
synced 2026-05-30 07:44:15 -05:00
BIT-1054, BIT-1055: Adding modal generator UI and navigation from Add/Edit item (#643)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user