From 15fcfce0b226421abc2df714f08a3261186da4d7 Mon Sep 17 00:00:00 2001 From: David Perez Date: Thu, 4 Jan 2024 11:30:50 -0600 Subject: [PATCH] Populate the send screen with real data (#488) --- .../platform/base/util/ModifierExtensions.kt | 38 +++ .../platform/components/BitwardenGroupItem.kt | 105 ++++++ .../platform/components/BitwardenListItem.kt | 139 ++++++++ .../ui/tools/feature/send/SendContent.kt | 78 ++++- .../ui/tools/feature/send/SendListItem.kt | 76 +++++ .../ui/tools/feature/send/SendNavigation.kt | 2 +- .../ui/tools/feature/send/SendScreen.kt | 17 +- .../ui/tools/feature/send/SendViewModel.kt | 96 +++++- .../feature/send/handlers/SendHandlers.kt | 35 ++ .../feature/send/util/SendDataExtensions.kt | 21 +- app/src/main/res/drawable/ic_send_file.xml | 13 + app/src/main/res/drawable/ic_send_text.xml | 22 ++ .../datasource/sdk/model/SendViewUtil.kt | 7 +- .../ui/tools/feature/send/SendScreenTest.kt | 315 +++++++++++++++++- .../tools/feature/send/SendViewModelTest.kt | 52 ++- .../send/util/SendDataExtensionsTest.kt | 41 ++- 16 files changed, 1023 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenGroupItem.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenListItem.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendListItem.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/handlers/SendHandlers.kt create mode 100644 app/src/main/res/drawable/ic_send_file.xml create mode 100644 app/src/main/res/drawable/ic_send_text.xml diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ModifierExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ModifierExtensions.kt index 7510626bb7..3432456b44 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ModifierExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ModifierExtensions.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.platform.base.util +import androidx.compose.material3.DividerDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TopAppBarScrollBehavior @@ -7,6 +8,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage /** @@ -31,3 +37,35 @@ fun Modifier.scrolledContainerBackground( ) } } + +/** + * This is a [Modifier] extension for drawing a divider at the bottom of the composable. + */ +@OmitFromCoverage +@Stable +@Composable +fun Modifier.bottomDivider( + paddingStart: Dp = 0.dp, + paddingEnd: Dp = 0.dp, + thickness: Dp = DividerDefaults.Thickness, + color: Color = DividerDefaults.color, + enabled: Boolean = true, +): Modifier = drawWithCache { + onDrawWithContent { + drawContent() + if (enabled) { + drawLine( + color = color, + strokeWidth = thickness.toPx(), + start = Offset( + x = paddingStart.toPx(), + y = size.height - thickness.toPx() / 2, + ), + end = Offset( + x = size.width - paddingEnd.toPx(), + y = size.height - thickness.toPx() / 2, + ), + ) + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenGroupItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenGroupItem.kt new file mode 100644 index 0000000000..532ad4cda4 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenGroupItem.kt @@ -0,0 +1,105 @@ +package com.x8bit.bitwarden.ui.platform.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider +import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme + +/** + * A reusable composable function that displays a group item. + * The list item consists of a start icon, a label, a supporting label and an optional divider. + * + * @param label The main text label to be displayed in the group item. + * @param supportingLabel The secondary supporting text label to be displayed beside the label. + * @param startIcon The [Painter] object used to draw the icon at the start of the group item. + * @param onClick A lambda function that is invoked when the group is clicked. + * @param modifier The [Modifier] to be applied to the [Row] composable that holds the list item. + * @param showDivider Indicates whether the divider should be shown or not. + */ +@Composable +fun BitwardenGroupItem( + label: String, + supportingLabel: String, + startIcon: Painter, + onClick: () -> Unit, + modifier: Modifier = Modifier, + showDivider: Boolean = true, +) { + Row( + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(color = MaterialTheme.colorScheme.primary), + onClick = onClick, + ) + .bottomDivider( + enabled = showDivider, + paddingStart = 16.dp, + ) + .padding( + top = 16.dp, + bottom = 16.dp, + end = 8.dp, + ) + .then(modifier), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Icon( + painter = startIcon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.size(24.dp), + ) + + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + + Text( + text = supportingLabel, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Icon( + painter = painterResource(id = R.drawable.ic_navigate_next), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp), + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun BitwardenGroupItem_preview() { + BitwardenTheme { + BitwardenGroupItem( + label = "Sample Label", + supportingLabel = "5", + startIcon = painterResource(id = R.drawable.ic_send_text), + onClick = {}, + ) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenListItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenListItem.kt new file mode 100644 index 0000000000..4a9c36a809 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenListItem.kt @@ -0,0 +1,139 @@ +package com.x8bit.bitwarden.ui.platform.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme + +/** + * A Composable function that displays a row item. + * + * @param label The primary text label to display for the item. + * @param supportingLabel An secondary text label to display beneath the label. + * @param startIcon The [Painter] object used to draw the icon at the start of the item. + * @param onClick The lambda to be invoked when the item is clicked. + * @param modifier An optional [Modifier] for this Composable, defaulting to an empty Modifier. + * This allows the caller to specify things like padding, size, etc. + * @param selectionDataList A list of all the selection items to be displayed in the overflow + * dialog. + */ +@Suppress("LongMethod") +@Composable +fun BitwardenListItem( + label: String, + supportingLabel: String, + startIcon: Painter, + onClick: () -> Unit, + selectionDataList: List, + modifier: Modifier = Modifier, +) { + var shouldShowDialog by remember { mutableStateOf(false) } + Row( + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(color = MaterialTheme.colorScheme.primary), + onClick = onClick, + ) + .defaultMinSize(minHeight = 72.dp) + .padding(vertical = 8.dp) + .then(modifier), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Icon( + painter = startIcon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.size(24.dp), + ) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + + Text( + text = supportingLabel, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + IconButton( + onClick = { shouldShowDialog = true }, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_more_horizontal), + contentDescription = stringResource(id = R.string.options), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp), + ) + } + } + + if (shouldShowDialog) { + BitwardenSelectionDialog( + title = label, + onDismissRequest = { shouldShowDialog = false }, + selectionItems = { + selectionDataList.forEach { + BitwardenBasicDialogRow( + text = it.text, + onClick = { + shouldShowDialog = false + it.onClick() + }, + ) + } + }, + ) + } +} + +/** + * Wrapper for the an individual selection item's data. + */ +data class SelectionItemData( + val text: String, + val onClick: () -> Unit, +) + +@Preview(showBackground = true) +@Composable +private fun BitwardenListItem_preview() { + BitwardenTheme { + BitwardenListItem( + label = "Sample Label", + supportingLabel = "Jan 3, 2024, 10:35 AM", + startIcon = painterResource(id = R.drawable.ic_send_text), + onClick = {}, + selectionDataList = emptyList(), + ) + } +} 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 585e15adef..607d3975ab 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 @@ -6,31 +6,89 @@ 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.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.components.BitwardenGroupItem +import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderTextWithSupportLabel +import com.x8bit.bitwarden.ui.tools.feature.send.handlers.SendHandlers /** * Content view for the [SendScreen]. */ +@Suppress("LongMethod") @Composable fun SendContent( state: SendState.ViewState.Content, + sendHandlers: SendHandlers, 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, + BitwardenListHeaderText( + label = stringResource(id = R.string.types), modifier = Modifier - .padding(horizontal = 16.dp) + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + + item { + BitwardenGroupItem( + label = stringResource(id = R.string.type_text), + supportingLabel = state.textTypeCount.toString(), + startIcon = painterResource(id = R.drawable.ic_send_text), + onClick = sendHandlers.onTextTypeClick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + + item { + BitwardenGroupItem( + label = stringResource(id = R.string.type_file), + supportingLabel = state.fileTypeCount.toString(), + startIcon = painterResource(id = R.drawable.ic_send_file), + onClick = sendHandlers.onFileTypeClick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + + item { + Spacer(modifier = Modifier.height(16.dp)) + BitwardenListHeaderTextWithSupportLabel( + label = stringResource(id = R.string.all_sends), + supportingLabel = state.sendItems.size.toString(), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + + items(state.sendItems) { + SendListItem( + startIcon = painterResource(id = it.type.iconRes), + label = it.name, + supportingLabel = it.deletionDate, + onClick = { sendHandlers.onSendClick(it) }, + onCopyClick = { sendHandlers.onCopySendClick(it) }, + onEditClick = { sendHandlers.onEditSendClick(it) }, + onShareClick = { sendHandlers.onShareSendClick(it) }, + modifier = Modifier + .padding( + start = 16.dp, + // There is some built-in padding to the menu button that makes up + // the visual difference here. + end = 12.dp, + ) .fillMaxWidth(), ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendListItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendListItem.kt new file mode 100644 index 0000000000..dd465bf031 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendListItem.kt @@ -0,0 +1,76 @@ +package com.x8bit.bitwarden.ui.tools.feature.send + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.components.BitwardenListItem +import com.x8bit.bitwarden.ui.platform.components.SelectionItemData +import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme + +/** + * A Composable function that displays a row send item. + * + * @param label The primary text label to display for the item. + * @param supportingLabel An secondary text label to display beneath the label. + * @param startIcon The [Painter] object used to draw the icon at the start of the item. + * @param onClick The lambda to be invoked when the item is clicked. + * @param onEditClick The lambda to be invoked when the edit option is clicked from the menu. + * @param onCopyClick The lambda to be invoked when the copy option is clicked from the menu. + * @param onShareClick The lambda to be invoked when the share option is clicked from the menu. + * @param modifier An optional [Modifier] for this Composable, defaulting to an empty Modifier. + * This allows the caller to specify things like padding, size, etc. + */ +@Suppress("LongMethod") +@Composable +fun SendListItem( + label: String, + supportingLabel: String, + startIcon: Painter, + onClick: () -> Unit, + onEditClick: () -> Unit, + onCopyClick: () -> Unit, + onShareClick: () -> Unit, + modifier: Modifier = Modifier, +) { + BitwardenListItem( + label = label, + supportingLabel = supportingLabel, + startIcon = startIcon, + onClick = onClick, + selectionDataList = listOf( + SelectionItemData( + text = stringResource(id = R.string.edit), + onClick = onEditClick, + ), + SelectionItemData( + text = stringResource(id = R.string.copy_link), + onClick = onCopyClick, + ), + SelectionItemData( + text = stringResource(id = R.string.share_link), + onClick = onShareClick, + ), + ), + modifier = modifier, + ) +} + +@Preview(showBackground = true) +@Composable +private fun SendListItem_preview() { + BitwardenTheme { + SendListItem( + label = "Sample Label", + supportingLabel = "Jan 3, 2024, 10:35 AM", + startIcon = painterResource(id = R.drawable.ic_send_text), + onClick = {}, + onCopyClick = {}, + onEditClick = {}, + onShareClick = {}, + ) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendNavigation.kt index e30e8853b4..9a7290c5e3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendNavigation.kt @@ -17,7 +17,7 @@ fun NavGraphBuilder.sendDestination( route = SEND_ROUTE, ) { SendScreen( - onNavigateAddSend = onNavigateToAddSend, + onNavigateToAddSend = onNavigateToAddSend, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt index 2ef42ec200..1327770cfa 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt @@ -18,6 +18,8 @@ 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.ClipboardManager +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -27,6 +29,7 @@ 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.IntentHandler +import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent import com.x8bit.bitwarden.ui.platform.components.BitwardenMediumTopAppBar @@ -34,6 +37,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.BitwardenSearchActionItem import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData +import com.x8bit.bitwarden.ui.tools.feature.send.handlers.SendHandlers import kotlinx.collections.immutable.persistentListOf /** @@ -43,15 +47,22 @@ import kotlinx.collections.immutable.persistentListOf @OptIn(ExperimentalMaterial3Api::class) @Composable fun SendScreen( - onNavigateAddSend: () -> Unit, + onNavigateToAddSend: () -> Unit, viewModel: SendViewModel = hiltViewModel(), + clipboardManager: ClipboardManager = LocalClipboardManager.current, intentHandler: IntentHandler = IntentHandler(context = LocalContext.current), ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() val context = LocalContext.current EventsEffect(viewModel = viewModel) { event -> when (event) { - is SendEvent.NavigateNewSend -> onNavigateAddSend() + is SendEvent.CopyToClipboard -> { + clipboardManager.setText( + event.message(context.resources).toString().toAnnotatedString(), + ) + } + + is SendEvent.NavigateNewSend -> onNavigateToAddSend() is SendEvent.NavigateToAboutSend -> { intentHandler.launchUri("https://bitwarden.com/products/send".toUri()) @@ -65,6 +76,7 @@ fun SendScreen( } } + val sendHandlers = remember(viewModel) { SendHandlers.create(viewModel) } val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( state = rememberTopAppBarState(), ) @@ -135,6 +147,7 @@ fun SendScreen( is SendState.ViewState.Content -> SendContent( modifier = modifier, state = viewState, + sendHandlers = sendHandlers, ) SendState.ViewState.Empty -> SendEmpty( 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 832a17d58b..1596142309 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 @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.ui.tools.feature.send import android.os.Parcelable +import androidx.annotation.DrawableRes import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.x8bit.bitwarden.R @@ -26,6 +27,7 @@ private const val KEY_STATE = "state" /** * View model for the send screen. */ +@Suppress("TooManyFunctions") @HiltViewModel class SendViewModel @Inject constructor( savedStateHandle: SavedStateHandle, @@ -53,6 +55,11 @@ class SendViewModel @Inject constructor( SendAction.RefreshClick -> handleRefreshClick() SendAction.SearchClick -> handleSearchClick() SendAction.SyncClick -> handleSyncClick() + is SendAction.CopyClick -> handleCopyClick(action) + SendAction.FileTypeClick -> handleFileTypeClick() + is SendAction.SendClick -> handleSendClick(action) + is SendAction.ShareClick -> handleShareClick(action) + SendAction.TextTypeClick -> handleTextTypeClick() is SendAction.Internal -> handleInternalAction(action) } @@ -130,6 +137,31 @@ class SendViewModel @Inject constructor( // TODO: Add loading dialog state BIT-481 vaultRepo.sync() } + + private fun handleCopyClick(action: SendAction.CopyClick) { + // TODO: Create a link and copy it to the clipboard BIT-?? + sendEvent(SendEvent.ShowToast("Not yet implemented".asText())) + } + + private fun handleSendClick(action: SendAction.SendClick) { + // TODO: Navigate to the edit send screen BIT-?? + sendEvent(SendEvent.ShowToast("Not yet implemented".asText())) + } + + private fun handleShareClick(action: SendAction.ShareClick) { + // TODO: Create a link and use the share sheet BIT-?? + sendEvent(SendEvent.ShowToast("Not yet implemented".asText())) + } + + private fun handleFileTypeClick() { + // TODO: Navigate to the file type send list screen BIT-?? + sendEvent(SendEvent.ShowToast("Not yet implemented".asText())) + } + + private fun handleTextTypeClick() { + // TODO: Navigate to the text type send list screen BIT-?? + sendEvent(SendEvent.ShowToast("Not yet implemented".asText())) + } } /** @@ -150,12 +182,34 @@ data class SendState( abstract val shouldDisplayFab: Boolean /** - * Show the empty state. + * Show the populated state. */ @Parcelize - // TODO: Add actual content BIT-481 - data object Content : ViewState() { + data class Content( + val textTypeCount: Int, + val fileTypeCount: Int, + val sendItems: List, + ) : ViewState() { override val shouldDisplayFab: Boolean get() = true + + /** + * Represents the an individual send item to be displayed. + */ + @Parcelize + data class SendItem( + val id: String, + val name: String, + val deletionDate: String, + val type: Type, + ) : Parcelable { + /** + * Indicates the type of send this, a text or file. + */ + enum class Type(@DrawableRes val iconRes: Int) { + FILE(iconRes = R.drawable.ic_send_file), + TEXT(iconRes = R.drawable.ic_send_text), + } + } } /** @@ -220,6 +274,37 @@ sealed class SendAction { */ data object SyncClick : SendAction() + /** + * User clicked the file type button. + */ + data object FileTypeClick : SendAction() + + /** + * User clicked the text type button. + */ + data object TextTypeClick : SendAction() + + /** + * User clicked the item row. + */ + data class SendClick( + val sendItem: SendState.ViewState.Content.SendItem, + ) : SendAction() + + /** + * User clicked the copy item button. + */ + data class CopyClick( + val sendItem: SendState.ViewState.Content.SendItem, + ) : SendAction() + + /** + * User clicked the share item button. + */ + data class ShareClick( + val sendItem: SendState.ViewState.Content.SendItem, + ) : SendAction() + /** * Models actions that the [SendViewModel] itself will send. */ @@ -237,6 +322,11 @@ sealed class SendAction { * Models events for the send screen. */ sealed class SendEvent { + /** + * Copies the given [message] to the clipboard. + */ + data class CopyToClipboard(val message: Text) : SendEvent() + /** * Navigate to the new send screen. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/handlers/SendHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/handlers/SendHandlers.kt new file mode 100644 index 0000000000..90560e2623 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/handlers/SendHandlers.kt @@ -0,0 +1,35 @@ +package com.x8bit.bitwarden.ui.tools.feature.send.handlers + +import com.x8bit.bitwarden.ui.tools.feature.send.SendAction +import com.x8bit.bitwarden.ui.tools.feature.send.SendState +import com.x8bit.bitwarden.ui.tools.feature.send.SendViewModel + +/** + * A collection of handler functions for managing actions within the context of viewing + * send items. + */ +data class SendHandlers( + val onTextTypeClick: () -> Unit, + val onFileTypeClick: () -> Unit, + val onSendClick: (SendState.ViewState.Content.SendItem) -> Unit, + val onEditSendClick: (SendState.ViewState.Content.SendItem) -> Unit, + val onCopySendClick: (SendState.ViewState.Content.SendItem) -> Unit, + val onShareSendClick: (SendState.ViewState.Content.SendItem) -> Unit, +) { + companion object { + /** + * Creates an instance of [SendHandlers] by binding actions to the provided [SendViewModel]. + */ + fun create( + viewModel: SendViewModel, + ): SendHandlers = + SendHandlers( + onTextTypeClick = { viewModel.trySendAction(SendAction.TextTypeClick) }, + onFileTypeClick = { viewModel.trySendAction(SendAction.FileTypeClick) }, + onSendClick = { viewModel.trySendAction(SendAction.SendClick(it)) }, + onEditSendClick = { viewModel.trySendAction(SendAction.SendClick(it)) }, + onCopySendClick = { viewModel.trySendAction(SendAction.CopyClick(it)) }, + onShareSendClick = { viewModel.trySendAction(SendAction.ShareClick(it)) }, + ) + } +} 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 index c9f474855e..6d1aa219f2 100644 --- 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 @@ -1,9 +1,13 @@ package com.x8bit.bitwarden.ui.tools.feature.send.util +import com.bitwarden.core.SendType import com.bitwarden.core.SendView import com.x8bit.bitwarden.data.vault.repository.model.SendData +import com.x8bit.bitwarden.ui.tools.feature.generator.util.toFormattedPattern import com.x8bit.bitwarden.ui.tools.feature.send.SendState +private const val DELETION_DATE_PATTERN: String = "MMM d, uuuu, hh:mm a" + /** * Transforms [SendData] into [SendState.ViewState]. */ @@ -15,6 +19,19 @@ fun SendData.toViewState(): SendState.ViewState = ?: SendState.ViewState.Empty private fun List.toSendContent(): SendState.ViewState.Content { - // TODO: Populate with real data BIT-481 - return SendState.ViewState.Content + return SendState.ViewState.Content( + textTypeCount = this.count { it.type == SendType.TEXT }, + fileTypeCount = this.count { it.type == SendType.FILE }, + sendItems = this.map { + SendState.ViewState.Content.SendItem( + id = requireNotNull(it.id), + name = it.name, + deletionDate = it.deletionDate.toFormattedPattern(DELETION_DATE_PATTERN), + type = when (it.type) { + SendType.TEXT -> SendState.ViewState.Content.SendItem.Type.TEXT + SendType.FILE -> SendState.ViewState.Content.SendItem.Type.FILE + }, + ) + }, + ) } diff --git a/app/src/main/res/drawable/ic_send_file.xml b/app/src/main/res/drawable/ic_send_file.xml new file mode 100644 index 0000000000..495c62f406 --- /dev/null +++ b/app/src/main/res/drawable/ic_send_file.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_send_text.xml b/app/src/main/res/drawable/ic_send_text.xml new file mode 100644 index 0000000000..21350fcf24 --- /dev/null +++ b/app/src/main/res/drawable/ic_send_text.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/SendViewUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/SendViewUtil.kt index 4fcbfcf6ee..4666ad56d1 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/SendViewUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/SendViewUtil.kt @@ -9,7 +9,10 @@ import java.time.ZonedDateTime /** * Create a mock [SendView] with a given [number]. */ -fun createMockSendView(number: Int): SendView = +fun createMockSendView( + number: Int, + type: SendType = SendType.FILE, +): SendView = SendView( id = "mockId-$number", accessId = "mockAccessId-$number", @@ -17,7 +20,7 @@ fun createMockSendView(number: Int): SendView = notes = "mockNotes-$number", key = "mockKey-$number", password = "mockPassword-$number", - type = SendType.FILE, + type = type, file = createMockFileView(number = number), text = createMockTextView(number = number), maxAccessCount = 1u, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreenTest.kt index 646841d697..725442ada1 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreenTest.kt @@ -1,24 +1,39 @@ package com.x8bit.bitwarden.ui.tools.feature.send +import androidx.compose.ui.platform.ClipboardManager +import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.filterToOne import androidx.compose.ui.test.hasAnyAncestor +import androidx.compose.ui.test.hasClickAction +import androidx.compose.ui.test.hasScrollToNodeAction +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.isPopup import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onChildren import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToNode +import androidx.core.net.toUri 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.IntentHandler import com.x8bit.bitwarden.ui.platform.base.util.asText +import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString +import com.x8bit.bitwarden.ui.util.assertNoDialogExists import com.x8bit.bitwarden.ui.util.isProgressBar import io.mockk.every +import io.mockk.just import io.mockk.mockk +import io.mockk.runs 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 @@ -26,7 +41,12 @@ class SendScreenTest : BaseComposeTest() { private var onNavigateToNewSendCalled = false - private val intentHandler = mockk() + private val clipboardManager = mockk { + every { setText(any()) } just runs + } + private val intentHandler = mockk { + every { launchUri(any()) } just runs + } private val mutableEventFlow = bufferedMutableSharedFlow() private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) private val viewModel = mockk(relaxed = true) { @@ -39,12 +59,36 @@ class SendScreenTest : BaseComposeTest() { composeTestRule.setContent { SendScreen( viewModel = viewModel, - onNavigateAddSend = { onNavigateToNewSendCalled = true }, + onNavigateToAddSend = { onNavigateToNewSendCalled = true }, + clipboardManager = clipboardManager, intentHandler = intentHandler, ) } } + @Test + fun `on CopyToClipboard should call setText on the clipboardManager`() { + val text = "copy text" + mutableEventFlow.tryEmit(SendEvent.CopyToClipboard(text.asText())) + verify { + clipboardManager.setText(text.toAnnotatedString()) + } + } + + @Test + fun `on NavigateToNewSend should call onNavigateToNewSend`() { + mutableEventFlow.tryEmit(SendEvent.NavigateNewSend) + assertTrue(onNavigateToNewSendCalled) + } + + @Test + fun `on NavigateToAboutSend should call launchUri on intentHandler`() { + mutableEventFlow.tryEmit(SendEvent.NavigateToAboutSend) + verify { + intentHandler.launchUri("https://bitwarden.com/products/send".toUri()) + } + } + @Test fun `on overflow item click should display menu`() { composeTestRule @@ -131,7 +175,7 @@ class SendScreenTest : BaseComposeTest() { composeTestRule.onNodeWithContentDescription("Add item").assertDoesNotExist() mutableStateFlow.update { - it.copy(viewState = SendState.ViewState.Content) + it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE) } composeTestRule.onNodeWithContentDescription("Add item").assertIsDisplayed() } @@ -166,12 +210,6 @@ class SendScreenTest : BaseComposeTest() { verify { viewModel.trySendAction(SendAction.SearchClick) } } - @Test - fun `on NavigateToNewSend should call onNavigateToNewSend`() { - mutableEventFlow.tryEmit(SendEvent.NavigateNewSend) - assert(onNavigateToNewSendCalled) - } - @Test fun `progressbar should be displayed according to state`() { mutableStateFlow.update { @@ -190,7 +228,7 @@ class SendScreenTest : BaseComposeTest() { composeTestRule.onNode(isProgressBar).assertDoesNotExist() mutableStateFlow.update { - it.copy(viewState = SendState.ViewState.Content) + it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE) } composeTestRule.onNode(isProgressBar).assertDoesNotExist() } @@ -216,8 +254,265 @@ class SendScreenTest : BaseComposeTest() { viewModel.trySendAction(SendAction.RefreshClick) } } + + @Test + fun `text type count should be updated according to state`() { + val rowText = "Text" + mutableStateFlow.update { + it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE) + } + composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(rowText)) + composeTestRule + .onAllNodes(hasText(rowText)) + .filterToOne(hasClickAction()) + .assertTextEquals(rowText, 1.toString()) + + mutableStateFlow.update { + it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE.copy(textTypeCount = 3)) + } + composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(rowText)) + composeTestRule + .onAllNodes(hasText(rowText)) + .filterToOne(hasClickAction()) + .assertTextEquals(rowText, 3.toString()) + } + + @Test + fun `text type row click should send TextTypeClick`() { + val rowText = "Text" + mutableStateFlow.update { + it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE) + } + composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(rowText)) + composeTestRule + .onAllNodes(hasText(rowText)) + .filterToOne(hasClickAction()) + .performClick() + + verify { + viewModel.trySendAction(SendAction.TextTypeClick) + } + } + + @Test + fun `file type count should be updated according to state`() { + val rowText = "File" + mutableStateFlow.update { + it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE) + } + composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(rowText)) + composeTestRule + .onAllNodes(hasText(rowText)) + .filterToOne(hasClickAction()) + .assertTextEquals(rowText, 1.toString()) + + mutableStateFlow.update { + it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE.copy(fileTypeCount = 3)) + } + composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(rowText)) + composeTestRule + .onAllNodes(hasText(rowText)) + .filterToOne(hasClickAction()) + .assertTextEquals(rowText, 3.toString()) + } + + @Test + fun `file type row click should send FileTypeClick`() { + val rowText = "File" + mutableStateFlow.update { + it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE) + } + composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(rowText)) + composeTestRule + .onAllNodes(hasText(rowText)) + .filterToOne(hasClickAction()) + .performClick() + + verify { + viewModel.trySendAction(SendAction.FileTypeClick) + } + } + + @Test + fun `on send item click should send SendClick`() { + val rowText = "mockName-1" + mutableStateFlow.update { + it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE) + } + composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(rowText)) + composeTestRule + .onAllNodes(hasText(rowText)) + .filterToOne(hasClickAction()) + .performClick() + + verify { + viewModel.trySendAction(SendAction.SendClick(DEFAULT_SEND_ITEM)) + } + } + + @Test + fun `on send item overflow click should display dialog`() { + mutableStateFlow.update { + it.copy( + viewState = SendState.ViewState.Content( + textTypeCount = 0, + fileTypeCount = 1, + sendItems = listOf(DEFAULT_SEND_ITEM), + ), + ) + } + composeTestRule.assertNoDialogExists() + + composeTestRule + .onNodeWithContentDescription("Options") + .assertIsDisplayed() + .performClick() + + composeTestRule + .onNode(isDialog()) + .onChildren() + .filterToOne(hasText(DEFAULT_SEND_ITEM.name)) + .assertIsDisplayed() + } + + @Test + fun `on send item overflow dialog edit click should send SendClick`() { + mutableStateFlow.update { + it.copy( + viewState = SendState.ViewState.Content( + textTypeCount = 0, + fileTypeCount = 1, + sendItems = listOf(DEFAULT_SEND_ITEM), + ), + ) + } + composeTestRule.assertNoDialogExists() + + composeTestRule + .onNodeWithContentDescription("Options") + .assertIsDisplayed() + .performClick() + + composeTestRule + .onNodeWithText("Edit") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + verify { + viewModel.trySendAction(SendAction.SendClick(DEFAULT_SEND_ITEM)) + } + + composeTestRule.assertNoDialogExists() + } + + @Test + fun `on send item overflow dialog copy click should send CopyClick`() { + mutableStateFlow.update { + it.copy( + viewState = SendState.ViewState.Content( + textTypeCount = 0, + fileTypeCount = 1, + sendItems = listOf(DEFAULT_SEND_ITEM), + ), + ) + } + composeTestRule.assertNoDialogExists() + + composeTestRule + .onNodeWithContentDescription("Options") + .assertIsDisplayed() + .performClick() + + composeTestRule + .onNodeWithText("Copy link") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + verify { + viewModel.trySendAction(SendAction.CopyClick(DEFAULT_SEND_ITEM)) + } + + composeTestRule.assertNoDialogExists() + } + + @Test + fun `on send item overflow dialog share link click should send ShareClick`() { + mutableStateFlow.update { + it.copy( + viewState = SendState.ViewState.Content( + textTypeCount = 0, + fileTypeCount = 1, + sendItems = listOf(DEFAULT_SEND_ITEM), + ), + ) + } + composeTestRule.assertNoDialogExists() + + composeTestRule + .onNodeWithContentDescription("Options") + .assertIsDisplayed() + .performClick() + + composeTestRule + .onNodeWithText("Share link") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + verify { + viewModel.trySendAction(SendAction.ShareClick(DEFAULT_SEND_ITEM)) + } + + composeTestRule.assertNoDialogExists() + } + + @Test + fun `on send item overflow dialog cancel click should close the dialog`() { + mutableStateFlow.update { + it.copy( + viewState = SendState.ViewState.Content( + textTypeCount = 0, + fileTypeCount = 1, + sendItems = listOf(DEFAULT_SEND_ITEM), + ), + ) + } + composeTestRule.assertNoDialogExists() + + composeTestRule + .onNodeWithContentDescription("Options") + .assertIsDisplayed() + .performClick() + + composeTestRule + .onNodeWithText("Cancel") + .assert(hasAnyAncestor(isDialog())) + .performClick() + composeTestRule.assertNoDialogExists() + } } private val DEFAULT_STATE: SendState = SendState( viewState = SendState.ViewState.Loading, ) + +private val DEFAULT_SEND_ITEM: SendState.ViewState.Content.SendItem = + SendState.ViewState.Content.SendItem( + id = "mockId-1", + name = "mockName-1", + deletionDate = "1", + type = SendState.ViewState.Content.SendItem.Type.FILE, + ) + +private val DEFAULT_CONTENT_VIEW_STATE: SendState.ViewState.Content = SendState.ViewState.Content( + textTypeCount = 1, + fileTypeCount = 1, + sendItems = listOf( + DEFAULT_SEND_ITEM, + SendState.ViewState.Content.SendItem( + id = "mockId-2", + name = "mockName-2", + deletionDate = "1", + type = SendState.ViewState.Content.SendItem.Type.TEXT, + ), + ), +) 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 995252dfef..03106cf141 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 @@ -110,6 +110,54 @@ class SendViewModelTest : BaseViewModelTest() { } } + @Test + fun `CopyClick should emit ShowToast`() = runTest { + val viewModel = createViewModel() + val sendItem = mockk() + viewModel.eventFlow.test { + viewModel.trySendAction(SendAction.CopyClick(sendItem)) + assertEquals(SendEvent.ShowToast("Not yet implemented".asText()), awaitItem()) + } + } + + @Test + fun `SendClick should emit ShowToast`() = runTest { + val viewModel = createViewModel() + val sendItem = mockk() + viewModel.eventFlow.test { + viewModel.trySendAction(SendAction.SendClick(sendItem)) + assertEquals(SendEvent.ShowToast("Not yet implemented".asText()), awaitItem()) + } + } + + @Test + fun `ShareClick should emit ShowToast`() = runTest { + val viewModel = createViewModel() + val sendItem = mockk() + viewModel.eventFlow.test { + viewModel.trySendAction(SendAction.ShareClick(sendItem)) + assertEquals(SendEvent.ShowToast("Not yet implemented".asText()), awaitItem()) + } + } + + @Test + fun `FileTypeClick should emit ShowToast`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(SendAction.FileTypeClick) + assertEquals(SendEvent.ShowToast("Not yet implemented".asText()), awaitItem()) + } + } + + @Test + fun `TextTypeClick should emit ShowToast`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(SendAction.TextTypeClick) + assertEquals(SendEvent.ShowToast("Not yet implemented".asText()), awaitItem()) + } + } + @Test fun `VaultRepository SendData Error should update view state to Error`() { val viewModel = createViewModel() @@ -129,7 +177,7 @@ class SendViewModelTest : BaseViewModelTest() { @Test fun `VaultRepository SendData Loaded should update view state`() { val viewModel = createViewModel() - val viewState = SendState.ViewState.Content + val viewState = mockk() val sendData = mockk { every { toViewState() } returns viewState } @@ -172,7 +220,7 @@ class SendViewModelTest : BaseViewModelTest() { @Test fun `VaultRepository SendData Pending should update view state`() { val viewModel = createViewModel() - val viewState = SendState.ViewState.Content + val viewState = mockk() val sendData = mockk { every { toViewState() } returns viewState } 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 index 2bd5ed79b9..1540246338 100644 --- 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 @@ -1,13 +1,29 @@ package com.x8bit.bitwarden.ui.tools.feature.send.util +import com.bitwarden.core.SendType 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.AfterEach import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import java.util.TimeZone class SendDataExtensionsTest { + @BeforeEach + fun setup() { + // Setting the timezone so the tests pass consistently no matter the environment. + TimeZone.setDefault(TimeZone.getTimeZone("UTC")) + } + + @AfterEach + fun tearDown() { + // Clearing the timezone after the test. + TimeZone.setDefault(null) + } + @Test fun `toViewState should return Empty when SendData is empty`() { val sendData = SendData(emptyList()) @@ -20,12 +36,33 @@ class SendDataExtensionsTest { @Test fun `toViewState should return Content when SendData is not empty`() { val list = listOf( - createMockSendView(number = 1), + createMockSendView(number = 1, type = SendType.FILE), + createMockSendView(number = 2, type = SendType.TEXT), ) val sendData = SendData(list) val result = sendData.toViewState() - assertEquals(SendState.ViewState.Content, result) + assertEquals( + SendState.ViewState.Content( + textTypeCount = 1, + fileTypeCount = 1, + sendItems = listOf( + SendState.ViewState.Content.SendItem( + id = "mockId-1", + name = "mockName-1", + deletionDate = "Oct 27, 2023, 12:00 PM", + type = SendState.ViewState.Content.SendItem.Type.FILE, + ), + SendState.ViewState.Content.SendItem( + id = "mockId-2", + name = "mockName-2", + deletionDate = "Oct 27, 2023, 12:00 PM", + type = SendState.ViewState.Content.SendItem.Type.TEXT, + ), + ), + ), + result, + ) } }