PM-23292: Migrate toasts to snackbars (#5940)

This commit is contained in:
David Perez 2025-09-26 10:09:35 -05:00 committed by GitHub
parent 7bf4acbb28
commit d5d4caea62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 175 additions and 69 deletions

View File

@ -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

View File

@ -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<CompleteRegistrationState, CompleteRegistrationEvent, CompleteRegistrationAction>(
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()

View File

@ -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 -> {

View File

@ -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<FoldersState, FoldersEvent, FoldersAction>(
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.

View File

@ -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()
}
}
}

View File

@ -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<FolderAddEditState, FolderAddEditEvent, FolderAddEditAction>(
// 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()
}
/**

View File

@ -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,

View File

@ -189,6 +189,7 @@ class VaultViewModel @Inject constructor(
SnackbarRelay.CIPHER_DELETED_SOFT,
SnackbarRelay.CIPHER_RESTORED,
SnackbarRelay.CIPHER_UPDATED,
SnackbarRelay.FOLDER_CREATED,
SnackbarRelay.LOGINS_IMPORTED,
),
)

View File

@ -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()

View File

@ -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<UserState?>(null)
private val mockAuthRepository = mockk<AuthRepository> {
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 {

View File

@ -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<BitwardenSnackbarData>()
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(

View File

@ -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,
)
}