Add initial attachments screen shell (#745)

This commit is contained in:
David Perez 2024-01-24 10:26:45 -06:00 committed by Álison Fernandes
parent 30ab22f826
commit 96201fd34c
15 changed files with 454 additions and 8 deletions

View file

@ -24,6 +24,8 @@ import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.navigateToAddSend import com.x8bit.bitwarden.ui.tools.feature.send.addsend.navigateToAddSend
import com.x8bit.bitwarden.ui.vault.feature.addedit.navigateToVaultAddEdit import com.x8bit.bitwarden.ui.vault.feature.addedit.navigateToVaultAddEdit
import com.x8bit.bitwarden.ui.vault.feature.addedit.vaultAddEditDestination import com.x8bit.bitwarden.ui.vault.feature.addedit.vaultAddEditDestination
import com.x8bit.bitwarden.ui.vault.feature.attachments.attachmentDestination
import com.x8bit.bitwarden.ui.vault.feature.attachments.navigateToAttachment
import com.x8bit.bitwarden.ui.vault.feature.item.navigateToVaultItem import com.x8bit.bitwarden.ui.vault.feature.item.navigateToVaultItem
import com.x8bit.bitwarden.ui.vault.feature.item.vaultItemDestination import com.x8bit.bitwarden.ui.vault.feature.item.vaultItemDestination
import com.x8bit.bitwarden.ui.vault.feature.manualcodeentry.navigateToManualCodeEntryScreen import com.x8bit.bitwarden.ui.vault.feature.manualcodeentry.navigateToManualCodeEntryScreen
@ -90,6 +92,7 @@ fun NavGraphBuilder.vaultUnlockedGraph(
}, },
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
onNavigateToGeneratorModal = { navController.navigateToGeneratorModal(mode = it) }, onNavigateToGeneratorModal = { navController.navigateToGeneratorModal(mode = it) },
onNavigateToAttachments = { navController.navigateToAttachment(it) },
) )
vaultMoveToOrganizationDestination( vaultMoveToOrganizationDestination(
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
@ -108,6 +111,7 @@ fun NavGraphBuilder.vaultUnlockedGraph(
onNavigateToMoveToOrganization = { onNavigateToMoveToOrganization = {
navController.navigateToVaultMoveToOrganization(it) navController.navigateToVaultMoveToOrganization(it)
}, },
onNavigateToAttachments = { navController.navigateToAttachment(it) },
) )
vaultQrCodeScanDestination( vaultQrCodeScanDestination(
onNavigateToManualCodeEntryScreen = { onNavigateToManualCodeEntryScreen = {
@ -136,6 +140,9 @@ fun NavGraphBuilder.vaultUnlockedGraph(
}, },
onNavigateToViewCipher = { navController.navigateToVaultItem(it) }, onNavigateToViewCipher = { navController.navigateToVaultItem(it) },
) )
attachmentDestination(
onNavigateBack = { navController.popBackStack() },
)
} }
} }

View file

@ -47,6 +47,7 @@ fun NavGraphBuilder.vaultAddEditDestination(
onNavigateToManualCodeEntryScreen: () -> Unit, onNavigateToManualCodeEntryScreen: () -> Unit,
onNavigateToQrCodeScanScreen: () -> Unit, onNavigateToQrCodeScanScreen: () -> Unit,
onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit, onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit,
onNavigateToAttachments: (cipherId: String) -> Unit,
) { ) {
composableWithSlideTransitions( composableWithSlideTransitions(
route = ADD_EDIT_ITEM_ROUTE, route = ADD_EDIT_ITEM_ROUTE,
@ -59,6 +60,7 @@ fun NavGraphBuilder.vaultAddEditDestination(
onNavigateToManualCodeEntryScreen = onNavigateToManualCodeEntryScreen, onNavigateToManualCodeEntryScreen = onNavigateToManualCodeEntryScreen,
onNavigateToQrCodeScanScreen = onNavigateToQrCodeScanScreen, onNavigateToQrCodeScanScreen = onNavigateToQrCodeScanScreen,
onNavigateToGeneratorModal = onNavigateToGeneratorModal, onNavigateToGeneratorModal = onNavigateToGeneratorModal,
onNavigateToAttachments = onNavigateToAttachments,
) )
} }
} }

