From c5989d117e630f66f2a2919aa8e1881bb5c45f64 Mon Sep 17 00:00:00 2001 From: David Perez Date: Wed, 3 Jan 2024 14:39:41 -0600 Subject: [PATCH] Subscribe to vault SendData (#485) --- .../ui/tools/feature/send/SendContent.kt | 18 +++ .../ui/tools/feature/send/SendViewModel.kt | 81 +++++++++++-- .../feature/send/util/SendDataExtensions.kt | 20 ++++ .../tools/feature/send/SendViewModelTest.kt | 111 ++++++++++++++++-- .../send/util/SendDataExtensionsTest.kt | 31 +++++ 5 files changed, 242 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/util/SendDataExtensions.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/util/SendDataExtensionsTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendContent.kt index 357fbe3344..585e15adef 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendContent.kt @@ -1,11 +1,16 @@ package com.x8bit.bitwarden.ui.tools.feature.send import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp /** @@ -17,6 +22,19 @@ fun SendContent( modifier: Modifier = Modifier, ) { LazyColumn(modifier = modifier) { + item { + // TODO: Populate with real data BIT-481 + Text( + text = "Not yet implemented", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) + } + item { Spacer(modifier = Modifier.height(88.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt index 521b73561e..832a17d58b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt @@ -3,15 +3,21 @@ package com.x8bit.bitwarden.ui.tools.feature.send import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.vault.repository.VaultRepository +import com.x8bit.bitwarden.data.vault.repository.model.SendData import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.asText +import com.x8bit.bitwarden.ui.platform.base.util.concat +import com.x8bit.bitwarden.ui.tools.feature.send.util.toViewState import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemScreen import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import javax.inject.Inject @@ -33,11 +39,11 @@ class SendViewModel @Inject constructor( ) { init { - // TODO: Remove this once we start listening to real vault data BIT-481 - viewModelScope.launch { - delay(timeMillis = 3_000L) - mutableStateFlow.update { it.copy(viewState = SendState.ViewState.Empty) } - } + vaultRepo + .sendDataStateFlow + .map { SendAction.Internal.SendDataReceive(it) } + .onEach(::sendAction) + .launchIn(viewModelScope) } override fun handleAction(action: SendAction): Unit = when (action) { @@ -47,6 +53,55 @@ class SendViewModel @Inject constructor( SendAction.RefreshClick -> handleRefreshClick() SendAction.SearchClick -> handleSearchClick() SendAction.SyncClick -> handleSyncClick() + is SendAction.Internal -> handleInternalAction(action) + } + + private fun handleInternalAction(action: SendAction.Internal): Unit = when (action) { + is SendAction.Internal.SendDataReceive -> handleSendDataReceive(action) + } + + private fun handleSendDataReceive(action: SendAction.Internal.SendDataReceive) { + when (val dataState = action.sendDataState) { + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + viewState = SendState.ViewState.Error( + message = R.string.generic_error_message.asText(), + ), + ) + } + } + + is DataState.Loaded -> { + mutableStateFlow.update { + it.copy(viewState = dataState.data.toViewState()) + } + } + + DataState.Loading -> { + mutableStateFlow.update { + it.copy(viewState = SendState.ViewState.Loading) + } + } + + is DataState.NoNetwork -> { + mutableStateFlow.update { + it.copy( + viewState = SendState.ViewState.Error( + message = R.string.internet_connection_required_title + .asText() + .concat(R.string.internet_connection_required_message.asText()), + ), + ) + } + } + + is DataState.Pending -> { + mutableStateFlow.update { + it.copy(viewState = dataState.data.toViewState()) + } + } + } } private fun handleAboutSendClick() { @@ -164,6 +219,18 @@ sealed class SendAction { * User clicked the sync button. */ data object SyncClick : SendAction() + + /** + * Models actions that the [SendViewModel] itself will send. + */ + sealed class Internal : SendAction() { + /** + * Indicates that the send data has been received. + */ + data class SendDataReceive( + val sendDataState: DataState, + ) : Internal() + } } /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/util/SendDataExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/util/SendDataExtensions.kt new file mode 100644 index 0000000000..c9f474855e --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/util/SendDataExtensions.kt @@ -0,0 +1,20 @@ +package com.x8bit.bitwarden.ui.tools.feature.send.util + +import com.bitwarden.core.SendView +import com.x8bit.bitwarden.data.vault.repository.model.SendData +import com.x8bit.bitwarden.ui.tools.feature.send.SendState + +/** + * Transforms [SendData] into [SendState.ViewState]. + */ +fun SendData.toViewState(): SendState.ViewState = + this + .sendViewList + .takeUnless { it.isEmpty() } + ?.toSendContent() + ?: SendState.ViewState.Empty + +private fun List.toSendContent(): SendState.ViewState.Content { + // TODO: Populate with real data BIT-481 + return SendState.ViewState.Content +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt index 2da90850d5..995252dfef 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt @@ -2,21 +2,44 @@ package com.x8bit.bitwarden.ui.tools.feature.send import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.vault.repository.VaultRepository +import com.x8bit.bitwarden.data.vault.repository.model.SendData import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.util.asText +import com.x8bit.bitwarden.ui.platform.base.util.concat +import com.x8bit.bitwarden.ui.tools.feature.send.util.toViewState 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 import kotlinx.coroutines.test.runTest +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 class SendViewModelTest : BaseViewModelTest() { - private val vaultRepo: VaultRepository = mockk() + private val mutableSendDataFlow = MutableStateFlow>(DataState.Loading) + private val vaultRepo: VaultRepository = mockk { + every { sendDataStateFlow } returns mutableSendDataFlow + } + + @BeforeEach + fun setup() { + mockkStatic(SEND_DATA_EXTENSIONS_PATH) + } + + @AfterEach + fun tearDown() { + unmockkStatic(SEND_DATA_EXTENSIONS_PATH) + } @Test fun `initial state should be Empty`() { @@ -24,13 +47,6 @@ class SendViewModelTest : BaseViewModelTest() { assertEquals(DEFAULT_STATE, viewModel.stateFlow.value) } - @Test - fun `initial state should read from saved state when present`() { - val savedState = mockk() - val viewModel = createViewModel(state = savedState) - assertEquals(savedState, viewModel.stateFlow.value) - } - @Test fun `AboutSendClick should emit NavigateToAboutSend`() = runTest { val viewModel = createViewModel() @@ -94,6 +110,78 @@ class SendViewModelTest : BaseViewModelTest() { } } + @Test + fun `VaultRepository SendData Error should update view state to Error`() { + val viewModel = createViewModel() + + mutableSendDataFlow.value = DataState.Error(Throwable("Fail")) + + assertEquals( + SendState( + viewState = SendState.ViewState.Error( + message = R.string.generic_error_message.asText(), + ), + ), + viewModel.stateFlow.value, + ) + } + + @Test + fun `VaultRepository SendData Loaded should update view state`() { + val viewModel = createViewModel() + val viewState = SendState.ViewState.Content + val sendData = mockk { + every { toViewState() } returns viewState + } + + mutableSendDataFlow.value = DataState.Loaded(sendData) + + assertEquals(SendState(viewState = viewState), viewModel.stateFlow.value) + } + + @Test + fun `VaultRepository SendData Loading should update view state to Loading`() { + val viewModel = createViewModel() + + mutableSendDataFlow.value = DataState.Loading + + assertEquals( + SendState(viewState = SendState.ViewState.Loading), + viewModel.stateFlow.value, + ) + } + + @Test + fun `VaultRepository SendData NoNetwork should update view state to Error`() { + val viewModel = createViewModel() + + mutableSendDataFlow.value = DataState.NoNetwork() + + assertEquals( + SendState( + viewState = SendState.ViewState.Error( + message = R.string.internet_connection_required_title + .asText() + .concat(R.string.internet_connection_required_message.asText()), + ), + ), + viewModel.stateFlow.value, + ) + } + + @Test + fun `VaultRepository SendData Pending should update view state`() { + val viewModel = createViewModel() + val viewState = SendState.ViewState.Content + val sendData = mockk { + every { toViewState() } returns viewState + } + + mutableSendDataFlow.value = DataState.Pending(sendData) + + assertEquals(SendState(viewState = viewState), viewModel.stateFlow.value) + } + private fun createViewModel( state: SendState? = null, vaultRepository: VaultRepository = vaultRepo, @@ -105,10 +193,9 @@ class SendViewModelTest : BaseViewModelTest() { ) } +private const val SEND_DATA_EXTENSIONS_PATH: String = + "com.x8bit.bitwarden.ui.tools.feature.send.util.SendDataExtensionsKt" + private val DEFAULT_STATE: SendState = SendState( viewState = SendState.ViewState.Loading, ) - -private val DEFAULT_ERROR_STATE: SendState = DEFAULT_STATE.copy( - viewState = SendState.ViewState.Error("Fail".asText()), -) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/util/SendDataExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/util/SendDataExtensionsTest.kt new file mode 100644 index 0000000000..2bd5ed79b9 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/util/SendDataExtensionsTest.kt @@ -0,0 +1,31 @@ +package com.x8bit.bitwarden.ui.tools.feature.send.util + +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView +import com.x8bit.bitwarden.data.vault.repository.model.SendData +import com.x8bit.bitwarden.ui.tools.feature.send.SendState +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class SendDataExtensionsTest { + + @Test + fun `toViewState should return Empty when SendData is empty`() { + val sendData = SendData(emptyList()) + + val result = sendData.toViewState() + + assertEquals(SendState.ViewState.Empty, result) + } + + @Test + fun `toViewState should return Content when SendData is not empty`() { + val list = listOf( + createMockSendView(number = 1), + ) + val sendData = SendData(list) + + val result = sendData.toViewState() + + assertEquals(SendState.ViewState.Content, result) + } +}