Subscribe to vault SendData (#485)

This commit is contained in:
David Perez 2024-01-03 14:39:41 -06:00 committed by Álison Fernandes
parent 11fcaa6678
commit c5989d117e
5 changed files with 242 additions and 19 deletions

View file

@ -1,11 +1,16 @@
package com.x8bit.bitwarden.ui.tools.feature.send package com.x8bit.bitwarden.ui.tools.feature.send
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
/** /**
@ -17,6 +22,19 @@ fun SendContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LazyColumn(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 { item {
Spacer(modifier = Modifier.height(88.dp)) Spacer(modifier = Modifier.height(88.dp))
Spacer(modifier = Modifier.navigationBarsPadding()) Spacer(modifier = Modifier.navigationBarsPadding())

View file

@ -3,15 +3,21 @@ package com.x8bit.bitwarden.ui.tools.feature.send
import android.os.Parcelable import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope 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.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.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text 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.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 com.x8bit.bitwarden.ui.vault.feature.item.VaultItemScreen
import dagger.hilt.android.lifecycle.HiltViewModel 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.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import javax.inject.Inject import javax.inject.Inject
@ -33,11 +39,11 @@ class SendViewModel @Inject constructor(
) { ) {
init { init {
// TODO: Remove this once we start listening to real vault data BIT-481 vaultRepo
viewModelScope.launch { .sendDataStateFlow
delay(timeMillis = 3_000L) .map { SendAction.Internal.SendDataReceive(it) }
mutableStateFlow.update { it.copy(viewState = SendState.ViewState.Empty) } .onEach(::sendAction)
} .launchIn(viewModelScope)
} }
override fun handleAction(action: SendAction): Unit = when (action) { override fun handleAction(action: SendAction): Unit = when (action) {
@ -47,6 +53,55 @@ class SendViewModel @Inject constructor(
SendAction.RefreshClick -> handleRefreshClick() SendAction.RefreshClick -> handleRefreshClick()
SendAction.SearchClick -> handleSearchClick() SendAction.SearchClick -> handleSearchClick()
SendAction.SyncClick -> handleSyncClick() 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() { private fun handleAboutSendClick() {
@ -164,6 +219,18 @@ sealed class SendAction {
* User clicked the sync button. * User clicked the sync button.
*/ */
data object SyncClick : SendAction() 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<SendData>,
) : Internal()
}
} }
/** /**

View file

@ -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<SendView>.toSendContent(): SendState.ViewState.Content {
// TODO: Populate with real data BIT-481
return SendState.ViewState.Content
}

View file

@ -2,21 +2,44 @@ package com.x8bit.bitwarden.ui.tools.feature.send
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test 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.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.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText 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.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class SendViewModelTest : BaseViewModelTest() { class SendViewModelTest : BaseViewModelTest() {
private val vaultRepo: VaultRepository = mockk() private val mutableSendDataFlow = MutableStateFlow<DataState<SendData>>(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 @Test
fun `initial state should be Empty`() { fun `initial state should be Empty`() {
@ -24,13 +47,6 @@ class SendViewModelTest : BaseViewModelTest() {
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value) assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
} }
@Test
fun `initial state should read from saved state when present`() {
val savedState = mockk<SendState>()
val viewModel = createViewModel(state = savedState)
assertEquals(savedState, viewModel.stateFlow.value)
}
@Test @Test
fun `AboutSendClick should emit NavigateToAboutSend`() = runTest { fun `AboutSendClick should emit NavigateToAboutSend`() = runTest {
val viewModel = createViewModel() 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<SendData> {
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<SendData> {
every { toViewState() } returns viewState
}
mutableSendDataFlow.value = DataState.Pending(sendData)
assertEquals(SendState(viewState = viewState), viewModel.stateFlow.value)
}
private fun createViewModel( private fun createViewModel(
state: SendState? = null, state: SendState? = null,
vaultRepository: VaultRepository = vaultRepo, 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( private val DEFAULT_STATE: SendState = SendState(
viewState = SendState.ViewState.Loading, viewState = SendState.ViewState.Loading,
) )
private val DEFAULT_ERROR_STATE: SendState = DEFAULT_STATE.copy(
viewState = SendState.ViewState.Error("Fail".asText()),
)

View file

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