Replace usages of compose ClipboardManager in UI with BitwardenClipboardManager in ViewModels (#502)

This commit is contained in:
David Perez 2024-01-05 16:19:56 -06:00 committed by Álison Fernandes
parent 8d5de22c72
commit 889855d261
25 changed files with 316 additions and 392 deletions

View file

@ -0,0 +1,42 @@
package com.x8bit.bitwarden.data.platform.manager.clipboard
import androidx.compose.ui.text.AnnotatedString
import com.x8bit.bitwarden.ui.platform.base.util.Text
/**
* Wrapper class for using the clipboard.
*/
interface BitwardenClipboardManager {
/**
* Places the given [text] into the device's clipboard. Setting the data to [isSensitive] will
* obfuscate the displayed data on the default popup (true by default). A toast will be
* displayed on devices that do not have a default popup (pre-API 32) and will not be displayed
* on newer APIs. If a toast is displayed, it will be formatted as "[text] copied" or if a
* [toastDescriptorOverride] is provided, it will be formatted as
* "[toastDescriptorOverride] copied".
*/
fun setText(
text: AnnotatedString,
isSensitive: Boolean = true,
toastDescriptorOverride: String? = null,
)
/**
* See [setText] for more details.
*/
fun setText(
text: String,
isSensitive: Boolean = true,
toastDescriptorOverride: String? = null,
)
/**
* See [setText] for more details.
*/
fun setText(
text: Text,
isSensitive: Boolean = true,
toastDescriptorOverride: String? = null,
)
}

View file

@ -0,0 +1,58 @@
package com.x8bit.bitwarden.data.platform.manager.clipboard
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.widget.Toast
import androidx.compose.ui.text.AnnotatedString
import androidx.core.content.getSystemService
import androidx.core.os.persistableBundleOf
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
/**
* Default implementation of the [BitwardenClipboardManager] interface.
*/
@OmitFromCoverage
class BitwardenClipboardManagerImpl(
private val context: Context,
) : BitwardenClipboardManager {
private val clipboardManager: ClipboardManager = requireNotNull(context.getSystemService())
override fun setText(
text: AnnotatedString,
isSensitive: Boolean,
toastDescriptorOverride: String?,
) {
clipboardManager.setPrimaryClip(
ClipData
.newPlainText("", text)
.apply {
description.extras = persistableBundleOf(
"android.content.extra.IS_SENSITIVE" to isSensitive,
)
},
)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
val descriptor = toastDescriptorOverride ?: text
Toast
.makeText(
context,
context.resources.getString(R.string.value_has_been_copied, descriptor),
Toast.LENGTH_SHORT,
)
.show()
}
}
override fun setText(text: String, isSensitive: Boolean, toastDescriptorOverride: String?) {
setText(text.toAnnotatedString(), isSensitive, toastDescriptorOverride)
}
override fun setText(text: Text, isSensitive: Boolean, toastDescriptorOverride: String?) {
setText(text.toString(context.resources), isSensitive, toastDescriptorOverride)
}
}

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.manager.di
import android.content.Context
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
@ -8,12 +9,15 @@ import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManagerImpl
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
import com.x8bit.bitwarden.data.platform.manager.SdkClientManagerImpl
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManagerImpl
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManagerImpl
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@ -24,6 +28,12 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
object PlatformManagerModule {
@Provides
@Singleton
fun provideBitwardenClipboardManager(
@ApplicationContext context: Context,
): BitwardenClipboardManager = BitwardenClipboardManagerImpl(context)
@Provides
@Singleton
fun provideBitwardenDispatchers(): DispatcherManager = DispatcherManagerImpl()

View file

@ -29,8 +29,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.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
@ -46,7 +44,6 @@ 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.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
import com.x8bit.bitwarden.ui.platform.components.BitwardenExternalLinkRow
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
@ -62,7 +59,6 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
fun AboutScreen(
onNavigateBack: () -> Unit,
viewModel: AboutViewModel = hiltViewModel(),
clipboardManager: ClipboardManager = LocalClipboardManager.current,
intentHandler: IntentHandler = IntentHandler(context = LocalContext.current),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@ -70,10 +66,6 @@ fun AboutScreen(
val resources = context.resources
EventsEffect(viewModel = viewModel) { event ->
when (event) {
is AboutEvent.CopyToClipboard -> {
clipboardManager.setText(event.text.toString(resources).toAnnotatedString())
}
AboutEvent.NavigateBack -> onNavigateBack.invoke()
AboutEvent.NavigateToHelpCenter -> {

View file

@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.BuildConfig
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
@ -24,6 +25,7 @@ private const val KEY_STATE = "state"
@HiltViewModel
class AboutViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val clipboardManager: BitwardenClipboardManager,
) : BaseViewModel<AboutState, AboutEvent, AboutAction>(
initialState = savedStateHandle[KEY_STATE] ?: INITIAL_STATE,
) {
@ -67,7 +69,7 @@ class AboutViewModel @Inject constructor(
}
private fun handleVersionClick() {
sendEvent(AboutEvent.CopyToClipboard(text = stateFlow.value.version))
clipboardManager.setText(text = state.version)
}
private fun handleWebVaultClick() {
@ -97,13 +99,6 @@ data class AboutState(
* Models events for the about screen.
*/
sealed class AboutEvent {
/**
* Copy the given [text] to the clipboard.
*/
data class CopyToClipboard(
val text: Text,
) : AboutEvent()
/**
* Navigate back.
*/

View file

@ -33,14 +33,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
@ -87,7 +84,6 @@ import kotlinx.collections.immutable.toImmutableList
fun GeneratorScreen(
viewModel: GeneratorViewModel = hiltViewModel(),
onNavigateToPasswordHistory: () -> Unit,
clipboardManager: ClipboardManager = LocalClipboardManager.current,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current
@ -98,10 +94,6 @@ fun GeneratorScreen(
when (event) {
GeneratorEvent.NavigateToPasswordHistory -> onNavigateToPasswordHistory()
GeneratorEvent.CopyTextToClipboard -> {
clipboardManager.setText(AnnotatedString(state.generatedText))
}
is GeneratorEvent.ShowSnackbar -> {
snackbarHostState.showSnackbar(
message = event.message(resources).toString(),

View file

@ -11,6 +11,7 @@ import com.bitwarden.core.PasswordGeneratorRequest
import com.bitwarden.core.UsernameGeneratorRequest
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedForwardedServiceUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassphraseResult
@ -59,6 +60,7 @@ private const val KEY_STATE = "state"
@HiltViewModel
class GeneratorViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val clipboardManager: BitwardenClipboardManager,
private val generatorRepository: GeneratorRepository,
private val authRepository: AuthRepository,
) : BaseViewModel<GeneratorState, GeneratorEvent, GeneratorAction>(
@ -328,7 +330,7 @@ class GeneratorViewModel @Inject constructor(
}
private fun handleCopyClick() {
sendEvent(GeneratorEvent.CopyTextToClipboard)
clipboardManager.setText(text = state.generatedText)
}
private fun handleUpdateGeneratedPasswordResult(
@ -1949,11 +1951,6 @@ sealed class GeneratorEvent {
*/
data object NavigateToPasswordHistory : GeneratorEvent()
/**
* Copies text to the clipboard.
*/
data object CopyTextToClipboard : GeneratorEvent()
/**
* Displays the message in a snackbar.
*/

View file

@ -25,8 +25,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.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
@ -35,7 +33,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
@ -51,7 +48,6 @@ import kotlinx.collections.immutable.persistentListOf
fun PasswordHistoryScreen(
onNavigateBack: () -> Unit,
viewModel: PasswordHistoryViewModel = hiltViewModel(),
clipboardManager: ClipboardManager = LocalClipboardManager.current,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current
@ -61,10 +57,6 @@ fun PasswordHistoryScreen(
when (event) {
PasswordHistoryEvent.NavigateBack -> onNavigateBack.invoke()
is PasswordHistoryEvent.CopyTextToClipboard -> {
clipboardManager.setText(event.text.toAnnotatedString())
}
is PasswordHistoryEvent.ShowToast -> {
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
}

View file

@ -4,6 +4,7 @@ import android.os.Parcelable
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.PasswordHistoryView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
@ -26,6 +27,7 @@ import javax.inject.Inject
@HiltViewModel
@Suppress("TooManyFunctions")
class PasswordHistoryViewModel @Inject constructor(
private val clipboardManager: BitwardenClipboardManager,
private val generatorRepository: GeneratorRepository,
) : BaseViewModel<PasswordHistoryState, PasswordHistoryEvent, PasswordHistoryAction>(
initialState = PasswordHistoryState(PasswordHistoryState.ViewState.Loading),
@ -96,7 +98,7 @@ class PasswordHistoryViewModel @Inject constructor(
}
private fun handleCopyClick(password: GeneratedPassword) {
sendEvent(PasswordHistoryEvent.CopyTextToClipboard(password.password))
clipboardManager.setText(text = password.password)
}
}
@ -174,11 +176,6 @@ sealed class PasswordHistoryEvent {
* Event to navigate back to the previous screen.
*/
data object NavigateBack : PasswordHistoryEvent()
/**
* Copies text to the clipboard.
*/
data class CopyTextToClipboard(val text: String) : PasswordHistoryEvent()
}
/**

View file

@ -18,8 +18,6 @@ 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
@ -29,7 +27,6 @@ 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
@ -49,19 +46,12 @@ import kotlinx.collections.immutable.persistentListOf
fun SendScreen(
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.CopyToClipboard -> {
clipboardManager.setText(
event.message(context.resources).toString().toAnnotatedString(),
)
}
is SendEvent.NavigateNewSend -> onNavigateToAddSend()
is SendEvent.NavigateToAboutSend -> {

View file

@ -5,6 +5,7 @@ import androidx.annotation.DrawableRes
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.SendData
@ -32,6 +33,7 @@ private const val KEY_STATE = "state"
@HiltViewModel
class SendViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val clipboardManager: BitwardenClipboardManager,
private val vaultRepo: VaultRepository,
) : BaseViewModel<SendState, SendEvent, SendAction>(
// We load the state from the savedStateHandle for testing purposes.
@ -324,11 +326,6 @@ 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.
*/

View file

@ -13,8 +13,6 @@ 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
@ -25,7 +23,6 @@ import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.PermissionsManager
import com.x8bit.bitwarden.ui.platform.base.util.PermissionsManagerImpl
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
@ -50,7 +47,6 @@ fun VaultAddEditScreen(
onNavigateBack: () -> Unit,
onNavigateToQrCodeScanScreen: () -> Unit,
viewModel: VaultAddEditViewModel = hiltViewModel(),
clipboardManager: ClipboardManager = LocalClipboardManager.current,
permissionsManager: PermissionsManager =
PermissionsManagerImpl(LocalContext.current as Activity),
) {
@ -68,10 +64,6 @@ fun VaultAddEditScreen(
Toast.makeText(context, event.message(resources), Toast.LENGTH_SHORT).show()
}
is VaultAddEditEvent.CopyToClipboard -> {
clipboardManager.setText(event.text.toAnnotatedString())
}
VaultAddEditEvent.NavigateBack -> onNavigateBack.invoke()
}
}

View file

@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.takeUntilLoaded
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
@ -48,6 +49,7 @@ private const val KEY_STATE = "state"
@Suppress("TooManyFunctions", "LargeClass")
class VaultAddEditViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val clipboardManager: BitwardenClipboardManager,
private val vaultRepository: VaultRepository,
) : BaseViewModel<VaultAddEditState, VaultAddEditEvent, VaultAddEditAction>(
// We load the state from the savedStateHandle for testing purposes.
@ -435,11 +437,7 @@ class VaultAddEditViewModel @Inject constructor(
private fun handleLoginCopyTotpKeyText(
action: VaultAddEditAction.ItemType.LoginType.CopyTotpKeyClick,
) {
sendEvent(
event = VaultAddEditEvent.CopyToClipboard(
text = action.totpKey,
),
)
clipboardManager.setText(text = action.totpKey)
}
private fun handleLoginUriSettingsClick() {
@ -1226,11 +1224,6 @@ sealed class VaultAddEditEvent {
*/
data class ShowToast(val message: Text) : VaultAddEditEvent()
/**
* Copy the given [text] to the clipboard.
*/
data class CopyToClipboard(val text: String) : VaultAddEditEvent()
/**
* Navigate back to previous screen.
*/

View file

@ -18,8 +18,6 @@ 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
@ -31,7 +29,6 @@ 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.asText
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
@ -53,7 +50,6 @@ import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHand
@Composable
fun VaultItemScreen(
viewModel: VaultItemViewModel = hiltViewModel(),
clipboardManager: ClipboardManager = LocalClipboardManager.current,
intentHandler: IntentHandler = IntentHandler(context = LocalContext.current),
onNavigateBack: () -> Unit,
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
@ -63,10 +59,6 @@ fun VaultItemScreen(
val resources = context.resources
EventsEffect(viewModel = viewModel) { event ->
when (event) {
is VaultItemEvent.CopyToClipboard -> {
clipboardManager.setText(event.message.toString(resources).toAnnotatedString())
}
VaultItemEvent.NavigateBack -> onNavigateBack()
is VaultItemEvent.NavigateToEdit -> onNavigateToVaultEditItem(event.itemId)

View file

@ -8,6 +8,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VerifyPasswordResult
@ -36,6 +37,7 @@ private const val KEY_STATE = "state"
@HiltViewModel
class VaultItemViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val clipboardManager: BitwardenClipboardManager,
private val authRepository: AuthRepository,
private val vaultRepository: VaultRepository,
) : BaseViewModel<VaultItemState, VaultItemEvent, VaultItemAction>(
@ -148,14 +150,14 @@ class VaultItemViewModel @Inject constructor(
}
return@onContent
}
sendEvent(VaultItemEvent.CopyToClipboard(action.field.asText()))
clipboardManager.setText(text = action.field)
}
}
private fun handleCopyCustomTextFieldClick(
action: VaultItemAction.Common.CopyCustomTextFieldClick,
) {
sendEvent(VaultItemEvent.CopyToClipboard(action.field.asText()))
clipboardManager.setText(text = action.field)
}
private fun handleHiddenFieldVisibilityClicked(
@ -244,12 +246,12 @@ class VaultItemViewModel @Inject constructor(
return@onLoginContent
}
val password = requireNotNull(login.passwordData?.password)
sendEvent(VaultItemEvent.CopyToClipboard(password.asText()))
clipboardManager.setText(text = password)
}
}
private fun handleCopyUriClick(action: VaultItemAction.ItemType.Login.CopyUriClick) {
sendEvent(VaultItemEvent.CopyToClipboard(action.uri.asText()))
clipboardManager.setText(text = action.uri)
}
private fun handleCopyUsernameClick() {
@ -261,7 +263,7 @@ class VaultItemViewModel @Inject constructor(
return@onLoginContent
}
val username = requireNotNull(login.username)
sendEvent(VaultItemEvent.CopyToClipboard(username.asText()))
clipboardManager.setText(text = username)
}
}
@ -681,13 +683,6 @@ data class VaultItemState(
* Represents a set of events related view a vault item.
*/
sealed class VaultItemEvent {
/**
* Places the given [message] in your clipboard.
*/
data class CopyToClipboard(
val message: Text,
) : VaultItemEvent()
/**
* Navigates back.
*/

View file

@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.about
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn
@ -15,7 +14,6 @@ import androidx.core.net.toUri
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 io.mockk.Runs
import io.mockk.every
import io.mockk.just
@ -108,28 +106,6 @@ class AboutScreenTest : BaseComposeTest() {
}
}
@Test
fun `on CopyToClipboard should call setText on ClipboardManager`() {
val text = "copy text"
val clipboardManager = mockk<ClipboardManager> {
every { setText(any()) } just Runs
}
val viewModel = mockk<AboutViewModel> {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns flowOf(AboutEvent.CopyToClipboard(text.asText()))
}
composeTestRule.setContent {
AboutScreen(
viewModel = viewModel,
onNavigateBack = { },
clipboardManager = clipboardManager,
)
}
verify {
clipboardManager.setText(text.toAnnotatedString())
}
}
@Suppress("MaxLineLength")
@Test
fun `on learn about organizations click should display confirmation dialog and confirm click should emit LearnAboutOrganizationsClick`() {

View file

@ -2,8 +2,14 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.about
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
@ -12,12 +18,11 @@ import org.junit.jupiter.api.Test
class AboutViewModelTest : BaseViewModelTest() {
private val initialState = createAboutState()
private val initialSavedStateHandle = createSavedStateHandleWithState(initialState)
private val clipboardManager: BitwardenClipboardManager = mockk()
@Test
fun `on BackClick should emit NavigateBack`() = runTest {
val viewModel = AboutViewModel(initialSavedStateHandle)
val viewModel = createViewModel(DEFAULT_ABOUT_STATE)
viewModel.eventFlow.test {
viewModel.trySendAction(AboutAction.BackClick)
assertEquals(AboutEvent.NavigateBack, awaitItem())
@ -26,7 +31,7 @@ class AboutViewModelTest : BaseViewModelTest() {
@Test
fun `on HelpCenterClick should emit NavigateToHelpCenter`() = runTest {
val viewModel = AboutViewModel(initialSavedStateHandle)
val viewModel = createViewModel(DEFAULT_ABOUT_STATE)
viewModel.eventFlow.test {
viewModel.trySendAction(AboutAction.HelpCenterClick)
assertEquals(AboutEvent.NavigateToHelpCenter, awaitItem())
@ -36,7 +41,7 @@ class AboutViewModelTest : BaseViewModelTest() {
@Test
fun `on LearnAboutOrganizationsClick should emit NavigateToLearnAboutOrganizations`() =
runTest {
val viewModel = AboutViewModel(initialSavedStateHandle)
val viewModel = createViewModel(DEFAULT_ABOUT_STATE)
viewModel.eventFlow.test {
viewModel.trySendAction(AboutAction.LearnAboutOrganizationsClick)
assertEquals(AboutEvent.NavigateToLearnAboutOrganizations, awaitItem())
@ -45,7 +50,7 @@ class AboutViewModelTest : BaseViewModelTest() {
@Test
fun `on RateAppClick should emit ShowToast`() = runTest {
val viewModel = AboutViewModel(initialSavedStateHandle)
val viewModel = createViewModel(DEFAULT_ABOUT_STATE)
viewModel.eventFlow.test {
viewModel.trySendAction(AboutAction.RateAppClick)
assertEquals(AboutEvent.ShowToast("Navigate to rate the app.".asText()), awaitItem())
@ -54,38 +59,42 @@ class AboutViewModelTest : BaseViewModelTest() {
@Test
fun `on SubmitCrashLogsClick should update isSubmitCrashLogsEnabled to true`() = runTest {
val viewModel = AboutViewModel(initialSavedStateHandle)
val viewModel = createViewModel(DEFAULT_ABOUT_STATE)
assertFalse(viewModel.stateFlow.value.isSubmitCrashLogsEnabled)
viewModel.trySendAction(AboutAction.SubmitCrashLogsClick(true))
assertTrue(viewModel.stateFlow.value.isSubmitCrashLogsEnabled)
}
@Test
fun `on VersionClick should emit CopyToClipboard`() = runTest {
val viewModel = AboutViewModel(initialSavedStateHandle)
viewModel.eventFlow.test {
viewModel.trySendAction(AboutAction.VersionClick)
assertEquals(AboutEvent.CopyToClipboard("0".asText()), awaitItem())
fun `on VersionClick should call setText on the ClipboardManager`() {
val viewModel = createViewModel(DEFAULT_ABOUT_STATE)
every { clipboardManager.setText(text = "0".asText()) } just runs
viewModel.trySendAction(AboutAction.VersionClick)
verify(exactly = 1) {
clipboardManager.setText(text = "0".asText())
}
}
@Test
fun `on WebVaultClick should emit NavigateToWebVault`() = runTest {
val viewModel = AboutViewModel(initialSavedStateHandle)
val viewModel = createViewModel(DEFAULT_ABOUT_STATE)
viewModel.eventFlow.test {
viewModel.trySendAction(AboutAction.WebVaultClick)
assertEquals(AboutEvent.NavigateToWebVault, awaitItem())
}
}
private fun createAboutState(): AboutState =
AboutState(
version = "0".asText(),
isSubmitCrashLogsEnabled = false,
)
private fun createSavedStateHandleWithState(state: AboutState) =
SavedStateHandle().apply {
set("state", state)
}
private fun createViewModel(
state: AboutState? = null,
): AboutViewModel = AboutViewModel(
savedStateHandle = SavedStateHandle().apply { set("state", state) },
clipboardManager = clipboardManager,
)
}
private val DEFAULT_ABOUT_STATE: AboutState = AboutState(
version = "0".asText(),
isSubmitCrashLogsEnabled = false,
)

View file

@ -5,6 +5,7 @@ import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedForwardedServiceUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassphraseResult
@ -14,7 +15,10 @@ import com.x8bit.bitwarden.data.tools.generator.repository.util.FakeGeneratorRep
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
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.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
@ -65,6 +69,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
every { userStateFlow } returns mutableUserStateFlow
}
private val clipboardManager: BitwardenClipboardManager = mockk()
private val fakeGeneratorRepository = FakeGeneratorRepository().apply {
setMockGeneratePasswordResult(
GeneratedPasswordResult.Success("defaultPassword"),
@ -209,11 +214,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
@Test
fun `RegenerateClick for plus addressed email state should update the plus addressed email correctly`() =
runTest {
val viewModel = GeneratorViewModel(
usernameSavedStateHandle,
fakeGeneratorRepository,
authRepository,
)
val viewModel = createViewModel(usernameSavedStateHandle)
fakeGeneratorRepository.setMockGeneratePasswordResult(
GeneratedPasswordResult.Success("DifferentUsername"),
@ -235,13 +236,14 @@ class GeneratorViewModelTest : BaseViewModelTest() {
}
@Test
fun `CopyClick should emit CopyTextToClipboard event`() = runTest {
fun `CopyClick should call setText on ClipboardManager`() {
val viewModel = createViewModel()
every { clipboardManager.setText(viewModel.stateFlow.value.generatedText) } just runs
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(GeneratorAction.CopyClick)
viewModel.actionChannel.trySend(GeneratorAction.CopyClick)
assertEquals(GeneratorEvent.CopyTextToClipboard, awaitItem())
verify(exactly = 1) {
clipboardManager.setText(text = viewModel.stateFlow.value.generatedText)
}
}
@ -451,11 +453,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
fakeGeneratorRepository.setMockGeneratePasswordResult(
GeneratedPasswordResult.Success("defaultPassword"),
)
viewModel = GeneratorViewModel(
initialPasscodeSavedStateHandle,
fakeGeneratorRepository,
authRepository,
)
viewModel = createViewModel(initialPasscodeSavedStateHandle)
}
@Suppress("MaxLineLength")
@ -828,11 +826,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
fakeGeneratorRepository.setMockGeneratePasswordResult(
GeneratedPasswordResult.Success("defaultPassphrase"),
)
viewModel = GeneratorViewModel(
passphraseSavedStateHandle,
fakeGeneratorRepository,
authRepository,
)
viewModel = createViewModel(passphraseSavedStateHandle)
}
@Test
@ -969,12 +963,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
@BeforeEach
fun setup() {
viewModel =
GeneratorViewModel(
forwardedEmailAliasSavedStateHandle,
fakeGeneratorRepository,
authRepository,
)
viewModel = createViewModel(forwardedEmailAliasSavedStateHandle)
}
@Test
@ -1031,11 +1020,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
@BeforeEach
fun setup() {
viewModel = GeneratorViewModel(
addyIoSavedStateHandle,
fakeGeneratorRepository,
authRepository,
)
viewModel = createViewModel(addyIoSavedStateHandle)
}
@Test
@ -1125,11 +1110,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
@BeforeEach
fun setup() {
viewModel = GeneratorViewModel(
duckDuckGoSavedStateHandle,
fakeGeneratorRepository,
authRepository,
)
viewModel = createViewModel(duckDuckGoSavedStateHandle)
}
@Test
@ -1179,11 +1160,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
@BeforeEach
fun setup() {
viewModel = GeneratorViewModel(
fastMailSavedStateHandle,
fakeGeneratorRepository,
authRepository,
)
viewModel = createViewModel(fastMailSavedStateHandle)
}
@Test
@ -1233,11 +1210,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
@BeforeEach
fun setup() {
viewModel = GeneratorViewModel(
firefoxRelaySavedStateHandle,
fakeGeneratorRepository,
authRepository,
)
viewModel = createViewModel(firefoxRelaySavedStateHandle)
}
@Test
@ -1288,11 +1261,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
@BeforeEach
fun setup() {
viewModel = GeneratorViewModel(
simpleLoginSavedStateHandle,
fakeGeneratorRepository,
authRepository,
)
viewModel = createViewModel(simpleLoginSavedStateHandle)
}
@Test
@ -1343,11 +1312,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
@BeforeEach
fun setup() {
viewModel = GeneratorViewModel(
usernameSavedStateHandle,
fakeGeneratorRepository,
authRepository,
)
viewModel = createViewModel(usernameSavedStateHandle)
}
@Suppress("MaxLineLength")
@ -1390,11 +1355,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
@BeforeEach
fun setup() {
viewModel = GeneratorViewModel(
catchAllEmailSavedStateHandle,
fakeGeneratorRepository,
authRepository,
)
viewModel = createViewModel(catchAllEmailSavedStateHandle)
}
@Suppress("MaxLineLength")
@ -1436,11 +1397,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
@BeforeEach
fun setup() {
viewModel = GeneratorViewModel(
randomWordSavedStateHandle,
fakeGeneratorRepository,
authRepository,
)
viewModel = createViewModel(randomWordSavedStateHandle)
}
@Suppress("MaxLineLength")
@ -1704,13 +1661,20 @@ class GeneratorViewModelTest : BaseViewModelTest() {
}
private fun createViewModel(
state: GeneratorState? = initialPasscodeState,
savedStateHandle: SavedStateHandle,
): GeneratorViewModel = GeneratorViewModel(
savedStateHandle = SavedStateHandle().apply { set("state", state) },
savedStateHandle = savedStateHandle,
clipboardManager = clipboardManager,
generatorRepository = fakeGeneratorRepository,
authRepository = authRepository,
)
private fun createViewModel(
state: GeneratorState? = initialPasscodeState,
): GeneratorViewModel = createViewModel(
savedStateHandle = SavedStateHandle().apply { set("state", state) },
)
//endregion Helper Functions
}

View file

@ -3,11 +3,17 @@ package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
import app.cash.turbine.test
import com.bitwarden.core.PasswordHistoryView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
import com.x8bit.bitwarden.data.tools.generator.repository.util.FakeGeneratorRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.tools.feature.generator.util.toFormattedPattern
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
@ -18,6 +24,9 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() {
private val initialState = PasswordHistoryState(PasswordHistoryState.ViewState.Loading)
private val clipboardManager: BitwardenClipboardManager = mockk()
private val fakeGeneratorRepository = FakeGeneratorRepository()
@Test
fun `initial state should be correct`() = runTest {
val viewModel = createViewModel()
@ -28,10 +37,8 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() {
@Test
fun `when repository emits Loading state the state updates correctly`() = runTest {
val fakeRepository = FakeGeneratorRepository().apply {
emitPasswordHistoryState(LocalDataState.Loading)
}
val viewModel = PasswordHistoryViewModel(generatorRepository = fakeRepository)
fakeGeneratorRepository.emitPasswordHistoryState(LocalDataState.Loading)
val viewModel = createViewModel()
viewModel.stateFlow.test {
val expectedState = PasswordHistoryState(PasswordHistoryState.ViewState.Loading)
@ -42,10 +49,10 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() {
@Test
fun `when repository emits Error state the state updates correctly`() = runTest {
val fakeRepository = FakeGeneratorRepository().apply {
emitPasswordHistoryState(LocalDataState.Error(Exception("An error has occurred.")))
}
val viewModel = PasswordHistoryViewModel(generatorRepository = fakeRepository)
fakeGeneratorRepository.emitPasswordHistoryState(
state = LocalDataState.Error(Exception("An error has occurred.")),
)
val viewModel = createViewModel()
viewModel.stateFlow.test {
val expectedState = PasswordHistoryState(
@ -58,10 +65,8 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() {
@Test
fun `when repository emits Empty state the state updates correctly`() = runTest {
val fakeRepository = FakeGeneratorRepository().apply {
emitPasswordHistoryState(LocalDataState.Loaded(emptyList()))
}
val viewModel = PasswordHistoryViewModel(generatorRepository = fakeRepository)
fakeGeneratorRepository.emitPasswordHistoryState(LocalDataState.Loaded(emptyList()))
val viewModel = createViewModel()
viewModel.stateFlow.test {
val expectedState = PasswordHistoryState(PasswordHistoryState.ViewState.Empty)
@ -72,11 +77,10 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() {
@Test
fun `when password history updates the state updates correctly`() = runTest {
val fakeRepository = FakeGeneratorRepository()
val viewModel = PasswordHistoryViewModel(generatorRepository = fakeRepository)
val viewModel = createViewModel()
val passwordHistoryView = PasswordHistoryView("password", Instant.now())
fakeRepository.storePasswordHistory(passwordHistoryView)
fakeGeneratorRepository.storePasswordHistory(passwordHistoryView)
val expectedState = PasswordHistoryState(
viewState = PasswordHistoryState.ViewState.Content(
@ -108,45 +112,44 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() {
}
@Test
fun `PasswordCopyClick action should emit CopyTextToClipboard event`() = runTest {
fun `PasswordCopyClick action should call setText on the ClipboardManager`() {
val viewModel = createViewModel()
val generatedPassword = PasswordHistoryState.GeneratedPassword(
password = "testPassword",
date = "01/01/23",
)
every { clipboardManager.setText(text = generatedPassword.password) } just runs
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(
PasswordHistoryAction.PasswordCopyClick(generatedPassword),
)
assertEquals(
PasswordHistoryEvent.CopyTextToClipboard(generatedPassword.password),
awaitItem(),
)
viewModel.actionChannel.trySend(PasswordHistoryAction.PasswordCopyClick(generatedPassword))
verify(exactly = 1) {
clipboardManager.setText(text = generatedPassword.password)
}
}
@Test
fun `PasswordClearClick action should update to Empty ViewState`() = runTest {
val fakeRepository = FakeGeneratorRepository()
val viewModel = PasswordHistoryViewModel(generatorRepository = fakeRepository)
val viewModel = createViewModel()
val passwordHistoryView = PasswordHistoryView("password", Instant.now())
fakeRepository.storePasswordHistory(passwordHistoryView)
fakeGeneratorRepository.storePasswordHistory(passwordHistoryView)
viewModel.actionChannel.trySend(PasswordHistoryAction.PasswordClearClick)
assertTrue(fakeRepository.passwordHistoryStateFlow.value is LocalDataState.Loaded)
assertTrue(fakeGeneratorRepository.passwordHistoryStateFlow.value is LocalDataState.Loaded)
assertTrue(
(fakeRepository.passwordHistoryStateFlow.value as LocalDataState.Loaded).data.isEmpty(),
(fakeGeneratorRepository.passwordHistoryStateFlow.value as LocalDataState.Loaded)
.data
.isEmpty(),
)
}
//region Helper Functions
private fun createViewModel(): PasswordHistoryViewModel {
return PasswordHistoryViewModel(generatorRepository = FakeGeneratorRepository())
}
private fun createViewModel(): PasswordHistoryViewModel = PasswordHistoryViewModel(
clipboardManager = clipboardManager,
generatorRepository = fakeGeneratorRepository,
)
//endregion Helper Functions
}

View file

@ -1,6 +1,5 @@
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
@ -23,7 +22,6 @@ 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.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
@ -41,9 +39,6 @@ class SendScreenTest : BaseComposeTest() {
private var onNavigateToNewSendCalled = false
private val clipboardManager = mockk<ClipboardManager> {
every { setText(any()) } just runs
}
private val intentHandler = mockk<IntentHandler> {
every { launchUri(any()) } just runs
}
@ -60,21 +55,11 @@ class SendScreenTest : BaseComposeTest() {
SendScreen(
viewModel = viewModel,
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)

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.tools.feature.send
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.SendData
@ -27,6 +28,8 @@ import org.junit.jupiter.api.Test
class SendViewModelTest : BaseViewModelTest() {
private val mutableSendDataFlow = MutableStateFlow<DataState<SendData>>(DataState.Loading)
private val clipboardManager: BitwardenClipboardManager = mockk()
private val vaultRepo: VaultRepository = mockk {
every { sendDataStateFlow } returns mutableSendDataFlow
}
@ -232,11 +235,13 @@ class SendViewModelTest : BaseViewModelTest() {
private fun createViewModel(
state: SendState? = null,
bitwardenClipboardManager: BitwardenClipboardManager = clipboardManager,
vaultRepository: VaultRepository = vaultRepo,
): SendViewModel = SendViewModel(
savedStateHandle = SavedStateHandle().apply {
set("state", state)
},
clipboardManager = bitwardenClipboardManager,
vaultRepo = vaultRepository,
)
}

View file

@ -1,7 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotDisplayed
@ -32,7 +31,6 @@ 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.FakePermissionManager
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
import com.x8bit.bitwarden.ui.util.isProgressBar
import com.x8bit.bitwarden.ui.util.onAllNodesWithTextAfterScroll
import com.x8bit.bitwarden.ui.util.onNodeWithContentDescriptionAfterScroll
@ -43,9 +41,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle
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
@ -59,8 +55,6 @@ class VaultAddEditScreenTest : BaseComposeTest() {
private var onNavigateBackCalled = false
private var onNavigateQrCodeScanScreenCalled = false
private val clipboardManager = mockk<ClipboardManager>()
private val mutableEventFlow = bufferedMutableSharedFlow<VaultAddEditEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE_LOGIN)
@ -78,7 +72,6 @@ class VaultAddEditScreenTest : BaseComposeTest() {
viewModel = viewModel,
onNavigateBack = { onNavigateBackCalled = true },
permissionsManager = fakePermissionManager,
clipboardManager = clipboardManager,
onNavigateToQrCodeScanScreen = {
onNavigateQrCodeScanScreenCalled = true
},
@ -99,19 +92,6 @@ class VaultAddEditScreenTest : BaseComposeTest() {
assertTrue(onNavigateQrCodeScanScreenCalled)
}
@Test
fun `on CopyToClipboard should call setText on ClipboardManager`() {
val textString = "text"
every { clipboardManager.setText(textString.toAnnotatedString()) } just runs
mutableEventFlow.tryEmit(VaultAddEditEvent.CopyToClipboard(textString))
verify(exactly = 1) {
clipboardManager.setText(textString.toAnnotatedString())
}
}
@Test
fun `clicking close button should send CloseClick action`() {
composeTestRule

View file

@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.core.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
@ -23,8 +24,10 @@ import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableSharedFlow
@ -50,6 +53,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
private val totpTestCodeFlow: MutableSharedFlow<String> = bufferedMutableSharedFlow()
private val mutableVaultItemFlow = MutableStateFlow<DataState<CipherView?>>(DataState.Loading)
private val clipboardManager: BitwardenClipboardManager = mockk()
private val vaultRepository: VaultRepository = mockk {
every { getVaultItemStateFlow(DEFAULT_EDIT_ITEM_ID) } returns mutableVaultItemFlow
every { totpCodeFlow } returns totpTestCodeFlow
@ -570,23 +575,20 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `CopyTotpKeyClick should emit a toast and CopyToClipboard`() = runTest {
fun `CopyTotpKeyClick should call setText on ClipboardManager`() {
val viewModel = createAddVaultItemViewModel()
val testKey = "TestKey"
every { clipboardManager.setText(text = testKey) } just runs
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(
VaultAddEditAction.ItemType.LoginType.CopyTotpKeyClick(
testKey,
),
)
viewModel.actionChannel.trySend(
VaultAddEditAction.ItemType.LoginType.CopyTotpKeyClick(
testKey,
),
)
assertEquals(
VaultAddEditEvent.CopyToClipboard(testKey),
awaitItem(),
)
verify(exactly = 1) {
clipboardManager.setText(text = testKey)
}
}
@ -1068,6 +1070,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
)
viewModel = VaultAddEditViewModel(
savedStateHandle = secureNotesInitialSavedStateHandle,
clipboardManager = clipboardManager,
vaultRepository = vaultRepository,
)
}
@ -1368,10 +1371,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
private fun createAddVaultItemViewModel(
savedStateHandle: SavedStateHandle = loginInitialSavedStateHandle,
bitwardenClipboardManager: BitwardenClipboardManager = clipboardManager,
vaultRepo: VaultRepository = vaultRepository,
): VaultAddEditViewModel =
VaultAddEditViewModel(
savedStateHandle = savedStateHandle,
clipboardManager = bitwardenClipboardManager,
vaultRepository = vaultRepo,
)

View file

@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.vault.feature.item
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
@ -23,7 +22,6 @@ 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.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.assertScrollableNodeDoesNotExist
import com.x8bit.bitwarden.ui.util.isProgressBar
import com.x8bit.bitwarden.ui.util.onFirstNodeWithTextAfterScroll
@ -47,7 +45,6 @@ class VaultItemScreenTest : BaseComposeTest() {
private var onNavigateBackCalled = false
private var onNavigateToVaultEditItemId: String? = null
private val clipboardManager = mockk<ClipboardManager>()
private val intentHandler = mockk<IntentHandler>()
private val mutableEventFlow = bufferedMutableSharedFlow<VaultItemEvent>()
@ -64,7 +61,6 @@ class VaultItemScreenTest : BaseComposeTest() {
viewModel = viewModel,
onNavigateBack = { onNavigateBackCalled = true },
onNavigateToVaultEditItem = { onNavigateToVaultEditItemId = it },
clipboardManager = clipboardManager,
intentHandler = intentHandler,
)
}
@ -86,19 +82,6 @@ class VaultItemScreenTest : BaseComposeTest() {
}
}
@Test
fun `CopyToClipboard event should invoke setText`() {
val textString = "text"
val text = textString.asText()
every { clipboardManager.setText(textString.toAnnotatedString()) } just runs
mutableEventFlow.tryEmit(VaultItemEvent.CopyToClipboard(text))
verify(exactly = 1) {
clipboardManager.setText(textString.toAnnotatedString())
}
}
@Test
fun `NavigateBack event should invoke onNavigateBack`() {
mutableEventFlow.tryEmit(VaultItemEvent.NavigateBack)

View file

@ -7,6 +7,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
@ -38,6 +39,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
private val mutableVaultItemFlow = MutableStateFlow<DataState<CipherView?>>(DataState.Loading)
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
private val clipboardManager: BitwardenClipboardManager = mockk()
private val authRepo: AuthRepository = mockk {
every { userStateFlow } returns mutableUserStateFlow
}
@ -216,40 +218,33 @@ class VaultItemViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `on CopyCustomHiddenFieldClick should emit CopyToClipboard when re-prompt is not required`() =
runTest {
val field = "field"
val mockCipherView = mockk<CipherView> {
every {
toViewState(isPremiumUser = true)
}
.returns(
createViewState(
common = DEFAULT_COMMON.copy(requiresReprompt = false),
),
)
}
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
viewModel.eventFlow.test {
viewModel.trySendAction(VaultItemAction.Common.CopyCustomHiddenFieldClick(field))
assertEquals(
VaultItemEvent.CopyToClipboard(field.asText()),
awaitItem(),
)
}
verify(exactly = 1) {
mockCipherView.toViewState(isPremiumUser = true)
}
fun `on CopyCustomHiddenFieldClick should call setText on ClipboardManager when re-prompt is not required`() {
val field = "field"
val mockCipherView = mockk<CipherView> {
every {
toViewState(isPremiumUser = true)
} returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false))
}
every { clipboardManager.setText(text = field) } just runs
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
viewModel.trySendAction(VaultItemAction.Common.CopyCustomHiddenFieldClick(field))
verify(exactly = 1) {
clipboardManager.setText(text = field)
mockCipherView.toViewState(isPremiumUser = true)
}
}
@Test
fun `on CopyCustomTextFieldClick should emit CopyToClipboard`() = runTest {
fun `on CopyCustomTextFieldClick should call setText on ClipboardManager`() {
val field = "field"
viewModel.eventFlow.test {
viewModel.trySendAction(VaultItemAction.Common.CopyCustomTextFieldClick(field))
assertEquals(VaultItemEvent.CopyToClipboard(field.asText()), awaitItem())
every { clipboardManager.setText(text = field) } just runs
viewModel.trySendAction(VaultItemAction.Common.CopyCustomTextFieldClick(field))
verify(exactly = 1) {
clipboardManager.setText(text = field)
}
}
@ -402,41 +397,33 @@ class VaultItemViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `on CopyPasswordClick should emit CopyToClipboard when re-prompt is not required`() =
runTest {
val mockCipherView = mockk<CipherView> {
every {
toViewState(isPremiumUser = true)
}
.returns(
createViewState(
common = DEFAULT_COMMON.copy(requiresReprompt = false),
),
)
}
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
viewModel.eventFlow.test {
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyPasswordClick)
assertEquals(
VaultItemEvent.CopyToClipboard(DEFAULT_LOGIN_PASSWORD.asText()),
awaitItem(),
)
}
verify(exactly = 1) {
mockCipherView.toViewState(isPremiumUser = true)
}
fun `on CopyPasswordClick should call setText on the CLipboardManager when re-prompt is not required`() {
val mockCipherView = mockk<CipherView> {
every {
toViewState(isPremiumUser = true)
} returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false))
}
every { clipboardManager.setText(text = DEFAULT_LOGIN_PASSWORD) } just runs
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyPasswordClick)
verify(exactly = 1) {
clipboardManager.setText(text = DEFAULT_LOGIN_PASSWORD)
mockCipherView.toViewState(isPremiumUser = true)
}
}
@Test
fun `on CopyUriClick should emit CopyToClipboard`() = runTest {
fun `on CopyUriClick should call setText on ClipboardManager`() {
val uri = "uri"
viewModel.eventFlow.test {
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUriClick(uri))
assertEquals(VaultItemEvent.CopyToClipboard(uri.asText()), awaitItem())
}
every { clipboardManager.setText(text = uri) } just runs
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUriClick(uri))
verify(exactly = 1) { clipboardManager.setText(text = uri) }
}
@Test
@ -460,33 +447,24 @@ class VaultItemViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `on CopyUsernameClick should emit CopyToClipboard when re-prompt is not required`() =
runTest {
val mockCipherView = mockk<CipherView> {
every {
toViewState(isPremiumUser = true)
}
.returns(
createViewState(
common = DEFAULT_COMMON.copy(requiresReprompt = false),
),
)
}
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
viewModel.eventFlow.test {
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUsernameClick)
assertEquals(
VaultItemEvent.CopyToClipboard(DEFAULT_LOGIN_USERNAME.asText()),
awaitItem(),
)
}
verify(exactly = 1) {
mockCipherView.toViewState(isPremiumUser = true)
}
fun `on CopyUsernameClick should call setText on ClipboardManager when re-prompt is not required`() {
val mockCipherView = mockk<CipherView> {
every {
toViewState(isPremiumUser = true)
} returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false))
}
every { clipboardManager.setText(text = DEFAULT_LOGIN_USERNAME) } just runs
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUsernameClick)
verify(exactly = 1) {
clipboardManager.setText(text = DEFAULT_LOGIN_USERNAME)
mockCipherView.toViewState(isPremiumUser = true)
}
}
@Test
fun `on LaunchClick should emit NavigateToUri`() = runTest {
@ -613,6 +591,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
private fun createViewModel(
state: VaultItemState?,
vaultItemId: String = VAULT_ITEM_ID,
bitwardenClipboardManager: BitwardenClipboardManager = clipboardManager,
authRepository: AuthRepository = authRepo,
vaultRepository: VaultRepository = vaultRepo,
): VaultItemViewModel = VaultItemViewModel(
@ -620,6 +599,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
set("state", state)
set("vault_item_id", vaultItemId)
},
clipboardManager = bitwardenClipboardManager,
authRepository = authRepository,
vaultRepository = vaultRepository,
)