mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
Adds the initial boilerplate for VaultEditItemScreen (#326)
This commit is contained in:
parent
a106f0852a
commit
a7dc5fe08f
5 changed files with 346 additions and 0 deletions
|
@ -0,0 +1,53 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.edit
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import com.x8bit.bitwarden.ui.platform.theme.TransitionProviders
|
||||
|
||||
private const val VAULT_EDIT_ITEM_PREFIX = "vault_edit_item"
|
||||
private const val VAULT_EDIT_ITEM_ID = "vault_edit_item_id"
|
||||
private const val VAULT_EDIT_ROUTE = "$VAULT_EDIT_ITEM_PREFIX/{$VAULT_EDIT_ITEM_ID}"
|
||||
|
||||
/**
|
||||
* Class to retrieve vault item arguments from the [SavedStateHandle].
|
||||
*/
|
||||
class VaultEditItemArgs(val vaultItemId: String) {
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
checkNotNull(savedStateHandle[VAULT_EDIT_ITEM_ID]) as String,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the vault edit item screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.vaultEditItemDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
composable(
|
||||
route = VAULT_EDIT_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(VAULT_EDIT_ITEM_ID) { type = NavType.StringType },
|
||||
),
|
||||
enterTransition = TransitionProviders.Enter.slideUp,
|
||||
exitTransition = TransitionProviders.Exit.slideDown,
|
||||
popEnterTransition = TransitionProviders.Enter.slideUp,
|
||||
popExitTransition = TransitionProviders.Exit.slideDown,
|
||||
) {
|
||||
VaultEditItemScreen(onNavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the vault edit item screen.
|
||||
*/
|
||||
fun NavController.navigateToVaultEditItem(
|
||||
vaultItemId: String,
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
navigate("$VAULT_EDIT_ITEM_PREFIX/$vaultItemId", navOptions)
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.edit
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
|
||||
/**
|
||||
* Top level composable for the vault edit item screen.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun VaultEditItemScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: VaultEditItemViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
VaultEditItemEvent.NavigateBack -> onNavigateBack()
|
||||
|
||||
is VaultEditItemEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.message(resources), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.edit_item),
|
||||
navigationIcon = painterResource(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultEditItemAction.CloseClick) }
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
actions = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.save),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultEditItemAction.SaveClick) }
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
) { innerPadding -> }
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.edit
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
||||
/**
|
||||
* ViewModel responsible for handling user interactions in the vault edit item screen
|
||||
*/
|
||||
@HiltViewModel
|
||||
class VaultEditItemViewModel @Inject constructor(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<VaultEditItemState, VaultEditItemEvent, VaultEditItemAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: VaultEditItemState(
|
||||
vaultItemId = VaultEditItemArgs(savedStateHandle).vaultItemId,
|
||||
),
|
||||
) {
|
||||
|
||||
init {
|
||||
stateFlow
|
||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: VaultEditItemAction) {
|
||||
when (action) {
|
||||
VaultEditItemAction.CloseClick -> handleCloseClick()
|
||||
VaultEditItemAction.SaveClick -> handleSaveClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCloseClick() {
|
||||
sendEvent(VaultEditItemEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleSaveClick() {
|
||||
// TODO: Persist the data to the vault (BIT-502)
|
||||
sendEvent(VaultEditItemEvent.ShowToast("Not yet implemented".asText()))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the state for editing an item to the vault.
|
||||
*/
|
||||
@Parcelize
|
||||
data class VaultEditItemState(
|
||||
val vaultItemId: String,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
* Represents a set of events that can be emitted during the process of editing an item in the
|
||||
* vault. Each subclass of this sealed class denotes a distinct event that can occur.
|
||||
*/
|
||||
sealed class VaultEditItemEvent {
|
||||
/**
|
||||
* Shows a toast with the given [message].
|
||||
*/
|
||||
data class ShowToast(
|
||||
val message: Text,
|
||||
) : VaultEditItemEvent()
|
||||
|
||||
/**
|
||||
* Navigate back to previous screen.
|
||||
*/
|
||||
data object NavigateBack : VaultEditItemEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a set of actions related to the process of editing an item in the vault.
|
||||
* Each subclass of this sealed class denotes a distinct action that can be taken.
|
||||
*/
|
||||
sealed class VaultEditItemAction {
|
||||
/**
|
||||
* Represents the action when the save button is clicked.
|
||||
*/
|
||||
data object SaveClick : VaultEditItemAction()
|
||||
|
||||
/**
|
||||
* User clicked close.
|
||||
*/
|
||||
data object CloseClick : VaultEditItemAction()
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.edit
|
||||
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class VaultEditItemScreenTest : BaseComposeTest() {
|
||||
|
||||
private var onNavigateBackCalled = false
|
||||
|
||||
private val mutableEventFlow = MutableSharedFlow<VaultEditItemEvent>(
|
||||
extraBufferCapacity = Int.MAX_VALUE,
|
||||
)
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val viewModel = mockk<VaultEditItemViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
composeTestRule.setContent {
|
||||
VaultEditItemScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateBack event should invoke onNavigateBack`() {
|
||||
mutableEventFlow.tryEmit(VaultEditItemEvent.NavigateBack)
|
||||
assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on close click should send CloseClick event`() {
|
||||
composeTestRule.onNodeWithContentDescription("Close").performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(VaultEditItemAction.CloseClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on save click should send SaveClick event`() {
|
||||
composeTestRule.onNodeWithText("Save").performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(VaultEditItemAction.SaveClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val DEFAULT_VAULT_ITEM_ID: String = "vault_item_id"
|
||||
|
||||
private val DEFAULT_STATE: VaultEditItemState = VaultEditItemState(
|
||||
vaultItemId = DEFAULT_VAULT_ITEM_ID,
|
||||
)
|
|
@ -0,0 +1,61 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.edit
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class VaultEditItemViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when not set`() {
|
||||
val viewModel = createViewModel(state = null)
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when set`() {
|
||||
val differentVaultItemId = "something_different"
|
||||
val state = DEFAULT_STATE.copy(vaultItemId = differentVaultItemId)
|
||||
val viewModel = createViewModel(state = state)
|
||||
|
||||
assertEquals(state, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CloseClick should emit NavigateBack`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultEditItemAction.CloseClick)
|
||||
assertEquals(VaultEditItemEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on SaveClick should emit ShowToast`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultEditItemAction.SaveClick)
|
||||
assertEquals(VaultEditItemEvent.ShowToast("Not yet implemented".asText()), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
state: VaultEditItemState? = DEFAULT_STATE,
|
||||
vaultItemId: String = VAULT_ITEM_ID,
|
||||
): VaultEditItemViewModel = VaultEditItemViewModel(
|
||||
savedStateHandle = SavedStateHandle().apply {
|
||||
set("state", state)
|
||||
set("vault_edit_item_id", vaultItemId)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private const val VAULT_ITEM_ID: String = "vault_item_id"
|
||||
|
||||
private val DEFAULT_STATE: VaultEditItemState = VaultEditItemState(
|
||||
vaultItemId = VAULT_ITEM_ID,
|
||||
)
|
Loading…
Reference in a new issue