mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
Populate the send screen with real data (#488)
This commit is contained in:
parent
da53c72a61
commit
15fcfce0b2
16 changed files with 1023 additions and 34 deletions
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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<SelectionItemData>,
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ fun NavGraphBuilder.sendDestination(
|
|||
route = SEND_ROUTE,
|
||||
) {
|
||||
SendScreen(
|
||||
onNavigateAddSend = onNavigateToAddSend,
|
||||
onNavigateToAddSend = onNavigateToAddSend,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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<SendItem>,
|
||||
) : 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.
|
||||
*/
|
||||
|
|
|
@ -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)) },
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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<SendView>.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
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
13
app/src/main/res/drawable/ic_send_file.xml
Normal file
13
app/src/main/res/drawable/ic_send_file.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportHeight="20"
|
||||
android:viewportWidth="20">
|
||||
<group>
|
||||
<clip-path android:pathData="M0,0h20v20h-20z" />
|
||||
<path
|
||||
android:fillColor="#1B1B1F"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M1.25,18.125V1.875C1.25,0.84 2.089,0 3.125,0H10.536C11.055,0 11.552,0.216 11.906,0.596L16.996,6.051C17.32,6.398 17.5,6.855 17.5,7.33V18.125C17.5,19.161 16.66,20 15.625,20H3.125C2.089,20 1.25,19.161 1.25,18.125ZM16.25,7.5V18.125C16.25,18.47 15.97,18.75 15.625,18.75H3.125C2.78,18.75 2.5,18.47 2.5,18.125V1.875C2.5,1.53 2.78,1.25 3.125,1.25H10V6.25C10,6.94 10.56,7.5 11.25,7.5H16.25ZM11.25,1.725L15.472,6.25H11.25V1.725Z" />
|
||||
</group>
|
||||
</vector>
|
22
app/src/main/res/drawable/ic_send_text.xml
Normal file
22
app/src/main/res/drawable/ic_send_text.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportHeight="20"
|
||||
android:viewportWidth="20">
|
||||
<path
|
||||
android:fillColor="#1B1B1F"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M1.25,18.125V1.875C1.25,0.839 2.089,0 3.125,0H10.536C11.055,0 11.552,0.216 11.906,0.596L16.996,6.05C17.32,6.398 17.5,6.855 17.5,7.33V18.125C17.5,19.16 16.66,20 15.625,20H3.125C2.089,20 1.25,19.16 1.25,18.125ZM16.25,7.5V18.125C16.25,18.47 15.97,18.75 15.625,18.75H3.125C2.78,18.75 2.5,18.47 2.5,18.125V1.875C2.5,1.53 2.78,1.25 3.125,1.25H10V6.25C10,6.94 10.56,7.5 11.25,7.5H16.25ZM11.25,1.724L15.472,6.25H11.25V1.724Z" />
|
||||
<path
|
||||
android:fillColor="#1B1B1F"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M5,10.625C5,10.28 5.28,10 5.625,10H13.125C13.47,10 13.75,10.28 13.75,10.625C13.75,10.97 13.47,11.25 13.125,11.25H5.625C5.28,11.25 5,10.97 5,10.625Z" />
|
||||
<path
|
||||
android:fillColor="#1B1B1F"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M5,13.125C5,12.78 5.28,12.5 5.625,12.5H13.125C13.47,12.5 13.75,12.78 13.75,13.125C13.75,13.47 13.47,13.75 13.125,13.75H5.625C5.28,13.75 5,13.47 5,13.125Z" />
|
||||
<path
|
||||
android:fillColor="#1B1B1F"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M5,15.625C5,15.28 5.28,15 5.625,15H13.125C13.47,15 13.75,15.28 13.75,15.625C13.75,15.97 13.47,16.25 13.125,16.25H5.625C5.28,16.25 5,15.97 5,15.625Z" />
|
||||
</vector>
|
|
@ -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,
|
||||
|
|
|
@ -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<IntentHandler>()
|
||||
private val clipboardManager = mockk<ClipboardManager> {
|
||||
every { setText(any()) } just runs
|
||||
}
|
||||
private val intentHandler = mockk<IntentHandler> {
|
||||
every { launchUri(any()) } just runs
|
||||
}
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<SendEvent>()
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val viewModel = mockk<SendViewModel>(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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -110,6 +110,54 @@ class SendViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CopyClick should emit ShowToast`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val sendItem = mockk<SendState.ViewState.Content.SendItem>()
|
||||
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<SendState.ViewState.Content.SendItem>()
|
||||
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<SendState.ViewState.Content.SendItem>()
|
||||
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<SendState.ViewState.Content>()
|
||||
val sendData = mockk<SendData> {
|
||||
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<SendState.ViewState.Content>()
|
||||
val sendData = mockk<SendData> {
|
||||
every { toViewState() } returns viewState
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue