From 141ec500b819f2bc49eb26cae1920751d847446f Mon Sep 17 00:00:00 2001 From: David Perez Date: Wed, 24 Jun 2026 14:23:02 -0500 Subject: [PATCH] PM-37232: Bug: Update error states for the Send and VaultAddEdit Screens (#7098) --- .../ui/tools/feature/send/SendViewModel.kt | 22 +++-- .../feature/addedit/VaultAddEditViewModel.kt | 63 ++++++-------- .../tools/feature/send/SendViewModelTest.kt | 67 +++++++++++---- .../addedit/VaultAddEditViewModelTest.kt | 85 +++++++++++++------ 4 files changed, 149 insertions(+), 88 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt index b42e1f0964..066504b4bd 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt @@ -265,11 +265,19 @@ class SendViewModel @Inject constructor( private fun handleSendDataReceive(action: SendAction.Internal.SendDataReceive) { when (val dataState = action.sendDataState) { is DataState.Error -> { - mutableStateFlow.update { - it.copy( - viewState = SendState.ViewState.Error( - message = BitwardenString.generic_error_message.asText(), - ), + mutableStateFlow.update { state -> + state.copy( + viewState = dataState + .data + ?.toViewState( + baseWebSendUrl = environmentRepo + .environment + .environmentUrlData + .baseWebSendUrl, + ) + ?: SendState.ViewState.Error( + message = BitwardenString.generic_error_message.asText(), + ), dialogState = null, isRefreshing = false, ) @@ -279,9 +287,7 @@ class SendViewModel @Inject constructor( is DataState.NoNetwork, is DataState.Loaded, -> { - val data = dataState - .data - ?: SendData(sendViewList = emptyList()) + val data = dataState.data ?: SendData(sendViewList = emptyList()) mutableStateFlow.update { it.copy( viewState = data.toViewState( diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt index 64d0535475..83f88fc2b1 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt @@ -2164,26 +2164,29 @@ class VaultAddEditViewModel @Inject constructor( private fun handleVaultDataReceive(action: VaultAddEditAction.Internal.VaultDataReceive) { when (val vaultDataState = action.vaultData) { is DataState.Error -> { - mutableStateFlow.update { - it.copy( - viewState = VaultAddEditState.ViewState.Error( - message = BitwardenString.generic_error_message.asText(), - ), - ) - } - } - - is DataState.Loaded -> { - viewModelScope.launch { - sendAction( - VaultAddEditAction.Internal.DetermineContentStateResultReceive( - vaultAddEditState = state.determineContentState( - vaultData = vaultDataState.data, - userData = action.userData, - ), - ), - ) - } + vaultDataState + .data?.let { + viewModelScope.launch { + sendAction( + VaultAddEditAction.Internal.DetermineContentStateResultReceive( + vaultAddEditState = state.determineContentState( + vaultData = it, + userData = action.userData, + ), + ), + ) + } + } + ?: run { + // No data to display, so let's just show the error state + mutableStateFlow.update { + it.copy( + viewState = VaultAddEditState.ViewState.Error( + message = BitwardenString.generic_error_message.asText(), + ), + ) + } + } } DataState.Loading -> { @@ -2196,24 +2199,14 @@ class VaultAddEditViewModel @Inject constructor( } } - is DataState.NoNetwork -> { + is DataState.NoNetwork, + is DataState.Pending, + is DataState.Loaded, + -> { viewModelScope.launch { sendAction( VaultAddEditAction.Internal.DetermineContentStateResultReceive( - state.determineContentState( - vaultData = vaultDataState.data, - userData = action.userData, - ), - ), - ) - } - } - - is DataState.Pending -> { - viewModelScope.launch { - sendAction( - VaultAddEditAction.Internal.DetermineContentStateResultReceive( - state.determineContentState( + vaultAddEditState = state.determineContentState( vaultData = vaultDataState.data, userData = action.userData, ), diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt index 8904110d60..85cf14e874 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt @@ -48,6 +48,7 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import kotlin.time.Duration.Companion.milliseconds @Suppress("LargeClass") class SendViewModelTest : BaseViewModelTest() { @@ -550,26 +551,58 @@ class SendViewModelTest : BaseViewModelTest() { assertEquals(initialState.copy(dialogState = null), viewModel.stateFlow.value) } + @Suppress("MaxLineLength") @Test - fun `VaultRepository SendData Error should update view state to Error`() = runTest { - val dialogState = SendState.DialogState.Loading(BitwardenString.syncing.asText()) - val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState)) + fun `VaultRepository SendData Error should update view state to Error when there is no data`() = + runTest { + val dialogState = SendState.DialogState.Loading(BitwardenString.syncing.asText()) + val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState)) - viewModel.eventFlow.test { - mutableSendDataFlow.value = DataState.Error(Throwable("Fail")) + viewModel.eventFlow.test { + mutableSendDataFlow.value = DataState.Error(Throwable("Fail")) + } + + assertEquals( + DEFAULT_STATE.copy( + viewState = SendState.ViewState.Error( + message = BitwardenString.generic_error_message.asText(), + ), + dialogState = null, + isRefreshing = false, + ), + viewModel.stateFlow.value, + ) } - assertEquals( - DEFAULT_STATE.copy( - viewState = SendState.ViewState.Error( - message = BitwardenString.generic_error_message.asText(), + @Suppress("MaxLineLength") + @Test + fun `VaultRepository SendData Error should update view state to data view state when there is data`() = + runTest { + val dialogState = SendState.DialogState.Loading(BitwardenString.syncing.asText()) + val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState)) + val viewState = mockk() + val sendData = mockk { + every { + toViewState(Environment.Us.environmentUrlData.baseWebSendUrl) + } returns viewState + } + + viewModel.eventFlow.test { + mutableSendDataFlow.value = DataState.Error( + error = Throwable("Fail"), + data = sendData, + ) + } + + assertEquals( + DEFAULT_STATE.copy( + viewState = viewState, + dialogState = null, + isRefreshing = false, ), - dialogState = null, - isRefreshing = false, - ), - viewModel.stateFlow.value, - ) - } + viewModel.stateFlow.value, + ) + } @Test fun `VaultRepository SendData Loaded should update view state`() = runTest { @@ -656,7 +689,7 @@ class SendViewModelTest : BaseViewModelTest() { val viewModel = createViewModel() viewModel.trySendAction(SendAction.RefreshPull) - advanceTimeBy(300) + advanceTimeBy(300.milliseconds) verify(exactly = 1) { vaultRepo.sync(forced = false) } @@ -671,7 +704,7 @@ class SendViewModelTest : BaseViewModelTest() { } returns false viewModel.trySendAction(SendAction.RefreshPull) - advanceTimeBy(300) + advanceTimeBy(300.milliseconds) assertEquals( DEFAULT_STATE.copy( isRefreshing = false, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index e4368c67bb..aec4e406e9 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -545,6 +545,40 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { } } + @Test + fun `vault data Error with data should determine the content state from the available data`() = + runTest { + val stateWithName = createVaultAddItemState( + commonContentViewState = createCommonContentViewState(name = "mockName-1"), + ) + mutableVaultDataFlow.value = DataState.Error( + error = Throwable("Fail"), + data = createVaultData(), + ) + val viewModel = createAddVaultItemViewModel( + createSavedStateHandleWithState( + state = stateWithName, + vaultAddEditType = VaultAddEditType.AddItem, + vaultItemCipherType = VaultItemCipherType.LOGIN, + ), + ) + + assertEquals(stateWithName, viewModel.stateFlow.value) + } + + @Test + fun `vault data Error without data should update view state to Error`() = runTest { + mutableVaultDataFlow.value = DataState.Error(error = Throwable("Fail")) + val viewModel = createAddVaultItemViewModel() + + assertEquals( + VaultAddEditState.ViewState.Error( + message = BitwardenString.generic_error_message.asText(), + ), + viewModel.stateFlow.value.viewState, + ) + } + @Test fun `CloseClick should emit NavigateBack`() = runTest { val viewModel = createAddVaultItemViewModel() @@ -6526,40 +6560,36 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { initialState: VaultAddEditState, type: CustomFieldType, ) { - lateinit var expectedCustomField: VaultAddEditState.Custom - lateinit var action: VaultAddEditAction.Common - lateinit var expectedState: VaultAddEditState.ViewState.Content - - when (type) { + val expectedCustomField: VaultAddEditState.Custom = when (type) { CustomFieldType.LINKED -> { - expectedCustomField = VaultAddEditState.Custom.LinkedField( - "TestId 4", - "Linked Field", - VaultLinkedFieldType.PASSWORD, + VaultAddEditState.Custom.LinkedField( + itemId = "TestId 4", + name = "Linked Field", + vaultLinkedFieldType = VaultLinkedFieldType.PASSWORD, ) } CustomFieldType.HIDDEN -> { - expectedCustomField = VaultAddEditState.Custom.HiddenField( - "TestId 2", - "Test Hidden", - "Updated Test Text", + VaultAddEditState.Custom.HiddenField( + itemId = "TestId 2", + name = "Test Hidden", + value = "Updated Test Text", ) } CustomFieldType.BOOLEAN -> { - expectedCustomField = VaultAddEditState.Custom.BooleanField( - "TestId 3", - "Boolean Field", - false, + VaultAddEditState.Custom.BooleanField( + itemId = "TestId 3", + name = "Boolean Field", + value = false, ) } CustomFieldType.TEXT -> { - expectedCustomField = VaultAddEditState.Custom.TextField( - "TestId 1", - "Test Text", - "Updated Test Text", + VaultAddEditState.Custom.TextField( + itemId = "TestId 1", + name = "Test Text", + value = "Updated Test Text", ) } } @@ -6574,13 +6604,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { val currentContentState = (viewModel.stateFlow.value.viewState as VaultAddEditState.ViewState.Content) - action = VaultAddEditAction.Common.CustomFieldValueChange(expectedCustomField) - expectedState = currentContentState - .copy( - common = currentContentState.common.copy( - customFieldData = listOf(expectedCustomField), - ), - ) + val action = VaultAddEditAction.Common.CustomFieldValueChange(expectedCustomField) + val expectedState = currentContentState.copy( + common = currentContentState.common.copy( + customFieldData = listOf(expectedCustomField), + ), + ) viewModel.trySendAction(action)