From d5d4caea62fbc392ab1f8ac5ffe367a6161ce2c3 Mon Sep 17 00:00:00 2001 From: David Perez Date: Fri, 26 Sep 2025 10:09:35 -0500 Subject: [PATCH] PM-23292: Migrate toasts to snackbars (#5940) --- .../CompleteRegistrationScreen.kt | 13 +++-- .../CompleteRegistrationViewModel.kt | 16 ++---- .../feature/settings/folders/FoldersScreen.kt | 6 ++ .../settings/folders/FoldersViewModel.kt | 41 ++++++++++++- .../folders/addedit/FolderAddEditScreen.kt | 8 --- .../folders/addedit/FolderAddEditViewModel.kt | 24 +++++--- .../manager/snackbar/SnackbarRelay.kt | 3 + .../ui/vault/feature/vault/VaultViewModel.kt | 1 + .../CompleteRegistrationScreenTest.kt | 8 +++ .../CompleteRegistrationViewModelTest.kt | 43 ++++++-------- .../settings/folders/FoldersViewModelTest.kt | 24 ++++++++ .../addedit/FolderAddEditViewModelTest.kt | 57 +++++++++++++++---- 12 files changed, 175 insertions(+), 69 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt index a4911be9eb..3242f73834 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt @@ -1,6 +1,5 @@ package com.x8bit.bitwarden.ui.auth.feature.completeregistration -import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column @@ -26,7 +25,6 @@ import androidx.compose.runtime.setValue 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.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -48,6 +46,9 @@ import com.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.bitwarden.ui.platform.components.field.BitwardenTextField import com.bitwarden.ui.platform.components.model.CardStyle import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold +import com.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost +import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData +import com.bitwarden.ui.platform.components.snackbar.model.rememberBitwardenSnackbarHostState import com.bitwarden.ui.platform.components.text.BitwardenClickableText import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.bitwarden.ui.platform.components.util.rememberVectorPainter @@ -74,16 +75,15 @@ fun CompleteRegistrationScreen( ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() val handler = rememberCompleteRegistrationHandler(viewModel = viewModel) - val context = LocalContext.current - + val snackbarHostState = rememberBitwardenSnackbarHostState() // route OS back actions through the VM to clear the special circumstance BackHandler(onBack = handler.onBackClick) EventsEffect(viewModel) { event -> when (event) { is CompleteRegistrationEvent.NavigateBack -> onNavigateBack.invoke() - is CompleteRegistrationEvent.ShowToast -> { - Toast.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT).show() + is CompleteRegistrationEvent.ShowSnackbar -> { + snackbarHostState.showSnackbar(BitwardenSnackbarData(message = event.message)) } CompleteRegistrationEvent.NavigateToMakePasswordStrong -> onNavigateToPasswordGuidance() @@ -143,6 +143,7 @@ fun CompleteRegistrationScreen( onNavigationIconClick = handler.onBackClick, ) }, + snackbarHost = { BitwardenSnackbarHost(bitwardenHostState = snackbarHostState) }, ) { Column( modifier = Modifier diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt index e2416d5c79..5d46efeb43 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.completeregistration import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.bitwarden.core.data.manager.toast.ToastManager import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.base.util.isValidEmail import com.bitwarden.ui.platform.resource.BitwardenPlurals @@ -54,6 +55,7 @@ class CompleteRegistrationViewModel @Inject constructor( private val authRepository: AuthRepository, private val environmentRepository: EnvironmentRepository, private val specialCircumstanceManager: SpecialCircumstanceManager, + private val toastManager: ToastManager, ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: run { val args = savedStateHandle.toCompleteRegistrationArgs() @@ -146,9 +148,7 @@ class CompleteRegistrationViewModel @Inject constructor( viewModelScope.launch { sendEvent( - CompleteRegistrationEvent.ShowToast( - message = BitwardenString.email_verified.asText(), - ), + CompleteRegistrationEvent.ShowSnackbar(BitwardenString.email_verified.asText()), ) } } @@ -243,11 +243,7 @@ class CompleteRegistrationViewModel @Inject constructor( private fun handleLoginResult(action: Internal.ReceiveLoginResult) { clearDialogState() - sendEvent( - CompleteRegistrationEvent.ShowToast( - message = BitwardenString.account_created_success.asText(), - ), - ) + toastManager.show(messageId = BitwardenString.account_created_success) authRepository.setOnboardingStatus( status = OnboardingStatus.NOT_STARTED, @@ -504,9 +500,9 @@ sealed class CompleteRegistrationEvent { data object NavigateBack : CompleteRegistrationEvent() /** - * Show a toast with the given message. + * Show a snackbar with the given message. */ - data class ShowToast( + data class ShowSnackbar( val message: Text, ) : CompleteRegistrationEvent() diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreen.kt index 5b57045d3a..e4d8d0933d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreen.kt @@ -33,6 +33,8 @@ import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent import com.bitwarden.ui.platform.components.fab.BitwardenFloatingActionButton import com.bitwarden.ui.platform.components.row.BitwardenTextRow import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold +import com.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost +import com.bitwarden.ui.platform.components.snackbar.model.rememberBitwardenSnackbarHostState import com.bitwarden.ui.platform.components.util.rememberVectorPainter import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenString @@ -54,6 +56,7 @@ fun FoldersScreen( viewModel: FoldersViewModel = hiltViewModel(), ) { val state = viewModel.stateFlow.collectAsStateWithLifecycle() + val snackbarHostState = rememberBitwardenSnackbarHostState() EventsEffect(viewModel = viewModel) { event -> when (event) { is FoldersEvent.NavigateBack -> onNavigateBack() @@ -61,6 +64,8 @@ fun FoldersScreen( is FoldersEvent.NavigateToEditFolderScreen -> { onNavigateToEditFolderScreen(event.folderId) } + + is FoldersEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data) } } @@ -92,6 +97,7 @@ fun FoldersScreen( .navigationBarsPadding(), ) }, + snackbarHost = { BitwardenSnackbarHost(bitwardenHostState = snackbarHostState) }, ) { when (val viewState = state.value.viewState) { is FoldersState.ViewState.Content -> { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersViewModel.kt index 142270a500..5983a85b5e 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersViewModel.kt @@ -3,7 +3,9 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.folders import android.os.Parcelable import androidx.lifecycle.viewModelScope import com.bitwarden.core.data.repository.model.DataState +import com.bitwarden.ui.platform.base.BackgroundEvent import com.bitwarden.ui.platform.base.BaseViewModel +import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText @@ -11,8 +13,11 @@ import com.bitwarden.ui.util.concat import com.bitwarden.vault.FolderView import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderDisplayItem +import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay +import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.parcelize.Parcelize @@ -25,6 +30,7 @@ import javax.inject.Inject @HiltViewModel class FoldersViewModel @Inject constructor( vaultRepository: VaultRepository, + snackbarRelayManager: SnackbarRelayManager, ) : BaseViewModel( initialState = FoldersState(viewState = FoldersState.ViewState.Loading), ) { @@ -33,15 +39,31 @@ class FoldersViewModel @Inject constructor( .foldersStateFlow .onEach { sendAction(FoldersAction.Internal.VaultDataReceive(it)) } .launchIn(viewModelScope) + snackbarRelayManager + .getSnackbarDataFlow( + SnackbarRelay.FOLDER_CREATED, + SnackbarRelay.FOLDER_DELETED, + SnackbarRelay.FOLDER_UPDATED, + ) + .map { FoldersAction.Internal.SnackbarDataReceived(it) } + .onEach(::sendAction) + .launchIn(viewModelScope) } override fun handleAction(action: FoldersAction): Unit = when (action) { is FoldersAction.AddFolderButtonClick -> handleAddFolderButtonClicked() is FoldersAction.CloseButtonClick -> handleCloseButtonClicked() - is FoldersAction.Internal.VaultDataReceive -> handleVaultDataReceive(action) + is FoldersAction.Internal -> handleInternalAction(action) is FoldersAction.FolderClick -> handleFolderClick(action) } + private fun handleInternalAction(action: FoldersAction.Internal) { + when (action) { + is FoldersAction.Internal.SnackbarDataReceived -> handleSnackbarDataReceived(action) + is FoldersAction.Internal.VaultDataReceive -> handleVaultDataReceive(action) + } + } + private fun handleFolderClick(action: FoldersAction.FolderClick) { sendEvent(FoldersEvent.NavigateToEditFolderScreen(action.folderId)) } @@ -54,6 +76,10 @@ class FoldersViewModel @Inject constructor( sendEvent(FoldersEvent.NavigateBack) } + private fun handleSnackbarDataReceived(action: FoldersAction.Internal.SnackbarDataReceived) { + sendEvent(FoldersEvent.ShowSnackbar(action.data)) + } + @Suppress("LongMethod") private fun handleVaultDataReceive(action: FoldersAction.Internal.VaultDataReceive) { when (val vaultDataState = action.vaultDataState) { @@ -180,6 +206,13 @@ sealed class FoldersEvent { * Navigates to the screen to edit a folder. */ data class NavigateToEditFolderScreen(val folderId: String) : FoldersEvent() + + /** + * Show a snackbar. + */ + data class ShowSnackbar( + val data: BitwardenSnackbarData, + ) : FoldersEvent(), BackgroundEvent } /** @@ -205,6 +238,12 @@ sealed class FoldersAction { * Actions for internal use by the ViewModel. */ sealed class Internal : FoldersAction() { + /** + * Indicates that the vault folders data has been received. + */ + data class SnackbarDataReceived( + val data: BitwardenSnackbarData, + ) : Internal() /** * Indicates that the vault folders data has been received. diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreen.kt index f9a4c2adc8..b61ee3f8c1 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreen.kt @@ -1,6 +1,5 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.folders.addedit -import android.widget.Toast import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -18,7 +17,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -55,16 +53,10 @@ fun FolderAddEditScreen( viewModel: FolderAddEditViewModel = hiltViewModel(), ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() - val context = LocalContext.current - var shouldShowConfirmationDialog by rememberSaveable { mutableStateOf(false) } - EventsEffect(viewModel = viewModel) { event -> when (event) { is FolderAddEditEvent.NavigateBack -> onNavigateBack.invoke() - is FolderAddEditEvent.ShowToast -> { - Toast.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT).show() - } } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModel.kt index 5845b326b2..e6a8e9977a 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import com.bitwarden.core.DateTime import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.ui.platform.base.BaseViewModel +import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText @@ -16,6 +17,8 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderAddEditType +import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay +import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -35,6 +38,7 @@ private const val KEY_STATE = "state" class FolderAddEditViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val vaultRepository: VaultRepository, + private val relayManager: SnackbarRelayManager, ) : BaseViewModel( // We load the state from the savedStateHandle for testing purposes. initialState = savedStateHandle[KEY_STATE] @@ -263,7 +267,10 @@ class FolderAddEditViewModel @Inject constructor( } is UpdateFolderResult.Success -> { - sendEvent(FolderAddEditEvent.ShowToast(BitwardenString.folder_updated.asText())) + relayManager.sendSnackbarData( + data = BitwardenSnackbarData(BitwardenString.folder_updated.asText()), + relay = SnackbarRelay.FOLDER_UPDATED, + ) sendEvent(FolderAddEditEvent.NavigateBack) } } @@ -289,7 +296,10 @@ class FolderAddEditViewModel @Inject constructor( } is CreateFolderResult.Success -> { - sendEvent(FolderAddEditEvent.ShowToast(BitwardenString.folder_created.asText())) + relayManager.sendSnackbarData( + data = BitwardenSnackbarData(BitwardenString.folder_created.asText()), + relay = SnackbarRelay.FOLDER_CREATED, + ) sendEvent(FolderAddEditEvent.NavigateBack) } } @@ -312,7 +322,10 @@ class FolderAddEditViewModel @Inject constructor( DeleteFolderResult.Success -> { mutableStateFlow.update { it.copy(dialog = null) } - sendEvent(FolderAddEditEvent.ShowToast(BitwardenString.folder_deleted.asText())) + relayManager.sendSnackbarData( + data = BitwardenSnackbarData(BitwardenString.folder_deleted.asText()), + relay = SnackbarRelay.FOLDER_DELETED, + ) sendEvent(event = FolderAddEditEvent.NavigateBack) } } @@ -416,11 +429,6 @@ sealed class FolderAddEditEvent { * Navigate back to previous screen. */ data object NavigateBack : FolderAddEditEvent() - - /** - * Shows a toast with the given [message]. - */ - data class ShowToast(val message: Text) : FolderAddEditEvent() } /** diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelay.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelay.kt index a3d3b2e020..7af129113f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelay.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelay.kt @@ -16,6 +16,9 @@ enum class SnackbarRelay { CIPHER_RESTORED, CIPHER_UPDATED, ENVIRONMENT_SAVED, + FOLDER_CREATED, + FOLDER_DELETED, + FOLDER_UPDATED, LOGIN_APPROVAL, LOGIN_SUCCESS, LOGINS_IMPORTED, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index e9da859444..1fd979fc52 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -189,6 +189,7 @@ class VaultViewModel @Inject constructor( SnackbarRelay.CIPHER_DELETED_SOFT, SnackbarRelay.CIPHER_RESTORED, SnackbarRelay.CIPHER_UPDATED, + SnackbarRelay.FOLDER_CREATED, SnackbarRelay.LOGINS_IMPORTED, ), ) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt index 12f1302512..5646b5a1bb 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt @@ -70,6 +70,14 @@ class CompleteRegistrationScreenTest : BitwardenComposeTest() { } } + @Test + fun `on ShowSnackbar should display snackbar content`() { + val message = "message" + composeTestRule.onNodeWithText(text = message).assertDoesNotExist() + mutableEventFlow.tryEmit(CompleteRegistrationEvent.ShowSnackbar(message = message.asText())) + composeTestRule.onNodeWithText(text = message).assertIsDisplayed() + } + @Test fun `close click should send CloseClick action`() { composeTestRule.onNodeWithContentDescription("Back").performClick() diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt index 492577c821..c2dc990406 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.auth.feature.completeregistration import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test +import com.bitwarden.core.data.manager.toast.ToastManager import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager import com.bitwarden.ui.platform.base.BaseViewModelTest @@ -38,6 +39,7 @@ import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.runs import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow @@ -46,7 +48,6 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -60,13 +61,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() { private val mutableUserStateFlow = MutableStateFlow(null) private val mockAuthRepository = mockk { every { userStateFlow } returns mutableUserStateFlow - coEvery { - login( - email = any(), - password = any(), - ) - } returns LoginResult.Success - + coEvery { login(email = any(), password = any()) } returns LoginResult.Success coEvery { register( email = any(), @@ -77,10 +72,10 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() { isMasterPasswordStrong = any(), ) } returns RegisterResult.Success - - coEvery { - setOnboardingStatus(OnboardingStatus.NOT_STARTED) - } just Runs + coEvery { setOnboardingStatus(OnboardingStatus.NOT_STARTED) } just Runs + } + private val toastManager: ToastManager = mockk { + every { show(messageId = any(), duration = any()) } just runs } private val fakeEnvironmentRepository = FakeEnvironmentRepository() @@ -155,21 +150,18 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() { ) } returns RegisterResult.Success val viewModel = createCompleteRegistrationViewModel(VALID_INPUT_STATE) - viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow -> - assertEquals(VALID_INPUT_STATE, stateFlow.awaitItem()) + viewModel.stateFlow.test { + assertEquals(VALID_INPUT_STATE, awaitItem()) viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick) assertEquals( VALID_INPUT_STATE.copy(dialog = CompleteRegistrationDialog.Loading), - stateFlow.awaitItem(), - ) - assertEquals( - CompleteRegistrationEvent.ShowToast( - BitwardenString.account_created_success.asText(), - ), - eventFlow.awaitItem(), + awaitItem(), ) // Make sure loading dialog is hidden: - assertEquals(VALID_INPUT_STATE, stateFlow.awaitItem()) + assertEquals(VALID_INPUT_STATE, awaitItem()) + } + verify(exactly = 1) { + toastManager.show(messageId = BitwardenString.account_created_success) } } @@ -224,7 +216,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() { val viewModel = createCompleteRegistrationViewModel(VALID_INPUT_STATE) viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick) viewModel.eventFlow.test { - assertTrue(awaitItem() is CompleteRegistrationEvent.ShowToast) expectNoEvents() } verify(exactly = 1) { @@ -246,7 +237,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() { val viewModel = createCompleteRegistrationViewModel(VALID_INPUT_STATE) viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick) viewModel.eventFlow.test { - assertTrue(awaitItem() is CompleteRegistrationEvent.ShowToast) assertEquals( CompleteRegistrationEvent.NavigateToLogin(EMAIL), awaitItem(), @@ -404,13 +394,13 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() { } @Test - fun `On init should show toast if from email is true`() = runTest { + fun `On init should show snackbar if from email is true`() = runTest { val viewModel = createCompleteRegistrationViewModel( DEFAULT_STATE.copy(fromEmail = true), ) viewModel.eventFlow.test { assertEquals( - CompleteRegistrationEvent.ShowToast(BitwardenString.email_verified.asText()), + CompleteRegistrationEvent.ShowSnackbar(BitwardenString.email_verified.asText()), awaitItem(), ) } @@ -666,6 +656,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() { environmentRepository = fakeEnvironmentRepository, specialCircumstanceManager = specialCircumstanceManager, generatorRepository = generatorRepository, + toastManager = toastManager, ) companion object { diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersViewModelTest.kt index 92f4a3d6e6..6224f2fd82 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersViewModelTest.kt @@ -3,13 +3,16 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.folders import app.cash.turbine.test import com.bitwarden.core.DateTime import com.bitwarden.core.data.repository.model.DataState +import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import com.bitwarden.ui.platform.base.BaseViewModelTest +import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.concat import com.bitwarden.vault.FolderView import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderDisplayItem +import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.flow.MutableStateFlow @@ -25,6 +28,26 @@ class FoldersViewModelTest : BaseViewModelTest() { private val vaultRepository: VaultRepository = mockk { every { foldersStateFlow } returns mutableFoldersStateFlow } + private val mutableSnackbarDataFlow = bufferedMutableSharedFlow() + private val snackbarRelayManager: SnackbarRelayManager = mockk { + every { + getSnackbarDataFlow(relay = any(), relays = anyVararg()) + } returns mutableSnackbarDataFlow + } + + @Test + fun `on snackbar data received should emit ShowSnackbar`() = runTest { + val viewModel = createViewModel() + + val data = BitwardenSnackbarData(message = "Snackbar!".asText()) + viewModel.eventFlow.test { + mutableSnackbarDataFlow.emit(data) + assertEquals( + FoldersEvent.ShowSnackbar(data = data), + awaitItem(), + ) + } + } @Test fun `BackClick should emit NavigateBack`() = runTest { @@ -161,6 +184,7 @@ class FoldersViewModelTest : BaseViewModelTest() { private fun createViewModel(): FoldersViewModel = FoldersViewModel( vaultRepository = vaultRepository, + snackbarRelayManager = snackbarRelayManager, ) private fun createFolderState( diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModelTest.kt index ad086fe3c3..680172b29d 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModelTest.kt @@ -5,6 +5,7 @@ import app.cash.turbine.test import com.bitwarden.core.DateTime import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.ui.platform.base.BaseViewModelTest +import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.concat @@ -14,11 +15,15 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderAddEditType +import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay +import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every +import io.mockk.just import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.runs import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow @@ -38,6 +43,9 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { private val vaultRepository: VaultRepository = mockk { every { getVaultFolderStateFlow(DEFAULT_EDIT_ITEM_ID) } returns mutableFoldersStateFlow } + private val relayManager: SnackbarRelayManager = mockk { + every { sendSnackbarData(data = any(), relay = any()) } just runs + } @BeforeEach fun setup() { @@ -125,15 +133,17 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { viewModel.trySendAction(FolderAddEditAction.DeleteClick) viewModel.eventFlow.test { - assertEquals( - FolderAddEditEvent.ShowToast(BitwardenString.folder_deleted.asText()), - awaitItem(), - ) assertEquals( FolderAddEditEvent.NavigateBack, awaitItem(), ) } + verify(exactly = 1) { + relayManager.sendSnackbarData( + data = BitwardenSnackbarData(BitwardenString.folder_deleted.asText()), + relay = SnackbarRelay.FOLDER_DELETED, + ) + } } @Suppress("MaxLineLength") @@ -180,6 +190,12 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { assertEquals(stateWithDialog, awaitItem()) assertEquals(stateWithoutDialog, awaitItem()) } + verify(exactly = 1) { + relayManager.sendSnackbarData( + data = BitwardenSnackbarData(BitwardenString.folder_deleted.asText()), + relay = SnackbarRelay.FOLDER_DELETED, + ) + } } @Suppress("MaxLineLength") @@ -322,6 +338,12 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { assertEquals(stateWithDialog, awaitItem()) assertEquals(stateWithoutDialog, awaitItem()) } + verify(exactly = 1) { + relayManager.sendSnackbarData( + data = BitwardenSnackbarData(BitwardenString.folder_created.asText()), + relay = SnackbarRelay.FOLDER_CREATED, + ) + } } @Suppress("MaxLineLength") @@ -348,9 +370,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { vaultRepository.createFolder(any()) } returns CreateFolderResult.Success(mockk()) viewModel.trySendAction(FolderAddEditAction.SaveClick) - coVerify( - exactly = 1, - ) { + coVerify(exactly = 1) { vaultRepository.createFolder( folderView = FolderView( name = DEFAULT_FOLDER_NAME, @@ -359,6 +379,12 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { ), ) } + verify(exactly = 1) { + relayManager.sendSnackbarData( + data = BitwardenSnackbarData(BitwardenString.folder_created.asText()), + relay = SnackbarRelay.FOLDER_CREATED, + ) + } unmockkStatic(DateTime::class) } @@ -387,9 +413,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { vaultRepository.createFolder(any()) } returns CreateFolderResult.Success(mockk()) viewModel.trySendAction(FolderAddEditAction.SaveClick) - coVerify( - exactly = 1, - ) { + coVerify(exactly = 1) { vaultRepository.createFolder( folderView = FolderView( name = "$parentFolderName/$DEFAULT_FOLDER_NAME", @@ -398,6 +422,12 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { ), ) } + verify(exactly = 1) { + relayManager.sendSnackbarData( + data = BitwardenSnackbarData(BitwardenString.folder_created.asText()), + relay = SnackbarRelay.FOLDER_CREATED, + ) + } unmockkStatic(DateTime::class) } @@ -483,6 +513,12 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { assertEquals(stateWithDialog, awaitItem()) assertEquals(stateWithoutDialog, awaitItem()) } + verify(exactly = 1) { + relayManager.sendSnackbarData( + data = BitwardenSnackbarData(BitwardenString.folder_updated.asText()), + relay = SnackbarRelay.FOLDER_UPDATED, + ) + } } @Test @@ -758,6 +794,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { ): FolderAddEditViewModel = FolderAddEditViewModel( savedStateHandle = savedStateHandle, vaultRepository = vaultRepository, + relayManager = relayManager, ) }