View file

@ -25,12 +25,15 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
import com.x8bit.bitwarden.ui.platform.theme.LocalPermissionsManager import com.x8bit.bitwarden.ui.platform.theme.LocalPermissionsManager
import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
@ -50,6 +53,7 @@ fun VaultAddEditScreen(
permissionsManager: PermissionsManager = LocalPermissionsManager.current, permissionsManager: PermissionsManager = LocalPermissionsManager.current,
onNavigateToManualCodeEntryScreen: () -> Unit, onNavigateToManualCodeEntryScreen: () -> Unit,
onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit, onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit,
onNavigateToAttachments: (cipherId: String) -> Unit,
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
@ -73,6 +77,8 @@ fun VaultAddEditScreen(
Toast.makeText(context, event.message(resources), Toast.LENGTH_SHORT).show() Toast.makeText(context, event.message(resources), Toast.LENGTH_SHORT).show()
} }
is VaultAddEditEvent.NavigateToAttachments -> onNavigateToAttachments(event.cipherId)
VaultAddEditEvent.NavigateBack -> onNavigateBack.invoke() VaultAddEditEvent.NavigateBack -> onNavigateBack.invoke()
} }
} }
@ -121,6 +127,21 @@ fun VaultAddEditScreen(
{ viewModel.trySendAction(VaultAddEditAction.Common.SaveClick) } { viewModel.trySendAction(VaultAddEditAction.Common.SaveClick) }
}, },
) )
BitwardenOverflowActionItem(
menuItemDataList = persistentListOfNotNull(
OverflowMenuItemData(
text = stringResource(id = R.string.attachments),
onClick = remember(viewModel) {
{
viewModel.trySendAction(
VaultAddEditAction.Common.AttachmentsClick,
)
}
},
)
.takeUnless { state.isAddItemMode },
),
)
}, },
) )
}, },

View file

