mirror of
https://github.com/bitwarden/android.git
synced 2025-02-17 12:30:00 +03:00
BIT-844: Move to Organization UI (#638)
This commit is contained in:
parent
10bf584c90
commit
21a9802ed4
7 changed files with 791 additions and 6 deletions
|
@ -0,0 +1,114 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.movetoorganization
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenMultiSelectButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Content view for the [VaultMoveToOrganizationScreen].
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun VaultMoveToOrganizationContent(
|
||||
state: VaultMoveToOrganizationState.ViewState.Content,
|
||||
organizationSelect: (VaultMoveToOrganizationState.ViewState.Content.Organization) -> Unit,
|
||||
collectionSelect: (VaultMoveToOrganizationState.ViewState.Content.Collection) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenMultiSelectButton(
|
||||
label = stringResource(id = R.string.organization),
|
||||
options = state
|
||||
.organizations
|
||||
.map { it.name }
|
||||
.toImmutableList(),
|
||||
selectedOption = state.selectedOrganization.name,
|
||||
onOptionSelected = { selectedString ->
|
||||
organizationSelect(
|
||||
state
|
||||
.organizations
|
||||
.first { it.name == selectedString },
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.move_to_org_desc),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.collections),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
if (state.selectedOrganization.collections.isNotEmpty()) {
|
||||
items(state.selectedOrganization.collections) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenWideSwitch(
|
||||
label = it.name,
|
||||
isChecked = it.isSelected,
|
||||
onCheckedChange = { _ ->
|
||||
collectionSelect(it)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.no_collections_to_list),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.movetoorganization
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
|
||||
/**
|
||||
* Empty view for the [VaultMoveToOrganizationScreen].
|
||||
*/
|
||||
@Composable
|
||||
fun VaultMoveToOrganizationEmpty(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.no_orgs_to_list),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -20,10 +19,16 @@ 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.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||
|
||||
/**
|
||||
* Displays the vault move to organization screen.
|
||||
|
@ -49,16 +54,54 @@ fun VaultMoveToOrganizationScreen(
|
|||
closeClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultMoveToOrganizationAction.BackClick) }
|
||||
},
|
||||
moveClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultMoveToOrganizationAction.MoveClick) }
|
||||
},
|
||||
dismissClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultMoveToOrganizationAction.DismissClick) }
|
||||
},
|
||||
organizationSelect = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultMoveToOrganizationAction.OrganizationSelect(it)) }
|
||||
},
|
||||
collectionSelect = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultMoveToOrganizationAction.CollectionSelect(it)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun VaultMoveToOrganizationScaffold(
|
||||
state: VaultMoveToOrganizationState,
|
||||
closeClick: () -> Unit,
|
||||
moveClick: () -> Unit,
|
||||
dismissClick: () -> Unit,
|
||||
organizationSelect: (VaultMoveToOrganizationState.ViewState.Content.Organization) -> Unit,
|
||||
collectionSelect: (VaultMoveToOrganizationState.ViewState.Content.Collection) -> Unit,
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
when (val dialog = state.dialogState) {
|
||||
is VaultMoveToOrganizationState.DialogState.Error -> {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = dialog.message,
|
||||
),
|
||||
onDismissRequest = dismissClick,
|
||||
)
|
||||
}
|
||||
|
||||
is VaultMoveToOrganizationState.DialogState.Loading -> {
|
||||
BitwardenLoadingDialog(
|
||||
visibilityState = LoadingDialogState.Shown(
|
||||
text = dialog.message,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
@ -70,6 +113,14 @@ private fun VaultMoveToOrganizationScaffold(
|
|||
navigationIcon = painterResource(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
onNavigationIconClick = closeClick,
|
||||
actions = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.move),
|
||||
onClick = moveClick,
|
||||
isEnabled = state.viewState is
|
||||
VaultMoveToOrganizationState.ViewState.Content,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
|
@ -80,18 +131,28 @@ private fun VaultMoveToOrganizationScaffold(
|
|||
|
||||
when (state.viewState) {
|
||||
is VaultMoveToOrganizationState.ViewState.Content -> {
|
||||
// TODO add real views in BIT-844 UI
|
||||
Text(text = "Content")
|
||||
VaultMoveToOrganizationContent(
|
||||
state = state.viewState,
|
||||
organizationSelect = organizationSelect,
|
||||
collectionSelect = collectionSelect,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
is VaultMoveToOrganizationState.ViewState.Error -> {
|
||||
BitwardenErrorContent(
|
||||
message = state.viewState.message(),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
is VaultMoveToOrganizationState.ViewState.Loading -> {
|
||||
BitwardenLoadingContent(modifier = modifier)
|
||||
}
|
||||
|
||||
is VaultMoveToOrganizationState.ViewState.Empty -> {
|
||||
VaultMoveToOrganizationEmpty(modifier = modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,15 @@ package com.x8bit.bitwarden.ui.vault.feature.movetoorganization
|
|||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
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.delay
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -25,19 +31,91 @@ class VaultMoveToOrganizationViewModel @Inject constructor(
|
|||
VaultMoveToOrganizationState(
|
||||
vaultItemId = VaultMoveToOrganizationArgs(savedStateHandle).vaultItemId,
|
||||
viewState = VaultMoveToOrganizationState.ViewState.Loading,
|
||||
dialogState = null,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
init {
|
||||
// TODO Load real orgs/collections BIT-769
|
||||
viewModelScope.launch {
|
||||
@Suppress("MagicNumber")
|
||||
delay(1500)
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultMoveToOrganizationState.ViewState.Empty,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleAction(action: VaultMoveToOrganizationAction) {
|
||||
when (action) {
|
||||
is VaultMoveToOrganizationAction.BackClick -> handleBackClick()
|
||||
is VaultMoveToOrganizationAction.CollectionSelect -> handleCollectionSelect(action)
|
||||
is VaultMoveToOrganizationAction.MoveClick -> handleMoveClick()
|
||||
is VaultMoveToOrganizationAction.DismissClick -> handleDismissClick()
|
||||
is VaultMoveToOrganizationAction.OrganizationSelect -> handleOrganizationSelect(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBackClick() {
|
||||
sendEvent(VaultMoveToOrganizationEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleOrganizationSelect(action: VaultMoveToOrganizationAction.OrganizationSelect) {
|
||||
updateContent { it.copy(selectedOrganizationId = action.organization.id) }
|
||||
}
|
||||
|
||||
private fun handleCollectionSelect(action: VaultMoveToOrganizationAction.CollectionSelect) {
|
||||
updateContent { currentContentState ->
|
||||
currentContentState.copy(
|
||||
organizations = currentContentState
|
||||
.organizations
|
||||
.toUpdatedOrganizations(
|
||||
selectedOrganizationId = currentContentState.selectedOrganizationId,
|
||||
selectedCollectionId = action.collection.id,
|
||||
),
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMoveClick() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VaultMoveToOrganizationState.DialogState.Loading(
|
||||
message = R.string.saving.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
// TODO implement move organization functionality BIT-769
|
||||
viewModelScope.launch {
|
||||
@Suppress("MagicNumber")
|
||||
delay(1500)
|
||||
sendEvent(VaultMoveToOrganizationEvent.ShowToast("Not yet implemented!".asText()))
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = null)
|
||||
}
|
||||
sendEvent(VaultMoveToOrganizationEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDismissClick() {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
}
|
||||
|
||||
private inline fun updateContent(
|
||||
crossinline block: (
|
||||
VaultMoveToOrganizationState.ViewState.Content,
|
||||
) -> VaultMoveToOrganizationState.ViewState.Content?,
|
||||
) {
|
||||
val currentViewState = state.viewState
|
||||
val updatedContent = (currentViewState as? VaultMoveToOrganizationState.ViewState.Content)
|
||||
?.let(block)
|
||||
?: return
|
||||
mutableStateFlow.update { it.copy(viewState = updatedContent) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,13 +123,36 @@ class VaultMoveToOrganizationViewModel @Inject constructor(
|
|||
*
|
||||
* @property vaultItemId Indicates whether the VM is in add or edit mode.
|
||||
* @property viewState indicates what view state the screen is in.
|
||||
* @property dialogState the dialog state.
|
||||
*/
|
||||
@Parcelize
|
||||
data class VaultMoveToOrganizationState(
|
||||
val vaultItemId: String,
|
||||
val viewState: ViewState,
|
||||
val dialogState: DialogState?,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* Represents the current state of any dialogs on the screen.
|
||||
*/
|
||||
sealed class DialogState : Parcelable {
|
||||
/**
|
||||
* Represents an error dialog with the given [message].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Error(
|
||||
val message: Text,
|
||||
) : DialogState()
|
||||
|
||||
/**
|
||||
* Represents a loading dialog with the given [message].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Loading(
|
||||
val message: Text,
|
||||
) : DialogState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the specific view states for the [VaultMoveToOrganizationScreen].
|
||||
*/
|
||||
|
@ -74,9 +175,53 @@ data class VaultMoveToOrganizationState(
|
|||
|
||||
/**
|
||||
* Represents a loaded content state for the [VaultMoveToOrganizationScreen].
|
||||
*
|
||||
* @property organizations the organizations available.
|
||||
*/
|
||||
@Parcelize
|
||||
data object Content : ViewState()
|
||||
data class Content(
|
||||
val selectedOrganizationId: String,
|
||||
val organizations: List<Organization>,
|
||||
) : ViewState() {
|
||||
|
||||
val selectedOrganization: Organization
|
||||
get() = organizations.first { it.id == selectedOrganizationId }
|
||||
|
||||
/**
|
||||
* Models an organization.
|
||||
*
|
||||
* @property id the organization id.
|
||||
* @property name the organization name.
|
||||
* @property isSelected if the organization is selected or not.
|
||||
* @property collections the list of collections associated with the organization.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Organization(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val collections: List<Collection>,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
* Models a collection.
|
||||
*
|
||||
* @property id the collection id.
|
||||
* @property name the collection name.
|
||||
* @property isSelected if the collection is selected or not.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Collection(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val isSelected: Boolean,
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an empty state for the [VaultMoveToOrganizationScreen].
|
||||
*/
|
||||
@Parcelize
|
||||
data object Empty : ViewState()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,4 +252,59 @@ sealed class VaultMoveToOrganizationAction {
|
|||
* Click the back button.
|
||||
*/
|
||||
data object BackClick : VaultMoveToOrganizationAction()
|
||||
|
||||
/**
|
||||
* Click the move button.
|
||||
*/
|
||||
data object MoveClick : VaultMoveToOrganizationAction()
|
||||
|
||||
/**
|
||||
* Dismiss the dialog.
|
||||
*/
|
||||
data object DismissClick : VaultMoveToOrganizationAction()
|
||||
|
||||
/**
|
||||
* Select an organization.
|
||||
*
|
||||
* @property organization the organization to select.
|
||||
*/
|
||||
data class OrganizationSelect(
|
||||
val organization: VaultMoveToOrganizationState.ViewState.Content.Organization,
|
||||
) : VaultMoveToOrganizationAction()
|
||||
|
||||
/**
|
||||
* Select a collection.
|
||||
*
|
||||
* @property collection the collection to select.
|
||||
*/
|
||||
data class CollectionSelect(
|
||||
val collection: VaultMoveToOrganizationState.ViewState.Content.Collection,
|
||||
) : VaultMoveToOrganizationAction()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
private fun List<VaultMoveToOrganizationState.ViewState.Content.Organization>.toUpdatedOrganizations(
|
||||
selectedOrganizationId: String,
|
||||
selectedCollectionId: String,
|
||||
): List<VaultMoveToOrganizationState.ViewState.Content.Organization> =
|
||||
map { organization ->
|
||||
if (organization.id != selectedOrganizationId) return@map organization
|
||||
organization.copy(
|
||||
collections = organization
|
||||
.collections
|
||||
.toUpdatedCollections(selectedCollectionId = selectedCollectionId),
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<VaultMoveToOrganizationState.ViewState.Content.Collection>.toUpdatedCollections(
|
||||
selectedCollectionId: String,
|
||||
): List<VaultMoveToOrganizationState.ViewState.Content.Collection> =
|
||||
map { collection ->
|
||||
collection.copy(
|
||||
isSelected = if (selectedCollectionId == collection.id) {
|
||||
!collection.isSelected
|
||||
} else {
|
||||
collection.isSelected
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,13 +1,28 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.movetoorganization
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||
import androidx.compose.ui.test.assertIsOff
|
||||
import androidx.compose.ui.test.assertIsOn
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onLast
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.util.onNodeWithContentDescriptionAfterScroll
|
||||
import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.util.createMockOrganizationList
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
@ -52,10 +67,191 @@ class VaultMoveToOrganizationScreenTest : BaseComposeTest() {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking move button should send MoveClick action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Move")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultMoveToOrganizationAction.MoveClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `selecting an organization should send OrganizationSelect action`() {
|
||||
composeTestRule
|
||||
.onNodeWithContentDescriptionAfterScroll(label = "Organization, Organization 1")
|
||||
.performClick()
|
||||
// Choose the option from the menu
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Organization 2")
|
||||
.onLast()
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultMoveToOrganizationAction.OrganizationSelect(
|
||||
VaultMoveToOrganizationState.ViewState.Content.Organization(
|
||||
id = "2",
|
||||
name = "Organization 2",
|
||||
collections = listOf(
|
||||
VaultMoveToOrganizationState.ViewState.Content.Collection(
|
||||
id = "1",
|
||||
name = "Collection 1",
|
||||
isSelected = true,
|
||||
),
|
||||
VaultMoveToOrganizationState.ViewState.Content.Collection(
|
||||
id = "2",
|
||||
name = "Collection 2",
|
||||
isSelected = false,
|
||||
),
|
||||
VaultMoveToOrganizationState.ViewState.Content.Collection(
|
||||
id = "3",
|
||||
name = "Collection 3",
|
||||
isSelected = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the organization option field should display according to state`() {
|
||||
composeTestRule
|
||||
.onNodeWithContentDescriptionAfterScroll(label = "Organization, Organization 1")
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = VaultMoveToOrganizationState.ViewState.Content(
|
||||
organizations = createMockOrganizationList(),
|
||||
selectedOrganizationId = "2",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescriptionAfterScroll(label = "Organization, Organization 2")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `selecting a collection should send CollectionSelect action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Collection 2")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultMoveToOrganizationAction.CollectionSelect(
|
||||
VaultMoveToOrganizationState.ViewState.Content.Collection(
|
||||
id = "2",
|
||||
name = "Collection 2",
|
||||
isSelected = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the collection list should display according to state`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Collection 1")
|
||||
.assertIsOn()
|
||||
composeTestRule
|
||||
.onNodeWithText("Collection 2")
|
||||
.assertIsOff()
|
||||
composeTestRule
|
||||
.onNodeWithText("Collection 3")
|
||||
.assertIsOff()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = VaultMoveToOrganizationState.ViewState.Content(
|
||||
organizations = createMockOrganizationList()
|
||||
.map { organization ->
|
||||
organization.copy(
|
||||
collections =
|
||||
if (organization.id == "1") {
|
||||
organization
|
||||
.collections
|
||||
.map { collection ->
|
||||
collection.copy(isSelected = collection.id != "1")
|
||||
}
|
||||
} else {
|
||||
organization.collections
|
||||
},
|
||||
)
|
||||
},
|
||||
selectedOrganizationId = "1",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Collection 1")
|
||||
.assertIsOff()
|
||||
composeTestRule
|
||||
.onNodeWithText("Collection 2")
|
||||
.assertIsOn()
|
||||
composeTestRule
|
||||
.onNodeWithText("Collection 3")
|
||||
.assertIsOn()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loading dialog should display according to state`() {
|
||||
composeTestRule
|
||||
.onAllNodesWithText("loading")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsNotDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VaultMoveToOrganizationState.DialogState.Loading("loading".asText()),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("loading")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error dialog should display according to state`() {
|
||||
composeTestRule
|
||||
.onAllNodesWithText("error")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsNotDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VaultMoveToOrganizationState.DialogState.Error("error".asText()),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("error")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createVaultMoveToOrganizationState(): VaultMoveToOrganizationState =
|
||||
VaultMoveToOrganizationState(
|
||||
vaultItemId = "mockId",
|
||||
viewState = VaultMoveToOrganizationState.ViewState.Content,
|
||||
viewState = VaultMoveToOrganizationState.ViewState.Content(
|
||||
organizations = createMockOrganizationList(),
|
||||
selectedOrganizationId = "1",
|
||||
),
|
||||
dialogState = null,
|
||||
)
|
||||
|
|
|
@ -2,7 +2,10 @@ package com.x8bit.bitwarden.ui.vault.feature.movetoorganization
|
|||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.util.createMockOrganizationList
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -50,6 +53,119 @@ class VaultMoveToOrganizationViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `OrganizationSelect should update selected Organization`() = runTest {
|
||||
val viewModel = createViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithState(
|
||||
state = createVaultMoveToOrganizationState(
|
||||
viewState = VaultMoveToOrganizationState.ViewState.Content(
|
||||
organizations = createMockOrganizationList(),
|
||||
selectedOrganizationId = "1",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val action = VaultMoveToOrganizationAction.OrganizationSelect(
|
||||
VaultMoveToOrganizationState.ViewState.Content.Organization(
|
||||
id = "3",
|
||||
name = "Organization 3",
|
||||
collections = emptyList(),
|
||||
),
|
||||
)
|
||||
val expectedState = createVaultMoveToOrganizationState(
|
||||
viewState = VaultMoveToOrganizationState.ViewState.Content(
|
||||
organizations = createMockOrganizationList(),
|
||||
selectedOrganizationId = "3",
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
assertEquals(
|
||||
expectedState,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CollectionSelect should update selected Collections`() = runTest {
|
||||
val viewModel = createViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithState(
|
||||
state = createVaultMoveToOrganizationState(
|
||||
viewState = VaultMoveToOrganizationState.ViewState.Content(
|
||||
organizations = createMockOrganizationList(),
|
||||
selectedOrganizationId = "1",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val selectCollection3Action = VaultMoveToOrganizationAction.CollectionSelect(
|
||||
VaultMoveToOrganizationState.ViewState.Content.Collection(
|
||||
id = "3",
|
||||
name = "Collection 3",
|
||||
isSelected = false,
|
||||
),
|
||||
)
|
||||
val unselectCollection1Action = VaultMoveToOrganizationAction.CollectionSelect(
|
||||
VaultMoveToOrganizationState.ViewState.Content.Collection(
|
||||
id = "1",
|
||||
name = "Collection 1",
|
||||
isSelected = true,
|
||||
),
|
||||
)
|
||||
val expectedState = createVaultMoveToOrganizationState(
|
||||
viewState = VaultMoveToOrganizationState.ViewState.Content(
|
||||
organizations = createMockOrganizationList()
|
||||
.map { organization ->
|
||||
organization.copy(
|
||||
collections =
|
||||
if (organization.id == "1") {
|
||||
organization.collections.map {
|
||||
it.copy(isSelected = it.id == "3")
|
||||
}
|
||||
} else {
|
||||
organization.collections
|
||||
},
|
||||
)
|
||||
},
|
||||
selectedOrganizationId = "1",
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.actionChannel.trySend(selectCollection3Action)
|
||||
viewModel.actionChannel.trySend(unselectCollection1Action)
|
||||
|
||||
assertEquals(
|
||||
expectedState,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MoveClick should show dialog, and remove it once an item is moved`() = runTest {
|
||||
val viewModel = createViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithState(
|
||||
state = initialState,
|
||||
),
|
||||
)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(initialState, awaitItem())
|
||||
viewModel.actionChannel.trySend(VaultMoveToOrganizationAction.MoveClick)
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
dialogState = VaultMoveToOrganizationState.DialogState.Loading(
|
||||
message = R.string.saving.asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
initialState,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
savedStateHandle: SavedStateHandle = initialSavedStateHandle,
|
||||
): VaultMoveToOrganizationViewModel =
|
||||
|
@ -67,11 +183,13 @@ class VaultMoveToOrganizationViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
private fun createVaultMoveToOrganizationState(
|
||||
viewState: VaultMoveToOrganizationState.ViewState = VaultMoveToOrganizationState.ViewState.Content,
|
||||
viewState: VaultMoveToOrganizationState.ViewState = VaultMoveToOrganizationState.ViewState.Empty,
|
||||
vaultItemId: String = "mockId",
|
||||
dialogState: VaultMoveToOrganizationState.DialogState? = null,
|
||||
): VaultMoveToOrganizationState =
|
||||
VaultMoveToOrganizationState(
|
||||
vaultItemId = vaultItemId,
|
||||
viewState = viewState,
|
||||
dialogState = dialogState,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.movetoorganization.util
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.VaultMoveToOrganizationState
|
||||
|
||||
/**
|
||||
* Creates a list of mock organizations.
|
||||
*/
|
||||
fun createMockOrganizationList():
|
||||
List<VaultMoveToOrganizationState.ViewState.Content.Organization> =
|
||||
listOf(
|
||||
VaultMoveToOrganizationState.ViewState.Content.Organization(
|
||||
id = "1",
|
||||
name = "Organization 1",
|
||||
collections = listOf(
|
||||
VaultMoveToOrganizationState.ViewState.Content.Collection(
|
||||
id = "1",
|
||||
name = "Collection 1",
|
||||
isSelected = true,
|
||||
),
|
||||
VaultMoveToOrganizationState.ViewState.Content.Collection(
|
||||
id = "2",
|
||||
name = "Collection 2",
|
||||
isSelected = false,
|
||||
),
|
||||
VaultMoveToOrganizationState.ViewState.Content.Collection(
|
||||
id = "3",
|
||||
name = "Collection 3",
|
||||
isSelected = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
VaultMoveToOrganizationState.ViewState.Content.Organization(
|
||||
id = "2",
|
||||
name = "Organization 2",
|
||||
collections = listOf(
|
||||
VaultMoveToOrganizationState.ViewState.Content.Collection(
|
||||
id = "1",
|
||||
name = "Collection 1",
|
||||
isSelected = true,
|
||||
),
|
||||
VaultMoveToOrganizationState.ViewState.Content.Collection(
|
||||
id = "2",
|
||||
name = "Collection 2",
|
||||
isSelected = false,
|
||||
),
|
||||
VaultMoveToOrganizationState.ViewState.Content.Collection(
|
||||
id = "3",
|
||||
name = "Collection 3",
|
||||
isSelected = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
VaultMoveToOrganizationState.ViewState.Content.Organization(
|
||||
id = "3",
|
||||
name = "Organization 3",
|
||||
collections = emptyList(),
|
||||
),
|
||||
)
|
Loading…
Add table
Reference in a new issue