BIT-1518: Process shared sends (#698)

This commit is contained in:
David Perez 2024-01-21 16:24:53 -06:00 committed by Álison Fernandes
parent 4510695f76
commit c9d7a48598
14 changed files with 462 additions and 60 deletions

View file

@ -0,0 +1,14 @@
package com.x8bit.bitwarden.ui.platform.manager.exit
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
/**
* A manager class for handling the various ways to exit the app.
*/
@OmitFromCoverage
interface ExitManager {
/**
* Finishes the activity.
*/
fun exitApplication()
}

View file

@ -0,0 +1,16 @@
package com.x8bit.bitwarden.ui.platform.manager.exit
import android.app.Activity
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
/**
* The default implementation of the [ExitManager] for managing the various ways to exit the app.
*/
@OmitFromCoverage
class ExitManagerImpl(
val activity: Activity,
) : ExitManager {
override fun exitApplication() {
activity.finish()
}
}

View file

@ -11,12 +11,6 @@ import androidx.compose.runtime.Composable
*/
@Suppress("TooManyFunctions")
interface IntentManager {
/**
* Starts an intent to exit the application.
*/
fun exitApplication()
/**
* Start an activity using the provided [Intent].
*/
@ -60,7 +54,12 @@ interface IntentManager {
/**
* Processes the [activityResult] and attempts to get the relevant file data from it.
*/
fun getFileDataFromIntent(activityResult: ActivityResult): FileData?
fun getFileDataFromActivityResult(activityResult: ActivityResult): FileData?
/**
* Processes the [intent] and attempts to get the relevant file data from it.
*/
fun getFileDataFromIntent(intent: Intent): FileData?
/**
* Processes the [intent] and attempts to derive [ShareData] information from it.
@ -97,7 +96,7 @@ interface IntentManager {
* The data required to create a new File Send.
*/
data class FileSend(
val fileData: IntentManager.FileData,
val fileData: FileData,
) : ShareData()
}
}

View file

@ -51,16 +51,6 @@ class IntentManagerImpl(
private val context: Context,
private val clock: Clock = Clock.systemDefaultZone(),
) : IntentManager {
override fun exitApplication() {
// Note that we fire an explicit Intent rather than try to cast to an Activity and call
// finish to avoid assumptions about what kind of context we have.
val intent = Intent(Intent.ACTION_MAIN).apply {
addCategory(Intent.CATEGORY_HOME)
}
startActivity(intent)
}
override fun startActivity(intent: Intent) {
context.startActivity(intent)
}
@ -116,12 +106,17 @@ class IntentManagerImpl(
startActivity(Intent.createChooser(sendIntent, null))
}
override fun getFileDataFromIntent(activityResult: ActivityResult): IntentManager.FileData? {
override fun getFileDataFromActivityResult(
activityResult: ActivityResult,
): IntentManager.FileData? {
if (activityResult.resultCode != Activity.RESULT_OK) return null
val uri = activityResult.data?.data
return if (uri != null) getLocalFileData(uri) else getCameraFileData()
}
override fun getFileDataFromIntent(intent: Intent): IntentManager.FileData? =
intent.clipData?.getItemAt(0)?.uri?.let { getLocalFileData(it) }
@Suppress("ReturnCount")
override fun getShareDataFromIntent(intent: Intent): IntentManager.ShareData? {
if (intent.action != Intent.ACTION_SEND) return null
@ -133,12 +128,7 @@ class IntentManagerImpl(
text = title,
)
} else {
getFileDataFromIntent(
ActivityResult(
Activity.RESULT_OK,
intent,
),
)
getFileDataFromIntent(intent = intent)
?.let {
IntentManager.ShareData.FileSend(
fileData = it,

View file

@ -23,6 +23,8 @@ import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManagerImpl
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManagerImpl
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
@ -45,6 +47,7 @@ fun BitwardenTheme(
// Get the current scheme
val context = LocalContext.current
val activity = context as Activity
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
@ -76,8 +79,9 @@ fun BitwardenTheme(
CompositionLocalProvider(
LocalNonMaterialColors provides nonMaterialColors,
LocalNonMaterialTypography provides nonMaterialTypography,
LocalPermissionsManager provides PermissionsManagerImpl(context as Activity),
LocalPermissionsManager provides PermissionsManagerImpl(activity),
LocalIntentManager provides IntentManagerImpl(context),
LocalExitManager provides ExitManagerImpl(activity),
) {
// Set overall theme based on color scheme and typography settings
MaterialTheme(
@ -166,6 +170,13 @@ private fun lightColorScheme(context: Context): ColorScheme =
private fun Int.toColor(context: Context): Color =
Color(context.getColor(this))
/**
* Provides access to the exit manager throughout the app.
*/
val LocalExitManager: ProvidableCompositionLocal<ExitManager> = compositionLocalOf {
error("CompositionLocal ExitManager not present")
}
/**
* Provides access to the intent manager throughout the app.
*/

View file

@ -55,6 +55,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandler
fun AddSendContent(
state: AddSendState.ViewState.Content,
isAddMode: Boolean,
isShared: Boolean,
addSendHandlers: AddSendHandlers,
permissionsManager: PermissionsManager,
modifier: Modifier = Modifier,
@ -77,7 +78,7 @@ fun AddSendContent(
onValueChange = addSendHandlers.onNamChange,
)
if (isAddMode) {
if (isAddMode && !isShared) {
Spacer(modifier = Modifier.height(16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.type),
@ -116,7 +117,25 @@ fun AddSendContent(
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
if (isAddMode) {
if (isShared) {
Text(
text = type.name.orEmpty(),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(id = R.string.max_file_size),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
} else if (isAddMode) {
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = type.name ?: stringResource(id = R.string.no_file_chosen),

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addsend
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
@ -33,9 +34,12 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.NavigationIcon
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
import com.x8bit.bitwarden.ui.platform.theme.LocalExitManager
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.theme.LocalPermissionsManager
import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
@ -49,6 +53,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandler
@Composable
fun AddSendScreen(
viewModel: AddSendViewModel = hiltViewModel(),
exitManager: ExitManager = LocalExitManager.current,
intentManager: IntentManager = LocalIntentManager.current,
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
onNavigateBack: () -> Unit,
@ -59,13 +64,20 @@ fun AddSendScreen(
val resources = context.resources
val fileChooserLauncher = intentManager.launchActivityForResult { activityResult ->
intentManager.getFileDataFromIntent(activityResult)?.let {
intentManager.getFileDataFromActivityResult(activityResult)?.let {
viewModel.trySendAction(AddSendAction.FileChoose(it))
}
}
BackHandler(
onBack = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.CloseClick) }
},
)
EventsEffect(viewModel = viewModel) { event ->
when (event) {
AddSendEvent.ExitApp -> exitManager.exitApplication()
is AddSendEvent.NavigateBack -> onNavigateBack()
is AddSendEvent.ShowChooserSheet -> {
@ -115,11 +127,14 @@ fun AddSendScreen(
topBar = {
BitwardenTopAppBar(
title = state.screenDisplayName(),
navigationIcon = painterResource(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.CloseClick) }
},
navigationIcon = NavigationIcon(
navigationIcon = painterResource(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.CloseClick) }
},
)
.takeUnless { state.isShared },
scrollBehavior = scrollBehavior,
actions = {
BitwardenTextButton(
@ -174,6 +189,7 @@ fun AddSendScreen(
is AddSendState.ViewState.Content -> AddSendContent(
state = viewState,
isAddMode = state.isAddMode,
isShared = state.isShared,
addSendHandlers = remember(viewModel) { AddSendHandlers.create(viewModel) },
permissionsManager = permissionsManager,
modifier = modifier,

View file

@ -24,6 +24,9 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.shouldFinishOnComplete
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toSendName
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toSendType
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toSendView
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toViewState
import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendUrl
@ -54,7 +57,7 @@ private const val MAX_FILE_SIZE_BYTES: Long = 100 * 1024 * 1024
@HiltViewModel
class AddSendViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
authRepo: AuthRepository,
private val authRepo: AuthRepository,
private val clock: Clock,
private val clipboardManager: BitwardenClipboardManager,
private val environmentRepo: EnvironmentRepository,
@ -62,13 +65,18 @@ class AddSendViewModel @Inject constructor(
) : BaseViewModel<AddSendState, AddSendEvent, AddSendAction>(
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE] ?: run {
val addSendType = AddSendArgs(savedStateHandle).sendAddType
// Check to see if we are navigating here from an external source
val specialCircumstance = authRepo.specialCircumstance
val shareSendType = specialCircumstance.toSendType()
val sendAddType = AddSendArgs(savedStateHandle).sendAddType
AddSendState(
addSendType = addSendType,
viewState = when (addSendType) {
shouldFinishOnComplete = specialCircumstance.shouldFinishOnComplete(),
isShared = shareSendType != null,
addSendType = sendAddType,
viewState = when (sendAddType) {
AddSendType.AddItem -> AddSendState.ViewState.Content(
common = AddSendState.ViewState.Content.Common(
name = "",
name = specialCircumstance.toSendName().orEmpty(),
currentAccessCount = null,
maxAccessCount = null,
passwordInput = "",
@ -85,7 +93,7 @@ class AddSendViewModel @Inject constructor(
sendUrl = null,
hasPassword = false,
),
selectedType = AddSendState.ViewState.Content.SendType.Text(
selectedType = shareSendType ?: AddSendState.ViewState.Content.SendType.Text(
input = "",
isHideByDefaultChecked = false,
),
@ -179,12 +187,17 @@ class AddSendViewModel @Inject constructor(
is CreateSendResult.Success -> {
mutableStateFlow.update { it.copy(dialogState = null) }
sendEvent(AddSendEvent.NavigateBack)
sendEvent(
AddSendEvent.ShowShareSheet(
message = result.sendView.toSendUrl(state.baseWebSendUrl),
),
)
if (state.isShared) {
navigateBack()
clipboardManager.setText(result.sendView.toSendUrl(state.baseWebSendUrl))
} else {
navigateBack()
sendEvent(
AddSendEvent.ShowShareSheet(
message = result.sendView.toSendUrl(state.baseWebSendUrl),
),
)
}
}
}
}
@ -209,7 +222,7 @@ class AddSendViewModel @Inject constructor(
is UpdateSendResult.Success -> {
mutableStateFlow.update { it.copy(dialogState = null) }
sendEvent(AddSendEvent.NavigateBack)
navigateBack()
sendEvent(
AddSendEvent.ShowShareSheet(
message = result.sendView.toSendUrl(state.baseWebSendUrl),
@ -236,7 +249,7 @@ class AddSendViewModel @Inject constructor(
is DeleteSendResult.Success -> {
mutableStateFlow.update { it.copy(dialogState = null) }
sendEvent(AddSendEvent.NavigateBack)
navigateBack()
sendEvent(AddSendEvent.ShowToast(message = R.string.send_deleted.asText()))
}
}
@ -421,7 +434,7 @@ class AddSendViewModel @Inject constructor(
}
}
private fun handleCloseClick() = sendEvent(AddSendEvent.NavigateBack)
private fun handleCloseClick() = navigateBack()
private fun handleDeletionDateChange(action: AddSendAction.DeletionDateChange) {
updateCommonContent {
@ -582,6 +595,17 @@ class AddSendViewModel @Inject constructor(
}
}
private fun navigateBack() {
authRepo.specialCircumstance = null
sendEvent(
event = if (state.shouldFinishOnComplete) {
AddSendEvent.ExitApp
} else {
AddSendEvent.NavigateBack
},
)
}
private inline fun onContent(
crossinline block: (AddSendState.ViewState.Content) -> Unit,
) {
@ -645,7 +669,9 @@ data class AddSendState(
val addSendType: AddSendType,
val dialogState: DialogState?,
val viewState: ViewState,
val shouldFinishOnComplete: Boolean,
val isPremiumUser: Boolean,
val isShared: Boolean,
val baseWebSendUrl: String,
) : Parcelable {
@ -783,6 +809,11 @@ data class AddSendState(
* Models events for the new send screen.
*/
sealed class AddSendEvent {
/**
* Closes the app.
*/
data object ExitApp : AddSendEvent()
/**
* Navigate back.
*/

View file

@ -0,0 +1,55 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addsend.util
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendState
/**
* Determines the initial [AddSendState.ViewState.Content.SendType] based on the data in the
* [UserState.SpecialCircumstance].
*/
fun UserState.SpecialCircumstance?.toSendType(): AddSendState.ViewState.Content.SendType? =
when (this) {
is UserState.SpecialCircumstance.ShareNewSend -> {
when (data) {
is IntentManager.ShareData.FileSend -> AddSendState.ViewState.Content.SendType.File(
uri = data.fileData.uri,
name = data.fileData.fileName,
sizeBytes = data.fileData.sizeBytes,
displaySize = null,
)
is IntentManager.ShareData.TextSend -> AddSendState.ViewState.Content.SendType.Text(
input = data.text,
isHideByDefaultChecked = false,
)
}
}
else -> null
}
/**
* Determines the initial send name based on the data in the [UserState.SpecialCircumstance].
*/
fun UserState.SpecialCircumstance?.toSendName(): String? =
when (this) {
is UserState.SpecialCircumstance.ShareNewSend -> {
when (data) {
is IntentManager.ShareData.FileSend -> data.fileData.fileName
is IntentManager.ShareData.TextSend -> data.subject
}
}
else -> null
}
/**
* Determines if the [UserState.SpecialCircumstance] requires the app to be closed after completing
* the send.
*/
fun UserState.SpecialCircumstance?.shouldFinishOnComplete(): Boolean =
when (this) {
is UserState.SpecialCircumstance.ShareNewSend -> shouldFinishWhenComplete
else -> false
}

View file

@ -51,8 +51,8 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
import com.x8bit.bitwarden.ui.platform.theme.LocalExitManager
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
import kotlinx.collections.immutable.persistentListOf
@ -72,7 +72,7 @@ fun VaultScreen(
onNavigateToVerificationCodeScreen: () -> Unit,
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
intentManager: IntentManager = LocalIntentManager.current,
exitManager: ExitManager = LocalExitManager.current,
) {
val state by viewModel.stateFlow.collectAsState()
val context = LocalContext.current
@ -107,7 +107,7 @@ fun VaultScreen(
onNavigateToVaultItemListingScreen(event.itemListingType)
}
VaultEvent.NavigateOutOfApp -> intentManager.exitApplication()
VaultEvent.NavigateOutOfApp -> exitManager.exitApplication()
is VaultEvent.ShowToast -> {
Toast
.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT)

View file

@ -23,6 +23,7 @@ import androidx.compose.ui.test.performTextInput
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
@ -45,6 +46,9 @@ class AddSendScreenTest : BaseComposeTest() {
private var onNavigateBackCalled = false
private val exitManager: ExitManager = mockk(relaxed = true) {
every { exitApplication() } just runs
}
private val permissionsManager = FakePermissionManager()
private val intentManager: IntentManager = mockk(relaxed = true) {
every { shareText(any()) } just runs
@ -61,6 +65,7 @@ class AddSendScreenTest : BaseComposeTest() {
composeTestRule.setContent {
AddSendScreen(
viewModel = viewModel,
exitManager = exitManager,
intentManager = intentManager,
permissionsManager = permissionsManager,
onNavigateBack = { onNavigateBackCalled = true },
@ -74,6 +79,14 @@ class AddSendScreenTest : BaseComposeTest() {
assert(onNavigateBackCalled)
}
@Test
fun `ExitApp should call exitApplication on ExitManager`() {
mutableEventFlow.tryEmit(AddSendEvent.ExitApp)
verify {
exitManager.exitApplication()
}
}
@Test
fun `on ShowShareSheet should call shareText on IntentManager`() {
val text = "sharable stuff"
@ -91,6 +104,14 @@ class AddSendScreenTest : BaseComposeTest() {
verify { viewModel.trySendAction(AddSendAction.CloseClick) }
}
@Test
fun `display navigation icon according to state`() {
mutableStateFlow.update { it.copy(isShared = false) }
composeTestRule.onNodeWithContentDescription("Close").assertIsDisplayed()
mutableStateFlow.update { it.copy(isShared = true) }
composeTestRule.onNodeWithContentDescription("Close").assertDoesNotExist()
}
@Test
fun `on save click should send SaveClick`() {
composeTestRule
@ -282,7 +303,25 @@ class AddSendScreenTest : BaseComposeTest() {
@Test
fun `segmented buttons should appear based on state`() {
mutableStateFlow.update { it.copy(addSendType = AddSendType.AddItem) }
mutableStateFlow.update { it.copy(isShared = true) }
composeTestRule
.onNodeWithText("Type")
.assertDoesNotExist()
composeTestRule
.onAllNodesWithText("File")
.filterToOne(!isEditableText)
.assertDoesNotExist()
composeTestRule
.onAllNodesWithText("Text")
.filterToOne(!isEditableText)
.assertDoesNotExist()
mutableStateFlow.update {
it.copy(
isShared = false,
addSendType = AddSendType.AddItem,
)
}
composeTestRule
.onNodeWithText("Type")
.performScrollTo()
@ -889,6 +928,8 @@ class AddSendScreenTest : BaseComposeTest() {
addSendType = AddSendType.AddItem,
viewState = DEFAULT_VIEW_STATE,
dialogState = null,
shouldFinishOnComplete = false,
isShared = false,
isPremiumUser = false,
baseWebSendUrl = "https://vault.bitwarden.com/#/send/",
)

View file

@ -56,6 +56,8 @@ class AddSendViewModelTest : BaseViewModelTest() {
}
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
private val authRepository: AuthRepository = mockk {
every { specialCircumstance } returns null
every { specialCircumstance = null } just runs
every { userStateFlow } returns mutableUserStateFlow
}
private val environmentRepository: EnvironmentRepository = mockk {
@ -108,8 +110,9 @@ class AddSendViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `SaveClick with createSend success should emit NavigateBack and ShowShareSheet`() =
fun `SaveClick with createSend success should emit NavigateBack and ShowShareSheet when not an external shared`() =
runTest {
val viewState = DEFAULT_VIEW_STATE.copy(
common = DEFAULT_COMMON_STATE.copy(name = "input"),
@ -137,6 +140,76 @@ class AddSendViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `SaveClick with createSend success should copy the send URL to the clipboard and emit NavigateBack`() =
runTest {
val viewState = DEFAULT_VIEW_STATE.copy(
common = DEFAULT_COMMON_STATE.copy(name = "input"),
)
val initialState = DEFAULT_STATE.copy(
shouldFinishOnComplete = false,
isShared = true,
viewState = viewState,
)
val mockSendView = mockk<SendView>()
every { viewState.toSendView(clock) } returns mockSendView
val sendUrl = "www.test.com/send/test"
val resultSendView = mockk<SendView> {
every { toSendUrl(DEFAULT_ENVIRONMENT_URL) } returns sendUrl
}
coEvery {
vaultRepository.createSend(sendView = mockSendView, fileUri = null)
} returns CreateSendResult.Success(sendView = resultSendView)
val viewModel = createViewModel(initialState)
viewModel.eventFlow.test {
viewModel.trySendAction(AddSendAction.SaveClick)
assertEquals(AddSendEvent.NavigateBack, awaitItem())
}
assertEquals(initialState, viewModel.stateFlow.value)
coVerify(exactly = 1) {
vaultRepository.createSend(sendView = mockSendView, fileUri = null)
authRepository.specialCircumstance = null
clipboardManager.setText(sendUrl)
}
}
@Suppress("MaxLineLength")
@Test
fun `SaveClick with createSend success should copy the send URL to the clipboard and emit ExitApp`() =
runTest {
val viewState = DEFAULT_VIEW_STATE.copy(
common = DEFAULT_COMMON_STATE.copy(name = "input"),
)
val initialState = DEFAULT_STATE.copy(
shouldFinishOnComplete = true,
isShared = true,
viewState = viewState,
)
val mockSendView = mockk<SendView>()
every { viewState.toSendView(clock) } returns mockSendView
val sendUrl = "www.test.com/send/test"
val resultSendView = mockk<SendView> {
every { toSendUrl(DEFAULT_ENVIRONMENT_URL) } returns sendUrl
}
coEvery {
vaultRepository.createSend(sendView = mockSendView, fileUri = null)
} returns CreateSendResult.Success(sendView = resultSendView)
val viewModel = createViewModel(initialState)
viewModel.eventFlow.test {
viewModel.trySendAction(AddSendAction.SaveClick)
assertEquals(AddSendEvent.ExitApp, awaitItem())
}
assertEquals(initialState, viewModel.stateFlow.value)
coVerify(exactly = 1) {
vaultRepository.createSend(sendView = mockSendView, fileUri = null)
authRepository.specialCircumstance = null
clipboardManager.setText(sendUrl)
}
}
@Test
fun `SaveClick with createSend failure should show error dialog`() = runTest {
val viewState = DEFAULT_VIEW_STATE.copy(
@ -915,6 +988,8 @@ class AddSendViewModelTest : BaseViewModelTest() {
addSendType = AddSendType.AddItem,
viewState = DEFAULT_VIEW_STATE,
dialogState = null,
shouldFinishOnComplete = false,
isShared = false,
isPremiumUser = false,
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
)

View file

@ -0,0 +1,135 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addsend.util
import android.net.Uri
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendState
import io.mockk.mockk
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class SpecialCircumstanceExtensionsTest {
@Test
fun `toSendType with TextSend should return Text SendType with correct text`() {
val text = "Share Text"
val expected = AddSendState.ViewState.Content.SendType.Text(
input = text,
isHideByDefaultChecked = false,
)
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
data = IntentManager.ShareData.TextSend(
subject = "",
text = text,
),
shouldFinishWhenComplete = false,
)
val result = specialCircumstance.toSendType()
assertEquals(expected, result)
}
@Test
fun `toSendType with FileSend should return File SendType with correct data`() {
val uri = mockk<Uri>()
val fileName = "Share Name"
val sizeBytes = 100L
val expected = AddSendState.ViewState.Content.SendType.File(
uri = uri,
name = fileName,
sizeBytes = sizeBytes,
displaySize = null,
)
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
data = IntentManager.ShareData.FileSend(
fileData = IntentManager.FileData(
fileName = fileName,
uri = uri,
sizeBytes = sizeBytes,
),
),
shouldFinishWhenComplete = false,
)
val result = specialCircumstance.toSendType()
assertEquals(expected, result)
}
@Test
fun `toSendType with null SpecialCircumstance should return null`() {
val specialCircumstance: UserState.SpecialCircumstance? = null
assertNull(specialCircumstance.toSendType())
}
@Test
fun `toSendName with TextSend should return subject`() {
val subject = "Subject"
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
data = IntentManager.ShareData.TextSend(
subject = subject,
text = "",
),
shouldFinishWhenComplete = false,
)
val result = specialCircumstance.toSendName()
assertEquals(subject, result)
}
@Test
fun `toSendName with FileSend should return file name`() {
val fileName = "File Name"
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
data = IntentManager.ShareData.FileSend(
fileData = IntentManager.FileData(
fileName = fileName,
uri = mockk(),
sizeBytes = 0L,
),
),
shouldFinishWhenComplete = false,
)
val result = specialCircumstance.toSendName()
assertEquals(fileName, result)
}
@Test
fun `toSendName with null SpecialCircumstance should return null`() {
val specialCircumstance: UserState.SpecialCircumstance? = null
assertNull(specialCircumstance.toSendName())
}
@Suppress("MaxLineLength")
@Test
fun `shouldFinishOnComplete with ShareNewSend shouldFinishWhenComplete true should return true`() {
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
data = mockk(),
shouldFinishWhenComplete = true,
)
assertTrue(specialCircumstance.shouldFinishOnComplete())
}
@Suppress("MaxLineLength")
@Test
fun `shouldFinishOnComplete with ShareNewSend shouldFinishWhenComplete false should return false`() {
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
data = mockk(),
shouldFinishWhenComplete = false,
)
assertFalse(specialCircumstance.shouldFinishOnComplete())
}
@Test
fun `shouldFinishOnComplete with null SpecialCircumstance should return false`() {
val specialCircumstance: UserState.SpecialCircumstance? = null
assertFalse(specialCircumstance.shouldFinishOnComplete())
}
}

View file

@ -22,7 +22,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed
import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
@ -61,7 +61,7 @@ class VaultScreenTest : BaseComposeTest() {
private var onNavigateToVaultItemListingType: VaultItemListingType? = null
private var onDimBottomNavBarRequestCalled = false
private var onNavigateToVerificationCodeScreen = false
private val intentManager = mockk<IntentManager>(relaxed = true)
private val exitManager = mockk<ExitManager>(relaxed = true)
private val mutableEventFlow = bufferedMutableSharedFlow<VaultEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
@ -81,7 +81,7 @@ class VaultScreenTest : BaseComposeTest() {
onNavigateToVaultItemListingScreen = { onNavigateToVaultItemListingType = it },
onDimBottomNavBarRequest = { onDimBottomNavBarRequestCalled = true },
onNavigateToVerificationCodeScreen = { onNavigateToVerificationCodeScreen = true },
intentManager = intentManager,
exitManager = exitManager,
)
}
}
@ -646,9 +646,9 @@ class VaultScreenTest : BaseComposeTest() {
}
@Test
fun `NavigateOutOfApp event should call exitApplication on the IntentManager`() {
fun `NavigateOutOfApp event should call exitApplication on the ExitManager`() {
mutableEventFlow.tryEmit(VaultEvent.NavigateOutOfApp)
verify { intentManager.exitApplication() }
verify { exitManager.exitApplication() }
}
@Test