mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 23:25:45 +03:00
BIT-710: Implement Password History UI (#352)
This commit is contained in:
parent
40fa3071ae
commit
d0205b4b59
14 changed files with 774 additions and 133 deletions
|
@ -8,6 +8,8 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteac
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.navigateToDeleteAccount
|
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.navigateToDeleteAccount
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE
|
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.vaultUnlockedNavBarDestination
|
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.vaultUnlockedNavBarDestination
|
||||||
|
import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.navigateToPasswordHistory
|
||||||
|
import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.passwordHistoryDestination
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.send.navigateToNewSend
|
import com.x8bit.bitwarden.ui.tools.feature.send.navigateToNewSend
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.send.newSendDestination
|
import com.x8bit.bitwarden.ui.tools.feature.send.newSendDestination
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.additem.navigateToVaultAddEditItem
|
import com.x8bit.bitwarden.ui.vault.feature.additem.navigateToVaultAddEditItem
|
||||||
|
@ -46,6 +48,7 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
||||||
},
|
},
|
||||||
onNavigateToNewSend = { navController.navigateToNewSend() },
|
onNavigateToNewSend = { navController.navigateToNewSend() },
|
||||||
onNavigateToDeleteAccount = { navController.navigateToDeleteAccount() },
|
onNavigateToDeleteAccount = { navController.navigateToDeleteAccount() },
|
||||||
|
onNavigateToPasswordHistory = { navController.navigateToPasswordHistory() },
|
||||||
)
|
)
|
||||||
deleteAccountDestination(onNavigateBack = { navController.popBackStack() })
|
deleteAccountDestination(onNavigateBack = { navController.popBackStack() })
|
||||||
vaultAddEditItemDestination(onNavigateBack = { navController.popBackStack() })
|
vaultAddEditItemDestination(onNavigateBack = { navController.popBackStack() })
|
||||||
|
@ -57,5 +60,6 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
||||||
)
|
)
|
||||||
vaultEditItemDestination(onNavigateBack = { navController.popBackStack() })
|
vaultEditItemDestination(onNavigateBack = { navController.popBackStack() })
|
||||||
newSendDestination(onNavigateBack = { navController.popBackStack() })
|
newSendDestination(onNavigateBack = { navController.popBackStack() })
|
||||||
|
passwordHistoryDestination(onNavigateBack = { navController.popBackStack() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,12 +21,14 @@ fun NavController.navigateToVaultUnlockedNavBar(navOptions: NavOptions? = null)
|
||||||
/**
|
/**
|
||||||
* Add vault unlocked destination to the root nav graph.
|
* Add vault unlocked destination to the root nav graph.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
fun NavGraphBuilder.vaultUnlockedNavBarDestination(
|
fun NavGraphBuilder.vaultUnlockedNavBarDestination(
|
||||||
onNavigateToVaultAddItem: () -> Unit,
|
onNavigateToVaultAddItem: () -> Unit,
|
||||||
onNavigateToVaultItem: (vaultItemId: String) -> Unit,
|
onNavigateToVaultItem: (vaultItemId: String) -> Unit,
|
||||||
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
|
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
|
||||||
onNavigateToNewSend: () -> Unit,
|
onNavigateToNewSend: () -> Unit,
|
||||||
onNavigateToDeleteAccount: () -> Unit,
|
onNavigateToDeleteAccount: () -> Unit,
|
||||||
|
onNavigateToPasswordHistory: () -> Unit,
|
||||||
) {
|
) {
|
||||||
composable(
|
composable(
|
||||||
route = VAULT_UNLOCKED_NAV_BAR_ROUTE,
|
route = VAULT_UNLOCKED_NAV_BAR_ROUTE,
|
||||||
|
@ -41,6 +43,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
|
||||||
onNavigateToVaultEditItem = onNavigateToVaultEditItem,
|
onNavigateToVaultEditItem = onNavigateToVaultEditItem,
|
||||||
onNavigateToNewSend = onNavigateToNewSend,
|
onNavigateToNewSend = onNavigateToNewSend,
|
||||||
onNavigateToDeleteAccount = onNavigateToDeleteAccount,
|
onNavigateToDeleteAccount = onNavigateToDeleteAccount,
|
||||||
|
onNavigateToPasswordHistory = onNavigateToPasswordHistory,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,7 @@ fun VaultUnlockedNavBarScreen(
|
||||||
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
|
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
|
||||||
onNavigateToNewSend: () -> Unit,
|
onNavigateToNewSend: () -> Unit,
|
||||||
onNavigateToDeleteAccount: () -> Unit,
|
onNavigateToDeleteAccount: () -> Unit,
|
||||||
|
onNavigateToPasswordHistory: () -> Unit,
|
||||||
) {
|
) {
|
||||||
EventsEffect(viewModel = viewModel) { event ->
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
navController.apply {
|
navController.apply {
|
||||||
|
@ -101,6 +102,7 @@ fun VaultUnlockedNavBarScreen(
|
||||||
navigateToVaultAddItem = onNavigateToVaultAddItem,
|
navigateToVaultAddItem = onNavigateToVaultAddItem,
|
||||||
navigateToNewSend = onNavigateToNewSend,
|
navigateToNewSend = onNavigateToNewSend,
|
||||||
navigateToDeleteAccount = onNavigateToDeleteAccount,
|
navigateToDeleteAccount = onNavigateToDeleteAccount,
|
||||||
|
navigateToPasswordHistory = onNavigateToPasswordHistory,
|
||||||
generatorTabClickedAction = {
|
generatorTabClickedAction = {
|
||||||
viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick)
|
viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick)
|
||||||
},
|
},
|
||||||
|
@ -132,6 +134,7 @@ private fun VaultUnlockedNavBarScaffold(
|
||||||
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
|
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
|
||||||
navigateToNewSend: () -> Unit,
|
navigateToNewSend: () -> Unit,
|
||||||
navigateToDeleteAccount: () -> Unit,
|
navigateToDeleteAccount: () -> Unit,
|
||||||
|
navigateToPasswordHistory: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var shouldDimNavBar by remember { mutableStateOf(false) }
|
var shouldDimNavBar by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
@ -191,7 +194,9 @@ private fun VaultUnlockedNavBarScaffold(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
sendGraph(onNavigateToNewSend = navigateToNewSend)
|
sendGraph(onNavigateToNewSend = navigateToNewSend)
|
||||||
generatorDestination()
|
generatorDestination(
|
||||||
|
onNavigateToPasswordHistory = { navigateToPasswordHistory() },
|
||||||
|
)
|
||||||
settingsGraph(
|
settingsGraph(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
onNavigateToDeleteAccount = navigateToDeleteAccount,
|
onNavigateToDeleteAccount = navigateToDeleteAccount,
|
||||||
|
|
|
@ -20,8 +20,12 @@ fun NavController.navigateToGenerator(navOptions: NavOptions? = null) {
|
||||||
/**
|
/**
|
||||||
* Add generator destination to the root nav graph.
|
* Add generator destination to the root nav graph.
|
||||||
*/
|
*/
|
||||||
fun NavGraphBuilder.generatorDestination() {
|
fun NavGraphBuilder.generatorDestination(
|
||||||
|
onNavigateToPasswordHistory: () -> Unit,
|
||||||
|
) {
|
||||||
composable(GENERATOR_ROUTE) {
|
composable(GENERATOR_ROUTE) {
|
||||||
GeneratorScreen()
|
GeneratorScreen(
|
||||||
|
onNavigateToPasswordHistory = onNavigateToPasswordHistory,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenStepper
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenStepper
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
|
||||||
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
|
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
|
||||||
import com.x8bit.bitwarden.ui.platform.components.model.TooltipData
|
import com.x8bit.bitwarden.ui.platform.components.model.TooltipData
|
||||||
import com.x8bit.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformation
|
import com.x8bit.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformation
|
||||||
|
@ -70,6 +71,7 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Pa
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_COUNTER_MIN
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_COUNTER_MIN
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MAX
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MAX
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MIN
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MIN
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -80,6 +82,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
@Composable
|
@Composable
|
||||||
fun GeneratorScreen(
|
fun GeneratorScreen(
|
||||||
viewModel: GeneratorViewModel = hiltViewModel(),
|
viewModel: GeneratorViewModel = hiltViewModel(),
|
||||||
|
onNavigateToPasswordHistory: () -> Unit,
|
||||||
clipboardManager: ClipboardManager = LocalClipboardManager.current,
|
clipboardManager: ClipboardManager = LocalClipboardManager.current,
|
||||||
) {
|
) {
|
||||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
|
@ -89,6 +92,8 @@ fun GeneratorScreen(
|
||||||
|
|
||||||
EventsEffect(viewModel = viewModel) { event ->
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
|
GeneratorEvent.NavigateToPasswordHistory -> onNavigateToPasswordHistory()
|
||||||
|
|
||||||
GeneratorEvent.CopyTextToClipboard -> {
|
GeneratorEvent.CopyTextToClipboard -> {
|
||||||
clipboardManager.setText(AnnotatedString(state.generatedText))
|
clipboardManager.setText(AnnotatedString(state.generatedText))
|
||||||
}
|
}
|
||||||
|
@ -165,7 +170,20 @@ fun GeneratorScreen(
|
||||||
title = stringResource(id = R.string.generator),
|
title = stringResource(id = R.string.generator),
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
actions = {
|
actions = {
|
||||||
BitwardenOverflowActionItem()
|
BitwardenOverflowActionItem(
|
||||||
|
menuItemDataList = persistentListOf(
|
||||||
|
OverflowMenuItemData(
|
||||||
|
text = stringResource(id = R.string.password_history),
|
||||||
|
onClick = remember(viewModel) {
|
||||||
|
{
|
||||||
|
viewModel.trySendAction(
|
||||||
|
GeneratorAction.PasswordHistoryClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -904,7 +922,9 @@ private fun RandomWordIncludeNumberToggleItem(
|
||||||
@Composable
|
@Composable
|
||||||
private fun GeneratorPreview() {
|
private fun GeneratorPreview() {
|
||||||
BitwardenTheme {
|
BitwardenTheme {
|
||||||
GeneratorScreen()
|
GeneratorScreen(
|
||||||
|
onNavigateToPasswordHistory = {},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,10 @@ class GeneratorViewModel @Inject constructor(
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
override fun handleAction(action: GeneratorAction) {
|
override fun handleAction(action: GeneratorAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
|
is GeneratorAction.PasswordHistoryClick -> {
|
||||||
|
handlePasswordHistoryClick()
|
||||||
|
}
|
||||||
|
|
||||||
is GeneratorAction.RegenerateClick -> {
|
is GeneratorAction.RegenerateClick -> {
|
||||||
handleRegenerationClick()
|
handleRegenerationClick()
|
||||||
}
|
}
|
||||||
|
@ -119,6 +123,14 @@ class GeneratorViewModel @Inject constructor(
|
||||||
|
|
||||||
//endregion Initialization and Overrides
|
//endregion Initialization and Overrides
|
||||||
|
|
||||||
|
//region Top Level Handlers
|
||||||
|
|
||||||
|
private fun handlePasswordHistoryClick() {
|
||||||
|
sendEvent(GeneratorEvent.NavigateToPasswordHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion Top Level Handlers
|
||||||
|
|
||||||
//region Generation Handlers
|
//region Generation Handlers
|
||||||
|
|
||||||
private fun loadPasscodeOptions(selectedType: Passcode) {
|
private fun loadPasscodeOptions(selectedType: Passcode) {
|
||||||
|
@ -1105,6 +1117,11 @@ data class GeneratorState(
|
||||||
*/
|
*/
|
||||||
sealed class GeneratorAction {
|
sealed class GeneratorAction {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the overflow option for password history has been clicked.
|
||||||
|
*/
|
||||||
|
data object PasswordHistoryClick : GeneratorAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the action to regenerate a new passcode or username.
|
* Represents the action to regenerate a new passcode or username.
|
||||||
*/
|
*/
|
||||||
|
@ -1375,6 +1392,11 @@ sealed class GeneratorAction {
|
||||||
*/
|
*/
|
||||||
sealed class GeneratorEvent {
|
sealed class GeneratorEvent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to the Password History screen.
|
||||||
|
*/
|
||||||
|
data object NavigateToPasswordHistory : GeneratorEvent()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copies text to the clipboard.
|
* Copies text to the clipboard.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.withVisualTransformation
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformation
|
||||||
|
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A composable function for displaying a password history list item.
|
||||||
|
*
|
||||||
|
* @param label The primary text to be displayed in the list item.
|
||||||
|
* @param supportingLabel A secondary text displayed below the primary label.
|
||||||
|
* @param onCopyClick The lambda function to be invoked when the list items icon is clicked.
|
||||||
|
* @param modifier The [Modifier] to be applied to the list item.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun PasswordHistoryListItem(
|
||||||
|
label: String,
|
||||||
|
supportingLabel: String,
|
||||||
|
onCopyClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
.then(modifier),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = label.withVisualTransformation(
|
||||||
|
visualTransformation = nonLetterColorVisualTransformation(),
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = supportingLabel,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = onCopyClick,
|
||||||
|
colors = IconButtonDefaults.iconButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_copy),
|
||||||
|
contentDescription = stringResource(id = R.string.copy),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PasswordHistoryListItem_preview() {
|
||||||
|
BitwardenTheme {
|
||||||
|
PasswordHistoryListItem(
|
||||||
|
label = "8gr6uY8CLYQwzr#",
|
||||||
|
supportingLabel = "8/24/2023 11:07 AM",
|
||||||
|
onCopyClick = {},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
|
||||||
|
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import com.x8bit.bitwarden.ui.platform.theme.TransitionProviders
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The functions below pertain to entry into the [PasswordHistoryScreen].
|
||||||
|
*/
|
||||||
|
private const val PASSWORD_HISTORY_ROUTE: String = "password_history"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add password history destination to the graph.
|
||||||
|
*/
|
||||||
|
fun NavGraphBuilder.passwordHistoryDestination(
|
||||||
|
onNavigateBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
composable(
|
||||||
|
// TODO: (BIT-617) Allow Password History screen to launch from VaultItemScreen
|
||||||
|
route = PASSWORD_HISTORY_ROUTE,
|
||||||
|
enterTransition = TransitionProviders.Enter.slideUp,
|
||||||
|
exitTransition = TransitionProviders.Exit.slideDown,
|
||||||
|
popEnterTransition = TransitionProviders.Enter.slideUp,
|
||||||
|
popExitTransition = TransitionProviders.Exit.slideDown,
|
||||||
|
) {
|
||||||
|
PasswordHistoryScreen(
|
||||||
|
onNavigateBack = onNavigateBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the Password History Screen.
|
||||||
|
*/
|
||||||
|
fun NavController.navigateToPasswordHistory(navOptions: NavOptions? = null) {
|
||||||
|
navigate(PASSWORD_HISTORY_ROUTE, navOptions)
|
||||||
|
}
|
|
@ -0,0 +1,211 @@
|
||||||
|
package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the password history screen
|
||||||
|
*/
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun PasswordHistoryScreen(
|
||||||
|
onNavigateBack: () -> Unit,
|
||||||
|
viewModel: PasswordHistoryViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
|
||||||
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
|
when (event) {
|
||||||
|
PasswordHistoryEvent.NavigateBack -> onNavigateBack.invoke()
|
||||||
|
is PasswordHistoryEvent.ShowToast -> {
|
||||||
|
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BitwardenScaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
BitwardenTopAppBar(
|
||||||
|
title = stringResource(id = R.string.password_history),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigationIcon = painterResource(id = R.drawable.ic_close),
|
||||||
|
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||||
|
onNavigationIconClick = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(PasswordHistoryAction.CloseClick) }
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
BitwardenOverflowActionItem(
|
||||||
|
menuItemDataList = persistentListOf(
|
||||||
|
OverflowMenuItemData(
|
||||||
|
text = stringResource(id = R.string.clear),
|
||||||
|
onClick = remember(viewModel) {
|
||||||
|
{
|
||||||
|
viewModel.trySendAction(
|
||||||
|
PasswordHistoryAction.PasswordClearClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = { innerPadding ->
|
||||||
|
when (val viewState = state.viewState) {
|
||||||
|
is PasswordHistoryState.ViewState.Loading -> {
|
||||||
|
PasswordHistoryLoading(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is PasswordHistoryState.ViewState.Error -> {
|
||||||
|
PasswordHistoryError(
|
||||||
|
state = viewState,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is PasswordHistoryState.ViewState.Empty -> {
|
||||||
|
PasswordHistoryEmpty(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is PasswordHistoryState.ViewState.Content -> {
|
||||||
|
PasswordHistoryContent(
|
||||||
|
state = viewState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.imePadding()
|
||||||
|
.padding(innerPadding),
|
||||||
|
onPasswordCopyClick = { password ->
|
||||||
|
viewModel.trySendAction(
|
||||||
|
PasswordHistoryAction.PasswordCopyClick(password),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PasswordHistoryLoading(modifier: Modifier = Modifier) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PasswordHistoryContent(
|
||||||
|
state: PasswordHistoryState.ViewState.Content,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onPasswordCopyClick: (PasswordHistoryState.GeneratedPassword) -> Unit,
|
||||||
|
) {
|
||||||
|
LazyColumn(modifier = modifier) {
|
||||||
|
items(state.passwords) { password ->
|
||||||
|
PasswordHistoryListItem(
|
||||||
|
label = password.password,
|
||||||
|
supportingLabel = password.date,
|
||||||
|
onCopyClick = { onPasswordCopyClick(password) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PasswordHistoryError(
|
||||||
|
state: PasswordHistoryState.ViewState.Error,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier,
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = state.message,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PasswordHistoryEmpty(modifier: Modifier = Modifier) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier,
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.no_passwords_to_list),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
|
import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.PasswordHistoryState.GeneratedPassword
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel responsible for handling user interactions in the PasswordHistoryScreen.
|
||||||
|
*/
|
||||||
|
@HiltViewModel
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
|
class PasswordHistoryViewModel @Inject constructor() :
|
||||||
|
BaseViewModel<PasswordHistoryState, PasswordHistoryEvent, PasswordHistoryAction>(
|
||||||
|
initialState = PasswordHistoryState(PasswordHistoryState.ViewState.Loading),
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun handleAction(action: PasswordHistoryAction) {
|
||||||
|
when (action) {
|
||||||
|
PasswordHistoryAction.CloseClick -> handleCloseClick()
|
||||||
|
is PasswordHistoryAction.PasswordCopyClick -> handleCopyClick(action.password)
|
||||||
|
PasswordHistoryAction.PasswordClearClick -> handlePasswordHistoryClearClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCloseClick() {
|
||||||
|
sendEvent(
|
||||||
|
event = PasswordHistoryEvent.NavigateBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePasswordHistoryClearClick() {
|
||||||
|
sendEvent(
|
||||||
|
event = PasswordHistoryEvent.ShowToast(
|
||||||
|
message = "Not yet implemented.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCopyClick(password: GeneratedPassword) {
|
||||||
|
sendEvent(
|
||||||
|
event = PasswordHistoryEvent.ShowToast(
|
||||||
|
message = "Not yet implemented.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the possible states for the password history screen.
|
||||||
|
*
|
||||||
|
* @property viewState The current view state of the password history screen.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class PasswordHistoryState(
|
||||||
|
val viewState: ViewState,
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the specific view states for the password history screen.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
sealed class ViewState : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading state for the password history screen.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data object Loading : ViewState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error state for the password history screen.
|
||||||
|
*
|
||||||
|
* @property message The error message to be displayed.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Error(val message: String) : ViewState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty state for the password history screen.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data object Empty : ViewState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content state for the password history screen.
|
||||||
|
*
|
||||||
|
* @property passwords A list of generated passwords, each with its creation date.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Content(val passwords: List<GeneratedPassword>) : ViewState()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a generated password with its creation date.
|
||||||
|
*
|
||||||
|
* @property password The generated password.
|
||||||
|
* @property date The date when the password was generated.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class GeneratedPassword(
|
||||||
|
val password: String,
|
||||||
|
val date: String,
|
||||||
|
) : Parcelable
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the set of events that can occur in the password history screen.
|
||||||
|
*/
|
||||||
|
sealed class PasswordHistoryEvent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event to show a toast message.
|
||||||
|
*
|
||||||
|
* @property message The message to be displayed in the toast.
|
||||||
|
*/
|
||||||
|
data class ShowToast(val message: String) : PasswordHistoryEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event to navigate back to the previous screen.
|
||||||
|
*/
|
||||||
|
data object NavigateBack : PasswordHistoryEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the set of actions that can be performed in the password history screen.
|
||||||
|
*/
|
||||||
|
sealed class PasswordHistoryAction {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the action triggered when a password copy button is clicked.
|
||||||
|
*
|
||||||
|
* @param password The [GeneratedPassword] to be copied.
|
||||||
|
*/
|
||||||
|
data class PasswordCopyClick(val password: GeneratedPassword) : PasswordHistoryAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action when the clear passwords button is clicked.
|
||||||
|
*/
|
||||||
|
data object PasswordClearClick : PasswordHistoryAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action when the close button is clicked.
|
||||||
|
*/
|
||||||
|
data object CloseClick : PasswordHistoryAction()
|
||||||
|
}
|
|
@ -37,6 +37,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
||||||
onNavigateToVaultEditItem = {},
|
onNavigateToVaultEditItem = {},
|
||||||
onNavigateToNewSend = {},
|
onNavigateToNewSend = {},
|
||||||
onNavigateToDeleteAccount = {},
|
onNavigateToDeleteAccount = {},
|
||||||
|
onNavigateToPasswordHistory = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onNodeWithText("My vault").performClick()
|
onNodeWithText("My vault").performClick()
|
||||||
|
@ -62,6 +63,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
||||||
onNavigateToVaultEditItem = {},
|
onNavigateToVaultEditItem = {},
|
||||||
onNavigateToNewSend = {},
|
onNavigateToNewSend = {},
|
||||||
onNavigateToDeleteAccount = {},
|
onNavigateToDeleteAccount = {},
|
||||||
|
onNavigateToPasswordHistory = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
|
runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
|
||||||
|
@ -88,6 +90,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
||||||
onNavigateToVaultEditItem = {},
|
onNavigateToVaultEditItem = {},
|
||||||
onNavigateToNewSend = {},
|
onNavigateToNewSend = {},
|
||||||
onNavigateToDeleteAccount = {},
|
onNavigateToDeleteAccount = {},
|
||||||
|
onNavigateToPasswordHistory = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onNodeWithText("Send").performClick()
|
onNodeWithText("Send").performClick()
|
||||||
|
@ -113,6 +116,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
||||||
onNavigateToVaultEditItem = {},
|
onNavigateToVaultEditItem = {},
|
||||||
onNavigateToNewSend = {},
|
onNavigateToNewSend = {},
|
||||||
onNavigateToDeleteAccount = {},
|
onNavigateToDeleteAccount = {},
|
||||||
|
onNavigateToPasswordHistory = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
|
runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
|
||||||
|
@ -139,6 +143,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
||||||
onNavigateToVaultEditItem = {},
|
onNavigateToVaultEditItem = {},
|
||||||
onNavigateToNewSend = {},
|
onNavigateToNewSend = {},
|
||||||
onNavigateToDeleteAccount = {},
|
onNavigateToDeleteAccount = {},
|
||||||
|
onNavigateToPasswordHistory = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onNodeWithText("Generator").performClick()
|
onNodeWithText("Generator").performClick()
|
||||||
|
@ -164,6 +169,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
||||||
onNavigateToVaultEditItem = {},
|
onNavigateToVaultEditItem = {},
|
||||||
onNavigateToNewSend = {},
|
onNavigateToNewSend = {},
|
||||||
onNavigateToDeleteAccount = {},
|
onNavigateToDeleteAccount = {},
|
||||||
|
onNavigateToPasswordHistory = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
|
runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
|
||||||
|
@ -190,6 +196,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
||||||
onNavigateToVaultEditItem = {},
|
onNavigateToVaultEditItem = {},
|
||||||
onNavigateToNewSend = {},
|
onNavigateToNewSend = {},
|
||||||
onNavigateToDeleteAccount = {},
|
onNavigateToDeleteAccount = {},
|
||||||
|
onNavigateToPasswordHistory = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onNodeWithText("Settings").performClick()
|
onNodeWithText("Settings").performClick()
|
||||||
|
@ -215,6 +222,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
||||||
onNavigateToVaultEditItem = {},
|
onNavigateToVaultEditItem = {},
|
||||||
onNavigateToNewSend = {},
|
onNavigateToNewSend = {},
|
||||||
onNavigateToDeleteAccount = {},
|
onNavigateToDeleteAccount = {},
|
||||||
|
onNavigateToPasswordHistory = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
|
runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
|
||||||
|
|
|
@ -31,10 +31,14 @@ import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
|
||||||
@Suppress("LargeClass")
|
@Suppress("LargeClass")
|
||||||
class GeneratorScreenTest : BaseComposeTest() {
|
class GeneratorScreenTest : BaseComposeTest() {
|
||||||
|
private var onNavigateToPasswordHistoryScreenCalled = false
|
||||||
|
|
||||||
private val mutableStateFlow = MutableStateFlow(
|
private val mutableStateFlow = MutableStateFlow(
|
||||||
GeneratorState(
|
GeneratorState(
|
||||||
generatedText = "Placeholder",
|
generatedText = "Placeholder",
|
||||||
|
@ -59,12 +63,24 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
every { stateFlow } returns mutableStateFlow
|
every { stateFlow } returns mutableStateFlow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
GeneratorScreen(
|
||||||
|
viewModel = viewModel,
|
||||||
|
onNavigateToPasswordHistory = { onNavigateToPasswordHistoryScreenCalled = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `NavigateToPasswordHistory event should call onNavigateToPasswordHistoryScreen`() {
|
||||||
|
mutableEventFlow.tryEmit(GeneratorEvent.NavigateToPasswordHistory)
|
||||||
|
assertTrue(onNavigateToPasswordHistoryScreenCalled)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Snackbar should be displayed with correct message on ShowSnackbar event`() {
|
fun `Snackbar should be displayed with correct message on ShowSnackbar event`() {
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
mutableEventFlow.tryEmit(GeneratorEvent.ShowSnackbar("Test Snackbar Message".asText()))
|
mutableEventFlow.tryEmit(GeneratorEvent.ShowSnackbar("Test Snackbar Message".asText()))
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
|
@ -74,10 +90,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking the Regenerate button should send RegenerateClick action`() {
|
fun `clicking the Regenerate button should send RegenerateClick action`() {
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithContentDescription(label = "Generate password")
|
.onNodeWithContentDescription(label = "Generate password")
|
||||||
.performClick()
|
.performClick()
|
||||||
|
@ -89,10 +101,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking the Copy button should send CopyClick action`() {
|
fun `clicking the Copy button should send CopyClick action`() {
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithContentDescription(label = "Copy")
|
.onNodeWithContentDescription(label = "Copy")
|
||||||
.performClick()
|
.performClick()
|
||||||
|
@ -104,10 +112,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking a MainStateOption should send MainTypeOptionSelect action`() {
|
fun `clicking a MainStateOption should send MainTypeOptionSelect action`() {
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opens the menu
|
// Opens the menu
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithContentDescription(label = "What would you like to generate?, Password")
|
.onNodeWithContentDescription(label = "What would you like to generate?, Password")
|
||||||
|
@ -135,10 +139,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking a PasscodeOption should send PasscodeTypeOption action`() {
|
fun `clicking a PasscodeOption should send PasscodeTypeOption action`() {
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opens the menu
|
// Opens the menu
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithContentDescription(label = "Password type, Password")
|
.onNodeWithContentDescription(label = "Password type, Password")
|
||||||
|
@ -178,10 +178,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opens the menu
|
// Opens the menu
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithContentDescription(label = "Username type, Plus addressed email")
|
.onNodeWithContentDescription(label = "Username type, Plus addressed email")
|
||||||
|
@ -212,10 +208,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `in Passcode_Password state, the ViewModel state should update the UI correctly`() {
|
fun `in Passcode_Password state, the ViewModel state should update the UI correctly`() {
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithContentDescription(label = "What would you like to generate?, Password")
|
.onNodeWithContentDescription(label = "What would you like to generate?, Password")
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
|
@ -275,10 +267,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `in Passcode_Password state, adjusting the slider should send SliderLengthChange action with length not equal to default`() {
|
fun `in Passcode_Password state, adjusting the slider should send SliderLengthChange action with length not equal to default`() {
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Length")
|
.onNodeWithText("Length")
|
||||||
.onSiblings()
|
.onSiblings()
|
||||||
|
@ -308,10 +296,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `in Passcode_Password state, toggling the capital letters toggle should send ToggleCapitalLettersChange action`() {
|
fun `in Passcode_Password state, toggling the capital letters toggle should send ToggleCapitalLettersChange action`() {
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText("A—Z")
|
composeTestRule.onNodeWithText("A—Z")
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
.performClick()
|
.performClick()
|
||||||
|
@ -328,10 +312,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `in Passcode_Password state, toggling the use lowercase toggle should send ToggleLowercaseLettersChange action`() {
|
fun `in Passcode_Password state, toggling the use lowercase toggle should send ToggleLowercaseLettersChange action`() {
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText("a—z")
|
composeTestRule.onNodeWithText("a—z")
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
.performClick()
|
.performClick()
|
||||||
|
@ -353,10 +333,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `in Passcode_Password state, toggling the use numbers toggle should send ToggleNumbersChange action`() {
|
fun `in Passcode_Password state, toggling the use numbers toggle should send ToggleNumbersChange action`() {
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText("0-9")
|
composeTestRule.onNodeWithText("0-9")
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
.performClick()
|
.performClick()
|
||||||
|
@ -373,10 +349,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `in Passcode_Password state, toggling the use special characters toggle should send ToggleSpecialCharactersChange action`() {
|
fun `in Passcode_Password state, toggling the use special characters toggle should send ToggleSpecialCharactersChange action`() {
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText("!@#$%^&*")
|
composeTestRule.onNodeWithText("!@#$%^&*")
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
.performClick()
|
.performClick()
|
||||||
|
@ -414,10 +386,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithContentDescription("Minimum numbers, 1")
|
composeTestRule.onNodeWithContentDescription("Minimum numbers, 1")
|
||||||
.onChildren()
|
.onChildren()
|
||||||
.filterToOne(hasContentDescription("\u2212"))
|
.filterToOne(hasContentDescription("\u2212"))
|
||||||
|
@ -452,10 +420,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithContentDescription("Minimum numbers, 1")
|
composeTestRule.onNodeWithContentDescription("Minimum numbers, 1")
|
||||||
.onChildren()
|
.onChildren()
|
||||||
.filterToOne(hasContentDescription("+"))
|
.filterToOne(hasContentDescription("+"))
|
||||||
|
@ -490,10 +454,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithContentDescription("Minimum numbers, $initialMinNumbers")
|
composeTestRule.onNodeWithContentDescription("Minimum numbers, $initialMinNumbers")
|
||||||
.onChildren()
|
.onChildren()
|
||||||
.filterToOne(hasContentDescription("\u2212"))
|
.filterToOne(hasContentDescription("\u2212"))
|
||||||
|
@ -522,10 +482,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithContentDescription("Minimum numbers, $initialMinNumbers")
|
composeTestRule.onNodeWithContentDescription("Minimum numbers, $initialMinNumbers")
|
||||||
.onChildren()
|
.onChildren()
|
||||||
.filterToOne(hasContentDescription("+"))
|
.filterToOne(hasContentDescription("+"))
|
||||||
|
@ -554,10 +510,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithContentDescription("Minimum special, 1")
|
composeTestRule.onNodeWithContentDescription("Minimum special, 1")
|
||||||
.onChildren()
|
.onChildren()
|
||||||
.filterToOne(hasContentDescription("\u2212"))
|
.filterToOne(hasContentDescription("\u2212"))
|
||||||
|
@ -592,10 +544,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithContentDescription("Minimum special, 1")
|
composeTestRule.onNodeWithContentDescription("Minimum special, 1")
|
||||||
.onChildren()
|
.onChildren()
|
||||||
.filterToOne(hasContentDescription("+"))
|
.filterToOne(hasContentDescription("+"))
|
||||||
|
@ -630,10 +578,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithContentDescription("Minimum special, $initialSpecialChars")
|
composeTestRule.onNodeWithContentDescription("Minimum special, $initialSpecialChars")
|
||||||
.onChildren()
|
.onChildren()
|
||||||
.filterToOne(hasContentDescription("\u2212"))
|
.filterToOne(hasContentDescription("\u2212"))
|
||||||
|
@ -662,10 +606,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithContentDescription("Minimum special, $initialSpecialChars")
|
composeTestRule.onNodeWithContentDescription("Minimum special, $initialSpecialChars")
|
||||||
.onChildren()
|
.onChildren()
|
||||||
.filterToOne(hasContentDescription("+"))
|
.filterToOne(hasContentDescription("+"))
|
||||||
|
@ -678,10 +618,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `in Passcode_Password state, toggling the use avoid ambiguous characters toggle should send ToggleSpecialCharactersChange action`() {
|
fun `in Passcode_Password state, toggling the use avoid ambiguous characters toggle should send ToggleSpecialCharactersChange action`() {
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText("Avoid ambiguous characters")
|
composeTestRule.onNodeWithText("Avoid ambiguous characters")
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
.performClick()
|
.performClick()
|
||||||
|
@ -723,10 +659,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unicode for "minus" used for content description
|
// Unicode for "minus" used for content description
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithContentDescription("Number of words, $initialNumWords")
|
.onNodeWithContentDescription("Number of words, $initialNumWords")
|
||||||
|
@ -762,10 +694,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unicode for "minus" used for content description
|
// Unicode for "minus" used for content description
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithContentDescription("Number of words, $initialNumWords")
|
.onNodeWithContentDescription("Number of words, $initialNumWords")
|
||||||
|
@ -794,10 +722,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unicode for "minus" used for content description
|
// Unicode for "minus" used for content description
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithContentDescription("Number of words, $initialNumWords")
|
.onNodeWithContentDescription("Number of words, $initialNumWords")
|
||||||
|
@ -827,10 +751,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithContentDescription("Number of words, 3")
|
.onNodeWithContentDescription("Number of words, 3")
|
||||||
.onChildren()
|
.onChildren()
|
||||||
|
@ -865,10 +785,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Capitalize")
|
.onNodeWithText("Capitalize")
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
|
@ -901,10 +817,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText("Include number")
|
composeTestRule.onNodeWithText("Include number")
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
.performClick()
|
.performClick()
|
||||||
|
@ -936,10 +848,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Word separator")
|
.onNodeWithText("Word separator")
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
|
@ -972,10 +880,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
val newEmail = "test@example.com"
|
val newEmail = "test@example.com"
|
||||||
|
|
||||||
// Find the text field for PlusAddressedEmail and input text
|
// Find the text field for PlusAddressedEmail and input text
|
||||||
|
@ -1011,10 +915,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
val newDomain = "test.com"
|
val newDomain = "test.com"
|
||||||
|
|
||||||
// Find the text field for Catch-All Email and input text
|
// Find the text field for Catch-All Email and input text
|
||||||
|
@ -1048,10 +948,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText("Capitalize")
|
composeTestRule.onNodeWithText("Capitalize")
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
.performClick()
|
.performClick()
|
||||||
|
@ -1077,10 +973,6 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.setContent {
|
|
||||||
GeneratorScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText("Include number")
|
composeTestRule.onNodeWithText("Include number")
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
.performClick()
|
.performClick()
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
|
||||||
|
class PasswordHistoryScreenTest : BaseComposeTest() {
|
||||||
|
private var onNavigateBackCalled = false
|
||||||
|
|
||||||
|
private val mutableEventFlow = MutableSharedFlow<PasswordHistoryEvent>(
|
||||||
|
extraBufferCapacity = Int.MAX_VALUE,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val mutableStateFlow = MutableStateFlow(
|
||||||
|
PasswordHistoryState(PasswordHistoryState.ViewState.Loading),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val viewModel = mockk<PasswordHistoryViewModel>(relaxed = true) {
|
||||||
|
every { eventFlow } returns mutableEventFlow
|
||||||
|
every { stateFlow } returns mutableStateFlow
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
PasswordHistoryScreen(
|
||||||
|
viewModel = viewModel,
|
||||||
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Empty state should display no passwords message`() {
|
||||||
|
updateState(PasswordHistoryState(PasswordHistoryState.ViewState.Empty))
|
||||||
|
composeTestRule.onNodeWithText("No passwords to list.").assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Error state should display error message`() {
|
||||||
|
val errorMessage = "Error occurred"
|
||||||
|
updateState(PasswordHistoryState(PasswordHistoryState.ViewState.Error(errorMessage)))
|
||||||
|
composeTestRule.onNodeWithText(errorMessage).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `navigation icon click should trigger navigate back`() {
|
||||||
|
composeTestRule.onNodeWithContentDescription("Close").performClick()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
PasswordHistoryAction.CloseClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `NavigateBack event should call onNavigateBack`() {
|
||||||
|
mutableEventFlow.tryEmit(PasswordHistoryEvent.NavigateBack)
|
||||||
|
assertTrue(onNavigateBackCalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking the Copy button should send PasswordCopyClick action`() {
|
||||||
|
val password = PasswordHistoryState.GeneratedPassword(password = "Password", date = "Date")
|
||||||
|
updateState(
|
||||||
|
PasswordHistoryState(
|
||||||
|
PasswordHistoryState.ViewState.Content(
|
||||||
|
passwords = listOf(password),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText(password.password).assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithContentDescription("Copy").performClick()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
PasswordHistoryAction.PasswordCopyClick(
|
||||||
|
PasswordHistoryState.GeneratedPassword("Password", "Date"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking the Clear button in the overflow menu should send PasswordClearClick action`() {
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription(label = "More")
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Clear")
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(PasswordHistoryAction.PasswordClearClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Content state should display list of passwords`() {
|
||||||
|
val passwords =
|
||||||
|
listOf(PasswordHistoryState.GeneratedPassword(password = "Password1", date = "Date1"))
|
||||||
|
|
||||||
|
updateState(
|
||||||
|
PasswordHistoryState(
|
||||||
|
PasswordHistoryState.ViewState.Content(
|
||||||
|
passwords = passwords,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText("Password1").assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateState(state: PasswordHistoryState) {
|
||||||
|
mutableStateFlow.value = state
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
|
||||||
|
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class PasswordHistoryViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
|
private val initialState = PasswordHistoryState(PasswordHistoryState.ViewState.Loading)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state should be correct`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
assertEquals(initialState, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `CloseClick action should emit NavigateBack event`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.actionChannel.trySend(PasswordHistoryAction.CloseClick)
|
||||||
|
assertEquals(PasswordHistoryEvent.NavigateBack, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `PasswordCopyClick action should emit password copied ShowToast event`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.actionChannel.trySend(
|
||||||
|
PasswordHistoryAction.PasswordCopyClick(
|
||||||
|
PasswordHistoryState.GeneratedPassword(password = "Password", date = "Date"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals(PasswordHistoryEvent.ShowToast("Not yet implemented."), awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `PasswordClearClick action should emit password history cleared ShowToast event`() =
|
||||||
|
runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.actionChannel.trySend(PasswordHistoryAction.PasswordClearClick)
|
||||||
|
assertEquals(
|
||||||
|
PasswordHistoryEvent.ShowToast("Not yet implemented."),
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//region Helper Functions
|
||||||
|
|
||||||
|
private fun createViewModel(): PasswordHistoryViewModel {
|
||||||
|
return PasswordHistoryViewModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion Helper Functions
|
||||||
|
}
|
Loading…
Reference in a new issue