@ -141,6 +141,7 @@ class VaultAddEditViewModel @Inject constructor(
handleToggleMasterPasswordReprompt(action) handleToggleMasterPasswordReprompt(action)
} }
is VaultAddEditAction.Common.AttachmentsClick -> handleAttachmentsClick()
is VaultAddEditAction.Common.CloseClick -> handleCloseClick() is VaultAddEditAction.Common.CloseClick -> handleCloseClick()
is VaultAddEditAction.Common.DismissDialog -> handleDismissDialog() is VaultAddEditAction.Common.DismissDialog -> handleDismissDialog()
is VaultAddEditAction.Common.SaveClick -> handleSaveClick() is VaultAddEditAction.Common.SaveClick -> handleSaveClick()
@ -257,6 +258,10 @@ class VaultAddEditViewModel @Inject constructor(
} }
} }
private fun handleAttachmentsClick() {
onEdit { sendEvent(VaultAddEditEvent.NavigateToAttachments(it.vaultItemId)) }
}
private fun handleCloseClick() { private fun handleCloseClick() {
sendEvent( sendEvent(
event = VaultAddEditEvent.NavigateBack, event = VaultAddEditEvent.NavigateBack,
@ -1007,6 +1012,12 @@ class VaultAddEditViewModel @Inject constructor(
(state.viewState as? VaultAddEditState.ViewState.Content)?.let(block) (state.viewState as? VaultAddEditState.ViewState.Content)?.let(block)
} }
private inline fun onEdit(
crossinline block: (VaultAddEditType.EditItem) -> Unit,
) {
(state.vaultAddEditType as? VaultAddEditType.EditItem)?.let(block)
}
private inline fun updateContent( private inline fun updateContent(
crossinline block: ( crossinline block: (
VaultAddEditState.ViewState.Content, VaultAddEditState.ViewState.Content,
@ -1424,6 +1435,13 @@ sealed class VaultAddEditEvent {
*/ */
data object NavigateBack : VaultAddEditEvent() data object NavigateBack : VaultAddEditEvent()
/**
* Navigate to attachments screen.
*/
data class NavigateToAttachments(
val cipherId: String,
) : VaultAddEditEvent()
/** /**
* Navigate to the QR code scan screen. * Navigate to the QR code scan screen.
*/ */
@ -1468,6 +1486,11 @@ sealed class VaultAddEditAction {
*/ */
data object DismissDialog : Common() data object DismissDialog : Common()
/**
* The user has clicked the attachments overflow option.
*/
data object AttachmentsClick : Common()
/** /**
* Represents the action when a type option is selected. * Represents the action when a type option is selected.
* *

View file

@ -0,0 +1,55 @@
package com.x8bit.bitwarden.ui.vault.feature.attachments
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val ATTACHMENTS_CIPHER_ID = "cipher_id"
private const val ATTACHMENTS_ROUTE_PREFIX = "attachments"
private const val ATTACHMENTS_ROUTE = "$ATTACHMENTS_ROUTE_PREFIX/{$ATTACHMENTS_CIPHER_ID}"
/**
* Class to retrieve arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class AttachmentsArgs(val cipherId: String) {
constructor(savedStateHandle: SavedStateHandle) : this(
cipherId = checkNotNull(savedStateHandle.get<String>(ATTACHMENTS_CIPHER_ID)),
)
}
/**
* Add the attachments screen to the nav graph.
*/
fun NavGraphBuilder.attachmentDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = ATTACHMENTS_ROUTE,
arguments = listOf(
navArgument(ATTACHMENTS_CIPHER_ID) { type = NavType.StringType },
),
) {
AttachmentsScreen(
onNavigateBack = onNavigateBack,
)
}
}
/**
* Navigate to the attachments screen.
*/
fun NavController.navigateToAttachment(
cipherId: String,
navOptions: NavOptions? = null,
) {
navigate(
route = "$ATTACHMENTS_ROUTE_PREFIX/$cipherId",
navOptions = navOptions,
)
}

View file

@ -0,0 +1,96 @@
package com.x8bit.bitwarden.ui.vault.feature.attachments
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
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
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
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
import com.x8bit.bitwarden.ui.platform.components.NavigationIcon
import com.x8bit.bitwarden.ui.vault.feature.attachments.handlers.AttachmentsHandlers
/**
* Displays the attachments screen.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AttachmentsScreen(
viewModel: AttachmentsViewModel = hiltViewModel(),
onNavigateBack: () -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current
EventsEffect(viewModel = viewModel) { event ->
when (event) {
AttachmentsEvent.NavigateBack -> onNavigateBack()
is AttachmentsEvent.ShowToast -> {
Toast
.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT)
.show()
}
}
}
val attachmentHandlers = remember(viewModel) { AttachmentsHandlers.create(viewModel) }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
.fillMaxSize(),
topBar = {
BitwardenTopAppBar(
title = stringResource(id = R.string.attachments),
scrollBehavior = scrollBehavior,
navigationIcon = NavigationIcon(
navigationIcon = painterResource(id = R.drawable.ic_back),
navigationIconContentDescription = stringResource(id = R.string.back),
onNavigationIconClick = attachmentHandlers.onBackClick,
),
actions = {
BitwardenTextButton(
label = stringResource(id = R.string.save),
onClick = attachmentHandlers.onSaveClick,
)
},
)
},
) { innerPadding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
item {
Text(text = "Not Yet Implemented")
}
item {
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
}

View file

@ -0,0 +1,82 @@
package com.x8bit.bitwarden.ui.vault.feature.attachments
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
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.parcelize.Parcelize
import javax.inject.Inject
private const val KEY_STATE = "state"
/**
* ViewModel responsible for handling user interactions in the attachments screen.
*/
@HiltViewModel
class AttachmentsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : BaseViewModel<AttachmentsState, AttachmentsEvent, AttachmentsAction>(
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE]
?: AttachmentsState(
cipherId = AttachmentsArgs(savedStateHandle).cipherId,
),
) {
override fun handleAction(action: AttachmentsAction) {
when (action) {
AttachmentsAction.BackClick -> handleBackClick()
AttachmentsAction.SaveClick -> handleSaveClick()
}
}
private fun handleBackClick() {
sendEvent(AttachmentsEvent.NavigateBack)
}
private fun handleSaveClick() {
sendEvent(AttachmentsEvent.ShowToast("Not Yet Implemented".asText()))
// TODO: Handle saving the attachments (BIT-522)
}
}
/**
* Represents the state for viewing attachments.
*/
@Parcelize
data class AttachmentsState(
val cipherId: String,
) : Parcelable
/**
* Represents a set of events related attachments.
*/
sealed class AttachmentsEvent {
/**
* Navigates back.
*/
data object NavigateBack : AttachmentsEvent()
/**
* Displays the given [message] as a toast.
*/
data class ShowToast(
val message: Text,
) : AttachmentsEvent()
}
/**
* Represents a set of actions related to attachments.
*/
sealed class AttachmentsAction {
/**
* User clicked the back button.
*/
data object BackClick : AttachmentsAction()
/**
* User clicked the save button.
*/
data object SaveClick : AttachmentsAction()
}

View file

@ -0,0 +1,24 @@
package com.x8bit.bitwarden.ui.vault.feature.attachments.handlers
import com.x8bit.bitwarden.ui.vault.feature.attachments.AttachmentsAction
import com.x8bit.bitwarden.ui.vault.feature.attachments.AttachmentsViewModel
/**
* A collection of handler functions for managing actions within the context of viewing attachments.
*/
data class AttachmentsHandlers(
val onBackClick: () -> Unit,
val onSaveClick: () -> Unit,
) {
companion object {
/**
* Creates the [AttachmentsHandlers] using the [AttachmentsViewModel] to send desired
* actions.
*/
fun create(viewModel: AttachmentsViewModel): AttachmentsHandlers =
AttachmentsHandlers(
onBackClick = { viewModel.trySendAction(AttachmentsAction.BackClick) },
onSaveClick = { viewModel.trySendAction(AttachmentsAction.SaveClick) },
)
}
}

View file

@ -30,6 +30,7 @@ fun NavGraphBuilder.vaultItemDestination(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToVaultEditItem: (vaultItemId: String, isClone: Boolean) -> Unit, onNavigateToVaultEditItem: (vaultItemId: String, isClone: Boolean) -> Unit,
onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit, onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit,
onNavigateToAttachments: (vaultItemId: String) -> Unit,
) { ) {
composableWithSlideTransitions( composableWithSlideTransitions(
route = VAULT_ITEM_ROUTE, route = VAULT_ITEM_ROUTE,
@ -41,6 +42,7 @@ fun NavGraphBuilder.vaultItemDestination(
onNavigateBack = onNavigateBack, onNavigateBack = onNavigateBack,
onNavigateToVaultAddEditItem = onNavigateToVaultEditItem, onNavigateToVaultAddEditItem = onNavigateToVaultEditItem,
onNavigateToMoveToOrganization = onNavigateToMoveToOrganization, onNavigateToMoveToOrganization = onNavigateToMoveToOrganization,
onNavigateToAttachments = onNavigateToAttachments,
) )
} }
} }

View file

@ -62,6 +62,7 @@ fun VaultItemScreen(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToVaultAddEditItem: (vaultItemId: String, isClone: Boolean) -> Unit, onNavigateToVaultAddEditItem: (vaultItemId: String, isClone: Boolean) -> Unit,
onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit, onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit,
onNavigateToAttachments: (vaultItemId: String) -> Unit,
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
@ -90,10 +91,7 @@ fun VaultItemScreen(
is VaultItemEvent.NavigateToUri -> intentManager.launchUri(event.uri.toUri()) is VaultItemEvent.NavigateToUri -> intentManager.launchUri(event.uri.toUri())
is VaultItemEvent.NavigateToAttachments -> { is VaultItemEvent.NavigateToAttachments -> onNavigateToAttachments(event.itemId)
// TODO implement attachments in BIT-522
Toast.makeText(context, "Not yet implemented.", Toast.LENGTH_SHORT).show()
}
is VaultItemEvent.NavigateToMoveToOrganization -> { is VaultItemEvent.NavigateToMoveToOrganization -> {
onNavigateToMoveToOrganization(event.itemId) onNavigateToMoveToOrganization(event.itemId)

View file

@ -60,6 +60,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
private var onNavigateQrCodeScanScreenCalled = false private var onNavigateQrCodeScanScreenCalled = false
private var onNavigateToManualCodeEntryScreenCalled = false private var onNavigateToManualCodeEntryScreenCalled = false
private var onNavigateToGeneratorModalType: GeneratorMode.Modal? = null private var onNavigateToGeneratorModalType: GeneratorMode.Modal? = null
private var onNavigateToAttachmentsId: String? = null
private val mutableEventFlow = bufferedMutableSharedFlow<VaultAddEditEvent>() private val mutableEventFlow = bufferedMutableSharedFlow<VaultAddEditEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE_LOGIN) private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE_LOGIN)
@ -76,13 +77,12 @@ class VaultAddEditScreenTest : BaseComposeTest() {
composeTestRule.setContent { composeTestRule.setContent {
VaultAddEditScreen( VaultAddEditScreen(
onNavigateBack = { onNavigateBackCalled = true }, onNavigateBack = { onNavigateBackCalled = true },
onNavigateToQrCodeScanScreen = { onNavigateToQrCodeScanScreen = { onNavigateQrCodeScanScreenCalled = true },
onNavigateQrCodeScanScreenCalled = true
},
onNavigateToManualCodeEntryScreen = { onNavigateToManualCodeEntryScreen = {
onNavigateToManualCodeEntryScreenCalled = true onNavigateToManualCodeEntryScreenCalled = true
}, },
onNavigateToGeneratorModal = { onNavigateToGeneratorModalType = it }, onNavigateToGeneratorModal = { onNavigateToGeneratorModalType = it },
onNavigateToAttachments = { onNavigateToAttachmentsId = it },
viewModel = viewModel, viewModel = viewModel,
permissionsManager = fakePermissionManager, permissionsManager = fakePermissionManager,
) )
@ -120,6 +120,13 @@ class VaultAddEditScreenTest : BaseComposeTest() {
assertEquals(GeneratorMode.Modal.Password, onNavigateToGeneratorModalType) assertEquals(GeneratorMode.Modal.Password, onNavigateToGeneratorModalType)
} }
@Test
fun `on NavigateToAttachments event should invoke onNavigateToAttachments`() {
val cipherId = "cipherId-1234"
mutableEventFlow.tryEmit(VaultAddEditEvent.NavigateToAttachments(cipherId))
assertEquals(cipherId, onNavigateToAttachmentsId)
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `on NavigateToGeneratorModal event in username mode should invoke NavigateToGeneratorModal with Username Generator Mode `() { fun `on NavigateToGeneratorModal event in username mode should invoke NavigateToGeneratorModal with Username Generator Mode `() {

View file

@ -161,6 +161,25 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
} }
} }
@Test
fun `AttachmentsClick should emit NavigateToAttachments`() = runTest {
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
val initState = createVaultAddItemState(vaultAddEditType = vaultAddEditType)
val viewModel = createAddVaultItemViewModel(
savedStateHandle = createSavedStateHandleWithState(
state = initState,
vaultAddEditType = vaultAddEditType,
),
)
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(VaultAddEditAction.Common.AttachmentsClick)
assertEquals(
VaultAddEditEvent.NavigateToAttachments(DEFAULT_EDIT_ITEM_ID),
awaitItem(),
)
}
}
@Test @Test
fun `in add mode, SaveClick should show dialog, and remove it once an item is saved`() = fun `in add mode, SaveClick should show dialog, and remove it once an item is saved`() =
runTest { runTest {
@ -1526,7 +1545,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
"TestId 1", "TestId 1",
"Boolean Field", "Boolean Field",
true, true,
), VaultAddEditState.Custom.BooleanField( ),
VaultAddEditState.Custom.BooleanField(
"TestId 3", "TestId 3",
"Boolean Field", "Boolean Field",
true, true,

View file

@ -0,0 +1,44 @@
package com.x8bit.bitwarden.ui.vault.feature.attachments
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert
import org.junit.Before
import org.junit.Test
class AttachmentsScreenTest : BaseComposeTest() {
private var onNavigateBackCalled = false
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val mutableEventFlow = bufferedMutableSharedFlow<AttachmentsEvent>()
val viewModel: AttachmentsViewModel = mockk {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns mutableEventFlow
every { trySendAction(any()) } just runs
}
@Before
fun setup() {
composeTestRule.setContent {
AttachmentsScreen(
viewModel = viewModel,
onNavigateBack = { onNavigateBackCalled = true },
)
}
}
@Test
fun `NavigateBack should call onNavigateBack`() {
mutableEventFlow.tryEmit(AttachmentsEvent.NavigateBack)
Assert.assertTrue(onNavigateBackCalled)
}
}
private val DEFAULT_STATE: AttachmentsState = AttachmentsState(
cipherId = "cipherId-1234",
)

View file

@ -0,0 +1,56 @@
package com.x8bit.bitwarden.ui.vault.feature.attachments
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 AttachmentsViewModelTest : BaseViewModelTest() {
@Test
fun `initial state should be correct when state is null`() = runTest {
val viewModel = createViewModel()
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
}
@Test
fun `initial state should be correct when state is set`() = runTest {
val initialState = DEFAULT_STATE.copy(cipherId = "123456789")
val viewModel = createViewModel(initialState)
assertEquals(initialState, viewModel.stateFlow.value)
}
@Test
fun `BackClick should emit NavigateBack`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(AttachmentsAction.BackClick)
assertEquals(AttachmentsEvent.NavigateBack, awaitItem())
}
}
@Test
fun `SaveClick should emit ShowToast`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(AttachmentsAction.SaveClick)
assertEquals(AttachmentsEvent.ShowToast("Not Yet Implemented".asText()), awaitItem())
}
}
private fun createViewModel(
initialState: AttachmentsState? = null,
): AttachmentsViewModel = AttachmentsViewModel(
savedStateHandle = SavedStateHandle().apply {
set("state", initialState)
set("cipher_id", initialState?.cipherId ?: "cipherId-1234")
},
)
}
private val DEFAULT_STATE: AttachmentsState = AttachmentsState(
cipherId = "cipherId-1234",
)

View file

@ -57,6 +57,7 @@ class VaultItemScreenTest : BaseComposeTest() {
private var onNavigateBackCalled = false private var onNavigateBackCalled = false
private var onNavigateToVaultEditItemId: String? = null private var onNavigateToVaultEditItemId: String? = null
private var onNavigateToMoveToOrganizationItemId: String? = null private var onNavigateToMoveToOrganizationItemId: String? = null
private var onNavigateToAttachmentsId: String? = null
private val intentManager = mockk<IntentManager>() private val intentManager = mockk<IntentManager>()
@ -75,6 +76,7 @@ class VaultItemScreenTest : BaseComposeTest() {
onNavigateBack = { onNavigateBackCalled = true }, onNavigateBack = { onNavigateBackCalled = true },
onNavigateToVaultAddEditItem = { id, _ -> onNavigateToVaultEditItemId = id }, onNavigateToVaultAddEditItem = { id, _ -> onNavigateToVaultEditItemId = id },
onNavigateToMoveToOrganization = { onNavigateToMoveToOrganizationItemId = it }, onNavigateToMoveToOrganization = { onNavigateToMoveToOrganizationItemId = it },
onNavigateToAttachments = { onNavigateToAttachmentsId = it },
intentManager = intentManager, intentManager = intentManager,
) )
} }
@ -95,6 +97,13 @@ class VaultItemScreenTest : BaseComposeTest() {
assertEquals(id, onNavigateToMoveToOrganizationItemId) assertEquals(id, onNavigateToMoveToOrganizationItemId)
} }
@Test
fun `NavigateToMoveToOrganization event should invoke onNavigateToAttachments`() {
val id = "id1234"
mutableEventFlow.tryEmit(VaultItemEvent.NavigateToAttachments(itemId = id))
assertEquals(id, onNavigateToAttachmentsId)
}
@Test @Test
fun `on close click should send CloseClick`() { fun `on close click should send CloseClick`() {
composeTestRule.onNodeWithContentDescription(label = "Close").performClick() composeTestRule.onNodeWithContentDescription(label = "Close").performClick()