diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt index 948af0453a..2a6036fefa 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt @@ -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.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE 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.newSendDestination import com.x8bit.bitwarden.ui.vault.feature.additem.navigateToVaultAddEditItem @@ -46,6 +48,7 @@ fun NavGraphBuilder.vaultUnlockedGraph( }, onNavigateToNewSend = { navController.navigateToNewSend() }, onNavigateToDeleteAccount = { navController.navigateToDeleteAccount() }, + onNavigateToPasswordHistory = { navController.navigateToPasswordHistory() }, ) deleteAccountDestination(onNavigateBack = { navController.popBackStack() }) vaultAddEditItemDestination(onNavigateBack = { navController.popBackStack() }) @@ -57,5 +60,6 @@ fun NavGraphBuilder.vaultUnlockedGraph( ) vaultEditItemDestination(onNavigateBack = { navController.popBackStack() }) newSendDestination(onNavigateBack = { navController.popBackStack() }) + passwordHistoryDestination(onNavigateBack = { navController.popBackStack() }) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt index 000a3d44ec..c42c792633 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt @@ -21,12 +21,14 @@ fun NavController.navigateToVaultUnlockedNavBar(navOptions: NavOptions? = null) /** * Add vault unlocked destination to the root nav graph. */ +@Suppress("LongParameterList") fun NavGraphBuilder.vaultUnlockedNavBarDestination( onNavigateToVaultAddItem: () -> Unit, onNavigateToVaultItem: (vaultItemId: String) -> Unit, onNavigateToVaultEditItem: (vaultItemId: String) -> Unit, onNavigateToNewSend: () -> Unit, onNavigateToDeleteAccount: () -> Unit, + onNavigateToPasswordHistory: () -> Unit, ) { composable( route = VAULT_UNLOCKED_NAV_BAR_ROUTE, @@ -41,6 +43,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination( onNavigateToVaultEditItem = onNavigateToVaultEditItem, onNavigateToNewSend = onNavigateToNewSend, onNavigateToDeleteAccount = onNavigateToDeleteAccount, + onNavigateToPasswordHistory = onNavigateToPasswordHistory, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt index bad2248170..e29f2fcf3b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt @@ -71,6 +71,7 @@ fun VaultUnlockedNavBarScreen( onNavigateToVaultEditItem: (vaultItemId: String) -> Unit, onNavigateToNewSend: () -> Unit, onNavigateToDeleteAccount: () -> Unit, + onNavigateToPasswordHistory: () -> Unit, ) { EventsEffect(viewModel = viewModel) { event -> navController.apply { @@ -101,6 +102,7 @@ fun VaultUnlockedNavBarScreen( navigateToVaultAddItem = onNavigateToVaultAddItem, navigateToNewSend = onNavigateToNewSend, navigateToDeleteAccount = onNavigateToDeleteAccount, + navigateToPasswordHistory = onNavigateToPasswordHistory, generatorTabClickedAction = { viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick) }, @@ -132,6 +134,7 @@ private fun VaultUnlockedNavBarScaffold( onNavigateToVaultEditItem: (vaultItemId: String) -> Unit, navigateToNewSend: () -> Unit, navigateToDeleteAccount: () -> Unit, + navigateToPasswordHistory: () -> Unit, ) { var shouldDimNavBar by remember { mutableStateOf(false) } @@ -191,7 +194,9 @@ private fun VaultUnlockedNavBarScaffold( }, ) sendGraph(onNavigateToNewSend = navigateToNewSend) - generatorDestination() + generatorDestination( + onNavigateToPasswordHistory = { navigateToPasswordHistory() }, + ) settingsGraph( navController = navController, onNavigateToDeleteAccount = navigateToDeleteAccount, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorNavigation.kt index 123d769bd6..07c4b5c31c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorNavigation.kt @@ -20,8 +20,12 @@ fun NavController.navigateToGenerator(navOptions: NavOptions? = null) { /** * Add generator destination to the root nav graph. */ -fun NavGraphBuilder.generatorDestination() { +fun NavGraphBuilder.generatorDestination( + onNavigateToPasswordHistory: () -> Unit, +) { composable(GENERATOR_ROUTE) { - GeneratorScreen() + GeneratorScreen( + onNavigateToPasswordHistory = onNavigateToPasswordHistory, + ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt index a47edfba8b..e8cde2e259 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt @@ -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.BitwardenTextField 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.TooltipData 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_LENGTH_SLIDER_MAX 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 /** @@ -80,6 +82,7 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun GeneratorScreen( viewModel: GeneratorViewModel = hiltViewModel(), + onNavigateToPasswordHistory: () -> Unit, clipboardManager: ClipboardManager = LocalClipboardManager.current, ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() @@ -89,6 +92,8 @@ fun GeneratorScreen( EventsEffect(viewModel = viewModel) { event -> when (event) { + GeneratorEvent.NavigateToPasswordHistory -> onNavigateToPasswordHistory() + GeneratorEvent.CopyTextToClipboard -> { clipboardManager.setText(AnnotatedString(state.generatedText)) } @@ -165,7 +170,20 @@ fun GeneratorScreen( title = stringResource(id = R.string.generator), scrollBehavior = scrollBehavior, 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 private fun GeneratorPreview() { BitwardenTheme { - GeneratorScreen() + GeneratorScreen( + onNavigateToPasswordHistory = {}, + ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt index 4219dcc518..97428bff7e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt @@ -67,6 +67,10 @@ class GeneratorViewModel @Inject constructor( @Suppress("MaxLineLength") override fun handleAction(action: GeneratorAction) { when (action) { + is GeneratorAction.PasswordHistoryClick -> { + handlePasswordHistoryClick() + } + is GeneratorAction.RegenerateClick -> { handleRegenerationClick() } @@ -119,6 +123,14 @@ class GeneratorViewModel @Inject constructor( //endregion Initialization and Overrides + //region Top Level Handlers + + private fun handlePasswordHistoryClick() { + sendEvent(GeneratorEvent.NavigateToPasswordHistory) + } + + //endregion Top Level Handlers + //region Generation Handlers private fun loadPasscodeOptions(selectedType: Passcode) { @@ -1105,6 +1117,11 @@ data class GeneratorState( */ 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. */ @@ -1375,6 +1392,11 @@ sealed class GeneratorAction { */ sealed class GeneratorEvent { + /** + * Navigates to the Password History screen. + */ + data object NavigateToPasswordHistory : GeneratorEvent() + /** * Copies text to the clipboard. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryListItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryListItem.kt new file mode 100644 index 0000000000..2c5b5194a4 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryListItem.kt @@ -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), + ) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryNavigation.kt new file mode 100644 index 0000000000..9c62afa202 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryNavigation.kt @@ -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) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreen.kt new file mode 100644 index 0000000000..f922b4b72d --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreen.kt @@ -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()) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModel.kt new file mode 100644 index 0000000000..76891803fb --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModel.kt @@ -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( + 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) : 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() +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt index 31bdb555dc..663aaf4326 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt @@ -37,6 +37,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { onNavigateToVaultEditItem = {}, onNavigateToNewSend = {}, onNavigateToDeleteAccount = {}, + onNavigateToPasswordHistory = {}, ) } onNodeWithText("My vault").performClick() @@ -62,6 +63,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { onNavigateToVaultEditItem = {}, onNavigateToNewSend = {}, onNavigateToDeleteAccount = {}, + onNavigateToPasswordHistory = {}, ) } runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") } @@ -88,6 +90,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { onNavigateToVaultEditItem = {}, onNavigateToNewSend = {}, onNavigateToDeleteAccount = {}, + onNavigateToPasswordHistory = {}, ) } onNodeWithText("Send").performClick() @@ -113,6 +116,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { onNavigateToVaultEditItem = {}, onNavigateToNewSend = {}, onNavigateToDeleteAccount = {}, + onNavigateToPasswordHistory = {}, ) } runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") } @@ -139,6 +143,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { onNavigateToVaultEditItem = {}, onNavigateToNewSend = {}, onNavigateToDeleteAccount = {}, + onNavigateToPasswordHistory = {}, ) } onNodeWithText("Generator").performClick() @@ -164,6 +169,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { onNavigateToVaultEditItem = {}, onNavigateToNewSend = {}, onNavigateToDeleteAccount = {}, + onNavigateToPasswordHistory = {}, ) } runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") } @@ -190,6 +196,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { onNavigateToVaultEditItem = {}, onNavigateToNewSend = {}, onNavigateToDeleteAccount = {}, + onNavigateToPasswordHistory = {}, ) } onNodeWithText("Settings").performClick() @@ -215,6 +222,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { onNavigateToVaultEditItem = {}, onNavigateToNewSend = {}, onNavigateToDeleteAccount = {}, + onNavigateToPasswordHistory = {}, ) } runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt index afc4f68c84..0ee03e4f09 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt @@ -31,10 +31,14 @@ 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 @Suppress("LargeClass") class GeneratorScreenTest : BaseComposeTest() { + private var onNavigateToPasswordHistoryScreenCalled = false + private val mutableStateFlow = MutableStateFlow( GeneratorState( generatedText = "Placeholder", @@ -59,12 +63,24 @@ class GeneratorScreenTest : BaseComposeTest() { 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 fun `Snackbar should be displayed with correct message on ShowSnackbar event`() { - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - mutableEventFlow.tryEmit(GeneratorEvent.ShowSnackbar("Test Snackbar Message".asText())) composeTestRule @@ -74,10 +90,6 @@ class GeneratorScreenTest : BaseComposeTest() { @Test fun `clicking the Regenerate button should send RegenerateClick action`() { - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule .onNodeWithContentDescription(label = "Generate password") .performClick() @@ -89,10 +101,6 @@ class GeneratorScreenTest : BaseComposeTest() { @Test fun `clicking the Copy button should send CopyClick action`() { - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule .onNodeWithContentDescription(label = "Copy") .performClick() @@ -104,10 +112,6 @@ class GeneratorScreenTest : BaseComposeTest() { @Test fun `clicking a MainStateOption should send MainTypeOptionSelect action`() { - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - // Opens the menu composeTestRule .onNodeWithContentDescription(label = "What would you like to generate?, Password") @@ -135,10 +139,6 @@ class GeneratorScreenTest : BaseComposeTest() { @Test fun `clicking a PasscodeOption should send PasscodeTypeOption action`() { - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - // Opens the menu composeTestRule .onNodeWithContentDescription(label = "Password type, Password") @@ -178,10 +178,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - // Opens the menu composeTestRule .onNodeWithContentDescription(label = "Username type, Plus addressed email") @@ -212,10 +208,6 @@ class GeneratorScreenTest : BaseComposeTest() { @Test fun `in Passcode_Password state, the ViewModel state should update the UI correctly`() { - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule .onNodeWithContentDescription(label = "What would you like to generate?, Password") .assertIsDisplayed() @@ -275,10 +267,6 @@ class GeneratorScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test fun `in Passcode_Password state, adjusting the slider should send SliderLengthChange action with length not equal to default`() { - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule .onNodeWithText("Length") .onSiblings() @@ -308,10 +296,6 @@ class GeneratorScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test fun `in Passcode_Password state, toggling the capital letters toggle should send ToggleCapitalLettersChange action`() { - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule.onNodeWithText("A—Z") .performScrollTo() .performClick() @@ -328,10 +312,6 @@ class GeneratorScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test fun `in Passcode_Password state, toggling the use lowercase toggle should send ToggleLowercaseLettersChange action`() { - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule.onNodeWithText("a—z") .performScrollTo() .performClick() @@ -353,10 +333,6 @@ class GeneratorScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test fun `in Passcode_Password state, toggling the use numbers toggle should send ToggleNumbersChange action`() { - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule.onNodeWithText("0-9") .performScrollTo() .performClick() @@ -373,10 +349,6 @@ class GeneratorScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test fun `in Passcode_Password state, toggling the use special characters toggle should send ToggleSpecialCharactersChange action`() { - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule.onNodeWithText("!@#$%^&*") .performScrollTo() .performClick() @@ -414,10 +386,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule.onNodeWithContentDescription("Minimum numbers, 1") .onChildren() .filterToOne(hasContentDescription("\u2212")) @@ -452,10 +420,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule.onNodeWithContentDescription("Minimum numbers, 1") .onChildren() .filterToOne(hasContentDescription("+")) @@ -490,10 +454,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule.onNodeWithContentDescription("Minimum numbers, $initialMinNumbers") .onChildren() .filterToOne(hasContentDescription("\u2212")) @@ -522,10 +482,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule.onNodeWithContentDescription("Minimum numbers, $initialMinNumbers") .onChildren() .filterToOne(hasContentDescription("+")) @@ -554,10 +510,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule.onNodeWithContentDescription("Minimum special, 1") .onChildren() .filterToOne(hasContentDescription("\u2212")) @@ -592,10 +544,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule.onNodeWithContentDescription("Minimum special, 1") .onChildren() .filterToOne(hasContentDescription("+")) @@ -630,10 +578,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule.onNodeWithContentDescription("Minimum special, $initialSpecialChars") .onChildren() .filterToOne(hasContentDescription("\u2212")) @@ -662,10 +606,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule.onNodeWithContentDescription("Minimum special, $initialSpecialChars") .onChildren() .filterToOne(hasContentDescription("+")) @@ -678,10 +618,6 @@ class GeneratorScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test 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") .performScrollTo() .performClick() @@ -723,10 +659,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - // Unicode for "minus" used for content description composeTestRule .onNodeWithContentDescription("Number of words, $initialNumWords") @@ -762,10 +694,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - // Unicode for "minus" used for content description composeTestRule .onNodeWithContentDescription("Number of words, $initialNumWords") @@ -794,10 +722,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - // Unicode for "minus" used for content description composeTestRule .onNodeWithContentDescription("Number of words, $initialNumWords") @@ -827,10 +751,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule .onNodeWithContentDescription("Number of words, 3") .onChildren() @@ -865,10 +785,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule .onNodeWithText("Capitalize") .performScrollTo() @@ -901,10 +817,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule.onNodeWithText("Include number") .performScrollTo() .performClick() @@ -936,10 +848,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule .onNodeWithText("Word separator") .performScrollTo() @@ -972,10 +880,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - val newEmail = "test@example.com" // 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" // 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") .performScrollTo() .performClick() @@ -1077,10 +973,6 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) - composeTestRule.setContent { - GeneratorScreen(viewModel = viewModel) - } - composeTestRule.onNodeWithText("Include number") .performScrollTo() .performClick() diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreenTest.kt new file mode 100644 index 0000000000..232e684324 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreenTest.kt @@ -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( + extraBufferCapacity = Int.MAX_VALUE, + ) + + private val mutableStateFlow = MutableStateFlow( + PasswordHistoryState(PasswordHistoryState.ViewState.Loading), + ) + + private val viewModel = mockk(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 + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModelTest.kt new file mode 100644 index 0000000000..128961cd71 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModelTest.kt @@ -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 +}