Provide IntentManager from compositionLocal (#630)

This commit is contained in:
David Perez 2024-01-15 21:27:07 -06:00 committed by Álison Fernandes
parent 7bf249c0dd
commit c52ae0ed2a
21 changed files with 227 additions and 537 deletions

View file

@ -67,7 +67,6 @@ import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.Ter
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.NavigateToPrivacyPolicy import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.NavigateToPrivacyPolicy
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.NavigateToTerms import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.NavigateToTerms
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect 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.asText
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
@ -79,6 +78,8 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.theme.clickableSpanStyle import com.x8bit.bitwarden.ui.platform.theme.clickableSpanStyle
/** /**
@ -90,7 +91,7 @@ import com.x8bit.bitwarden.ui.platform.theme.clickableSpanStyle
fun CreateAccountScreen( fun CreateAccountScreen(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToLogin: (emailAddress: String, captchaToken: String) -> Unit, onNavigateToLogin: (emailAddress: String, captchaToken: String) -> Unit,
intentHandler: IntentHandler = IntentHandler(context = LocalContext.current), intentManager: IntentManager = LocalIntentManager.current,
viewModel: CreateAccountViewModel = hiltViewModel(), viewModel: CreateAccountViewModel = hiltViewModel(),
) { ) {
val state by viewModel.stateFlow.collectAsState() val state by viewModel.stateFlow.collectAsState()
@ -98,11 +99,11 @@ fun CreateAccountScreen(
EventsEffect(viewModel) { event -> EventsEffect(viewModel) { event ->
when (event) { when (event) {
is NavigateToPrivacyPolicy -> { is NavigateToPrivacyPolicy -> {
intentHandler.launchUri("https://bitwarden.com/privacy/".toUri()) intentManager.launchUri("https://bitwarden.com/privacy/".toUri())
} }
is NavigateToTerms -> { is NavigateToTerms -> {
intentHandler.launchUri("https://bitwarden.com/terms/".toUri()) intentManager.launchUri("https://bitwarden.com/terms/".toUri())
} }
is CreateAccountEvent.NavigateBack -> onNavigateBack.invoke() is CreateAccountEvent.NavigateBack -> onNavigateBack.invoke()
@ -111,7 +112,7 @@ fun CreateAccountScreen(
} }
is CreateAccountEvent.NavigateToCaptcha -> { is CreateAccountEvent.NavigateToCaptcha -> {
intentHandler.startCustomTabsActivity(uri = event.uri) intentManager.startCustomTabsActivity(uri = event.uri)
} }
is CreateAccountEvent.NavigateToLogin -> { is CreateAccountEvent.NavigateToLogin -> {

View file

@ -39,7 +39,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenClickableText import com.x8bit.bitwarden.ui.platform.components.BitwardenClickableText
@ -52,6 +51,8 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenPlaceholderAccountAct
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -66,7 +67,7 @@ fun LoginScreen(
onNavigateToEnterpriseSignOn: () -> Unit, onNavigateToEnterpriseSignOn: () -> Unit,
onNavigateToLoginWithDevice: () -> Unit, onNavigateToLoginWithDevice: () -> Unit,
viewModel: LoginViewModel = hiltViewModel(), viewModel: LoginViewModel = hiltViewModel(),
intentHandler: IntentHandler = IntentHandler(context = LocalContext.current), intentManager: IntentManager = LocalIntentManager.current,
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
@ -74,7 +75,7 @@ fun LoginScreen(
when (event) { when (event) {
LoginEvent.NavigateBack -> onNavigateBack() LoginEvent.NavigateBack -> onNavigateBack()
is LoginEvent.NavigateToCaptcha -> { is LoginEvent.NavigateToCaptcha -> {
intentHandler.startCustomTabsActivity(uri = event.uri) intentManager.startCustomTabsActivity(uri = event.uri)
} }
LoginEvent.NavigateToEnterpriseSignOn -> onNavigateToEnterpriseSignOn() LoginEvent.NavigateToEnterpriseSignOn -> onNavigateToEnterpriseSignOn()

View file

@ -41,14 +41,15 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect 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.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.BitwardenExternalLinkRow import com.x8bit.bitwarden.ui.platform.components.BitwardenExternalLinkRow
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
/** /**
* Displays the about screen. * Displays the about screen.
@ -59,7 +60,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
fun AboutScreen( fun AboutScreen(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
viewModel: AboutViewModel = hiltViewModel(), viewModel: AboutViewModel = hiltViewModel(),
intentHandler: IntentHandler = IntentHandler(context = LocalContext.current), intentManager: IntentManager = LocalIntentManager.current,
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
@ -69,15 +70,15 @@ fun AboutScreen(
AboutEvent.NavigateBack -> onNavigateBack.invoke() AboutEvent.NavigateBack -> onNavigateBack.invoke()
AboutEvent.NavigateToHelpCenter -> { AboutEvent.NavigateToHelpCenter -> {
intentHandler.launchUri("https://bitwarden.com/help".toUri()) intentManager.launchUri("https://bitwarden.com/help".toUri())
} }
AboutEvent.NavigateToLearnAboutOrganizations -> { AboutEvent.NavigateToLearnAboutOrganizations -> {
intentHandler.launchUri("https://bitwarden.com/help/about-organizations".toUri()) intentManager.launchUri("https://bitwarden.com/help/about-organizations".toUri())
} }
AboutEvent.NavigateToWebVault -> { AboutEvent.NavigateToWebVault -> {
intentHandler.launchUri("https://vault.bitwarden.com".toUri()) intentManager.launchUri("https://vault.bitwarden.com".toUri())
} }
is AboutEvent.ShowToast -> { is AboutEvent.ShowToast -> {

View file

@ -34,7 +34,6 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect 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.Text
import com.x8bit.bitwarden.ui.platform.components.BitwardenExternalLinkRow import com.x8bit.bitwarden.ui.platform.components.BitwardenExternalLinkRow
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
@ -48,6 +47,8 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTimePickerDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTimePickerDialog
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
import com.x8bit.bitwarden.ui.platform.util.displayLabel import com.x8bit.bitwarden.ui.platform.util.displayLabel
@ -66,7 +67,7 @@ fun AccountSecurityScreen(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToDeleteAccount: () -> Unit, onNavigateToDeleteAccount: () -> Unit,
viewModel: AccountSecurityViewModel = hiltViewModel(), viewModel: AccountSecurityViewModel = hiltViewModel(),
intentHandler: IntentHandler = IntentHandler(context = LocalContext.current), intentManager: IntentManager = LocalIntentManager.current,
) { ) {
val state by viewModel.stateFlow.collectAsState() val state by viewModel.stateFlow.collectAsState()
val context = LocalContext.current val context = LocalContext.current
@ -78,7 +79,7 @@ fun AccountSecurityScreen(
AccountSecurityEvent.NavigateToDeleteAccount -> onNavigateToDeleteAccount() AccountSecurityEvent.NavigateToDeleteAccount -> onNavigateToDeleteAccount()
AccountSecurityEvent.NavigateToFingerprintPhrase -> { AccountSecurityEvent.NavigateToFingerprintPhrase -> {
intentHandler.launchUri("http://bitwarden.com/help/fingerprint-phrase".toUri()) intentManager.launchUri("http://bitwarden.com/help/fingerprint-phrase".toUri())
} }
is AccountSecurityEvent.ShowToast -> { is AccountSecurityEvent.ShowToast -> {

View file

@ -0,0 +1,35 @@
package com.x8bit.bitwarden.ui.platform.manager.intent
import android.content.Intent
import android.net.Uri
/**
* A manager class for simplifying the handling of Android Intents within a given context.
*/
interface IntentManager {
/**
* Starts an intent to exit the application.
*/
fun exitApplication()
/**
* Start an activity using the provided [Intent].
*/
fun startActivity(intent: Intent)
/**
* Start a Custom Tabs Activity using the provided [Uri].
*/
fun startCustomTabsActivity(uri: Uri)
/**
* Start an activity to view the given [uri] in an external browser.
*/
fun launchUri(uri: Uri)
/**
* Launches the share sheet with the given [text].
*/
fun shareText(text: String)
}

View file

@ -1,4 +1,4 @@
package com.x8bit.bitwarden.ui.platform.base.util package com.x8bit.bitwarden.ui.platform.manager.intent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -7,15 +7,15 @@ import androidx.browser.customtabs.CustomTabsIntent
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
/** /**
* A utility class for simplifying the handling of Android Intents within a given context. * The default implementation of the [IntentManager] for simplifying the handling of Android
* Intents within a given context.
*/ */
@OmitFromCoverage @OmitFromCoverage
class IntentHandler(private val context: Context) { class IntentManagerImpl(
private val context: Context,
) : IntentManager {
/** override fun exitApplication() {
* Starts an intent to exit the application.
*/
fun exitApplication() {
// Note that we fire an explicit Intent rather than try to cast to an Activity and call // 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. // finish to avoid assumptions about what kind of context we have.
val intent = Intent(Intent.ACTION_MAIN).apply { val intent = Intent(Intent.ACTION_MAIN).apply {
@ -24,27 +24,18 @@ class IntentHandler(private val context: Context) {
startActivity(intent) startActivity(intent)
} }
/** override fun startActivity(intent: Intent) {
* Start an activity using the provided [Intent].
*/
fun startActivity(intent: Intent) {
context.startActivity(intent) context.startActivity(intent)
} }
/** override fun startCustomTabsActivity(uri: Uri) {
* Start a Custom Tabs Activity using the provided [Uri].
*/
fun startCustomTabsActivity(uri: Uri) {
CustomTabsIntent CustomTabsIntent
.Builder() .Builder()
.build() .build()
.launchUrl(context, uri) .launchUrl(context, uri)
} }
/** override fun launchUri(uri: Uri) {
* Start an activity to view the given [uri] in an external browser.
*/
fun launchUri(uri: Uri) {
val newUri = if (uri.scheme == null) { val newUri = if (uri.scheme == null) {
uri.buildUpon().scheme("https").build() uri.buildUpon().scheme("https").build()
} else { } else {
@ -53,10 +44,7 @@ class IntentHandler(private val context: Context) {
startActivity(Intent(Intent.ACTION_VIEW, newUri)) startActivity(Intent(Intent.ACTION_VIEW, newUri))
} }
/** override fun shareText(text: String) {
* Launches the share sheet with the given [text].
*/
fun shareText(text: String) {
val sendIntent: Intent = Intent(Intent.ACTION_SEND).apply { val sendIntent: Intent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_TEXT, text) putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain" type = "text/plain"

View file

@ -22,6 +22,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
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 import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManagerImpl import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManagerImpl
@ -69,6 +71,7 @@ fun BitwardenTheme(
LocalNonMaterialColors provides nonMaterialColors, LocalNonMaterialColors provides nonMaterialColors,
LocalNonMaterialTypography provides nonMaterialTypography, LocalNonMaterialTypography provides nonMaterialTypography,
LocalPermissionsManager provides PermissionsManagerImpl(context as Activity), LocalPermissionsManager provides PermissionsManagerImpl(context as Activity),
LocalIntentManager provides IntentManagerImpl(context),
) { ) {
// Set overall theme based on color scheme and typography settings // Set overall theme based on color scheme and typography settings
MaterialTheme( MaterialTheme(
@ -156,7 +159,14 @@ private fun Int.toColor(context: Context): Color =
Color(context.getColor(this)) Color(context.getColor(this))
/** /**
* Provides access to non material theme typography throughout the app. * Provides access to the intent manager throughout the app.
*/
val LocalIntentManager: ProvidableCompositionLocal<IntentManager> = compositionLocalOf {
error("CompositionLocal LocalIntentManager not present")
}
/**
* Provides access to the permission manager throughout the app.
*/ */
val LocalPermissionsManager: ProvidableCompositionLocal<PermissionsManager> = compositionLocalOf { val LocalPermissionsManager: ProvidableCompositionLocal<PermissionsManager> = compositionLocalOf {
error("CompositionLocal LocalPermissionsManager not present") error("CompositionLocal LocalPermissionsManager not present")

View file

@ -28,7 +28,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
@ -40,6 +39,8 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenSearchActionItem import com.x8bit.bitwarden.ui.platform.components.BitwardenSearchActionItem
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
import com.x8bit.bitwarden.ui.tools.feature.send.handlers.SendHandlers import com.x8bit.bitwarden.ui.tools.feature.send.handlers.SendHandlers
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -53,7 +54,7 @@ fun SendScreen(
onNavigateToAddSend: () -> Unit, onNavigateToAddSend: () -> Unit,
onNavigateToEditSend: (sendItemId: String) -> Unit, onNavigateToEditSend: (sendItemId: String) -> Unit,
viewModel: SendViewModel = hiltViewModel(), viewModel: SendViewModel = hiltViewModel(),
intentHandler: IntentHandler = IntentHandler(context = LocalContext.current), intentManager: IntentManager = LocalIntentManager.current,
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
@ -74,11 +75,11 @@ fun SendScreen(
is SendEvent.NavigateToEditSend -> onNavigateToEditSend(event.sendId) is SendEvent.NavigateToEditSend -> onNavigateToEditSend(event.sendId)
is SendEvent.NavigateToAboutSend -> { is SendEvent.NavigateToAboutSend -> {
intentHandler.launchUri("https://bitwarden.com/products/send".toUri()) intentManager.launchUri("https://bitwarden.com/products/send".toUri())
} }
is SendEvent.ShowShareSheet -> { is SendEvent.ShowShareSheet -> {
intentHandler.shareText(event.url) intentManager.shareText(event.url)
} }
is SendEvent.ShowToast -> { is SendEvent.ShowToast -> {

View file

@ -22,7 +22,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
@ -35,6 +34,8 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandlers import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandlers
@ -46,7 +47,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandler
@Composable @Composable
fun AddSendScreen( fun AddSendScreen(
viewModel: AddSendViewModel = hiltViewModel(), viewModel: AddSendViewModel = hiltViewModel(),
intentHandler: IntentHandler = IntentHandler(LocalContext.current), intentManager: IntentManager = LocalIntentManager.current,
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@ -58,7 +59,7 @@ fun AddSendScreen(
when (event) { when (event) {
is AddSendEvent.NavigateBack -> onNavigateBack() is AddSendEvent.NavigateBack -> onNavigateBack()
is AddSendEvent.ShowShareSheet -> { is AddSendEvent.ShowShareSheet -> {
intentHandler.shareText(event.message) intentManager.shareText(event.message)
} }
is AddSendEvent.ShowToast -> { is AddSendEvent.ShowToast -> {

View file

@ -27,7 +27,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect 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.asText
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
@ -40,6 +39,8 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers
@ -53,7 +54,7 @@ import kotlinx.collections.immutable.persistentListOf
@Composable @Composable
fun VaultItemScreen( fun VaultItemScreen(
viewModel: VaultItemViewModel = hiltViewModel(), viewModel: VaultItemViewModel = hiltViewModel(),
intentHandler: IntentHandler = IntentHandler(context = LocalContext.current), intentManager: IntentManager = LocalIntentManager.current,
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToVaultAddEditItem: (vaultItemId: String) -> Unit, onNavigateToVaultAddEditItem: (vaultItemId: String) -> Unit,
onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit, onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit,
@ -75,7 +76,7 @@ fun VaultItemScreen(
Toast.makeText(context, "Not yet implemented.", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Not yet implemented.", Toast.LENGTH_SHORT).show()
} }
is VaultItemEvent.NavigateToUri -> intentHandler.launchUri(event.uri.toUri()) is VaultItemEvent.NavigateToUri -> intentManager.launchUri(event.uri.toUri())
is VaultItemEvent.NavigateToAttachments -> { is VaultItemEvent.NavigateToAttachments -> {
// TODO implement attachments in BIT-522 // TODO implement attachments in BIT-522

View file

@ -32,14 +32,15 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect 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.base.util.toAnnotatedString
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButton import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.theme.LocalPermissionsManager import com.x8bit.bitwarden.ui.platform.theme.LocalPermissionsManager
/** /**
@ -52,7 +53,7 @@ fun ManualCodeEntryScreen(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToQrCodeScreen: () -> Unit, onNavigateToQrCodeScreen: () -> Unit,
viewModel: ManualCodeEntryViewModel = hiltViewModel(), viewModel: ManualCodeEntryViewModel = hiltViewModel(),
intentHandler: IntentHandler = IntentHandler(LocalContext.current), intentManager: IntentManager = LocalIntentManager.current,
permissionsManager: PermissionsManager = LocalPermissionsManager.current, permissionsManager: PermissionsManager = LocalPermissionsManager.current,
) { ) {
var shouldShowPermissionDialog by rememberSaveable { mutableStateOf(false) } var shouldShowPermissionDialog by rememberSaveable { mutableStateOf(false) }
@ -74,7 +75,7 @@ fun ManualCodeEntryScreen(
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.parse("package:" + context.packageName) intent.data = Uri.parse("package:" + context.packageName)
intentHandler.startActivity(intent = intent) intentManager.startActivity(intent = intent)
} }
is ManualCodeEntryEvent.ShowToast -> { is ManualCodeEntryEvent.ShowToast -> {

View file

@ -35,7 +35,6 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect 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.asText
import com.x8bit.bitwarden.ui.platform.base.util.showNotYetImplementedToast import com.x8bit.bitwarden.ui.platform.base.util.showNotYetImplementedToast
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
@ -53,6 +52,8 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary 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.vault.feature.vault.model.VaultFilterType import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -71,7 +72,7 @@ fun VaultScreen(
onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit, onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit,
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit, onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit, onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
intentHandler: IntentHandler = IntentHandler(LocalContext.current), intentManager: IntentManager = LocalIntentManager.current,
) { ) {
val state by viewModel.stateFlow.collectAsState() val state by viewModel.stateFlow.collectAsState()
val context = LocalContext.current val context = LocalContext.current
@ -107,7 +108,7 @@ fun VaultScreen(
onNavigateToVaultItemListingScreen(event.itemListingType) onNavigateToVaultItemListingScreen(event.itemListingType)
} }
VaultEvent.NavigateOutOfApp -> intentHandler.exitApplication() VaultEvent.NavigateOutOfApp -> intentManager.exitApplication()
is VaultEvent.ShowToast -> { is VaultEvent.ShowToast -> {
Toast Toast
.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT) .makeText(context, event.message(context.resources), Toast.LENGTH_SHORT)

View file

@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.auth.feature.createaccount package com.x8bit.bitwarden.ui.auth.feature.createaccount
import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
@ -17,6 +16,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.core.net.toUri import androidx.core.net.toUri
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CloseClick import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CloseClick
@ -26,71 +26,64 @@ import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.Pas
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest 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.asText
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import io.mockk.every import io.mockk.every
import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test import org.junit.Test
class CreateAccountScreenTest : BaseComposeTest() { class CreateAccountScreenTest : BaseComposeTest() {
@Test private var onNavigateBackCalled = false
fun `app bar submit click should send SubmitClick action`() { private var onNavigateToLoginCalled = false
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE) private val intentManager = mockk<IntentManager>(relaxed = true) {
every { eventFlow } returns emptyFlow() every { startCustomTabsActivity(any()) } just runs
every { trySendAction(SubmitClick) } returns Unit every { startActivity(any()) } just runs
} }
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val mutableEventFlow = bufferedMutableSharedFlow<CreateAccountEvent>()
private val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns mutableEventFlow
every { trySendAction(any()) } just runs
}
@Before
fun setup() {
composeTestRule.setContent { composeTestRule.setContent {
CreateAccountScreen( CreateAccountScreen(
onNavigateBack = {}, onNavigateBack = { onNavigateBackCalled = true },
onNavigateToLogin = { _, _ -> }, onNavigateToLogin = { _, _ -> onNavigateToLoginCalled = true },
intentManager = intentManager,
viewModel = viewModel, viewModel = viewModel,
) )
} }
}
@Test
fun `app bar submit click should send SubmitClick action`() {
composeTestRule.onNodeWithText("Submit").performClick() composeTestRule.onNodeWithText("Submit").performClick()
verify { viewModel.trySendAction(SubmitClick) } verify { viewModel.trySendAction(SubmitClick) }
} }
@Test @Test
fun `close click should send CloseClick action`() { fun `close click should send CloseClick action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns emptyFlow()
every { trySendAction(CloseClick) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
)
}
composeTestRule.onNodeWithContentDescription("Close").performClick() composeTestRule.onNodeWithContentDescription("Close").performClick()
verify { viewModel.trySendAction(CloseClick) } verify { viewModel.trySendAction(CloseClick) }
} }
@Test @Test
fun `check data breaches click should send CheckDataBreachesToggle action`() { fun `check data breaches click should send CheckDataBreachesToggle action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns emptyFlow()
every { trySendAction(CheckDataBreachesToggle(true)) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
)
}
composeTestRule composeTestRule
.onNodeWithText("Check known data breaches for this password") .onNodeWithText("Check known data breaches for this password")
.performScrollTo() .performScrollTo()
@ -100,19 +93,6 @@ class CreateAccountScreenTest : BaseComposeTest() {
@Test @Test
fun `accept policies should be toggled on or off according to the state`() { fun `accept policies should be toggled on or off according to the state`() {
val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns emptyFlow()
every { trySendAction(AcceptPoliciesToggle(true)) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
)
}
composeTestRule composeTestRule
.onNodeWithText("By activating this switch you agree", substring = true) .onNodeWithText("By activating this switch you agree", substring = true)
.assertIsOff() .assertIsOff()
@ -126,18 +106,6 @@ class CreateAccountScreenTest : BaseComposeTest() {
@Test @Test
fun `accept policies click should send AcceptPoliciesToggle action`() { fun `accept policies click should send AcceptPoliciesToggle action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns emptyFlow()
every { trySendAction(AcceptPoliciesToggle(true)) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
)
}
composeTestRule composeTestRule
.onNodeWithText("By activating this switch you agree", substring = true) .onNodeWithText("By activating this switch you agree", substring = true)
.performScrollTo() .performScrollTo()
@ -147,188 +115,61 @@ class CreateAccountScreenTest : BaseComposeTest() {
@Test @Test
fun `NavigateBack event should invoke navigate back lambda`() { fun `NavigateBack event should invoke navigate back lambda`() {
var onNavigateBackCalled = false mutableEventFlow.tryEmit(CreateAccountEvent.NavigateBack)
val onNavigateBack = { onNavigateBackCalled = true }
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns flowOf(CreateAccountEvent.NavigateBack)
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = onNavigateBack,
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
)
}
assertTrue(onNavigateBackCalled) assertTrue(onNavigateBackCalled)
} }
@Test @Test
fun `NavigateToLogin event should invoke navigate login lambda`() { fun `NavigateToLogin event should invoke navigate login lambda`() {
var onNavigateToLoginCalled = false mutableEventFlow.tryEmit(CreateAccountEvent.NavigateToLogin(email = "", captchaToken = ""))
val onNavigateToLogin = { _: String, _: String -> onNavigateToLoginCalled = true }
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns flowOf(
CreateAccountEvent.NavigateToLogin(
email = "",
captchaToken = "",
),
)
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = onNavigateToLogin,
viewModel = viewModel,
)
}
assertTrue(onNavigateToLoginCalled) assertTrue(onNavigateToLoginCalled)
} }
@Test @Test
fun `NavigateToCaptcha event should invoke intent handler`() { fun `NavigateToCaptcha event should invoke intent manager`() {
val mockUri = mockk<Uri>() val mockUri = mockk<Uri>()
val intentHandler = mockk<IntentHandler>(relaxed = true) { mutableEventFlow.tryEmit(CreateAccountEvent.NavigateToCaptcha(uri = mockUri))
every { startCustomTabsActivity(mockUri) } returns Unit
}
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns flowOf(
CreateAccountEvent.NavigateToCaptcha(
uri = mockUri,
),
)
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
intentHandler = intentHandler,
)
}
verify { verify {
intentHandler.startCustomTabsActivity(mockUri) intentManager.startCustomTabsActivity(mockUri)
} }
} }
@Test @Test
fun `NavigateToPrivacyPolicy event should invoke intent handler`() { fun `NavigateToPrivacyPolicy event should invoke intent manager`() {
val expectedIntent = mutableEventFlow.tryEmit(CreateAccountEvent.NavigateToPrivacyPolicy)
Intent(Intent.ACTION_VIEW, Uri.parse("https://bitwarden.com/privacy/"))
val intentHandler = mockk<IntentHandler>(relaxed = true) {
every { startActivity(expectedIntent) } returns Unit
}
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns flowOf(CreateAccountEvent.NavigateToPrivacyPolicy)
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
intentHandler = intentHandler,
)
}
verify { verify {
intentHandler.launchUri("https://bitwarden.com/privacy/".toUri()) intentManager.launchUri("https://bitwarden.com/privacy/".toUri())
} }
} }
@Test @Test
fun `NavigateToTerms event should invoke intent handler`() { fun `NavigateToTerms event should invoke intent manager`() {
val expectedIntent = mutableEventFlow.tryEmit(CreateAccountEvent.NavigateToTerms)
Intent(Intent.ACTION_VIEW, Uri.parse("https://bitwarden.com/terms/"))
val intentHandler = mockk<IntentHandler>(relaxed = true) {
every { startActivity(expectedIntent) } returns Unit
}
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns flowOf(CreateAccountEvent.NavigateToTerms)
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
intentHandler = intentHandler,
)
}
verify { verify {
intentHandler.launchUri("https://bitwarden.com/terms/".toUri()) intentManager.launchUri("https://bitwarden.com/terms/".toUri())
} }
} }
@Test @Test
fun `email input change should send EmailInputChange action`() { fun `email input change should send EmailInputChange action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns emptyFlow()
every { trySendAction(EmailInputChange("input")) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
)
}
composeTestRule.onNodeWithText("Email address").performTextInput(TEST_INPUT) composeTestRule.onNodeWithText("Email address").performTextInput(TEST_INPUT)
verify { viewModel.trySendAction(EmailInputChange(TEST_INPUT)) } verify { viewModel.trySendAction(EmailInputChange(TEST_INPUT)) }
} }
@Test @Test
fun `password input change should send PasswordInputChange action`() { fun `password input change should send PasswordInputChange action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns emptyFlow()
every { trySendAction(PasswordInputChange("input")) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
)
}
composeTestRule.onNodeWithText("Master password").performTextInput(TEST_INPUT) composeTestRule.onNodeWithText("Master password").performTextInput(TEST_INPUT)
verify { viewModel.trySendAction(PasswordInputChange(TEST_INPUT)) } verify { viewModel.trySendAction(PasswordInputChange(TEST_INPUT)) }
} }
@Test @Test
fun `confirm password input change should send ConfirmPasswordInputChange action`() { fun `confirm password input change should send ConfirmPasswordInputChange action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns emptyFlow()
every { trySendAction(ConfirmPasswordInputChange("input")) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
)
}
composeTestRule.onNodeWithText("Re-type master password").performTextInput(TEST_INPUT) composeTestRule.onNodeWithText("Re-type master password").performTextInput(TEST_INPUT)
verify { viewModel.trySendAction(ConfirmPasswordInputChange(TEST_INPUT)) } verify { viewModel.trySendAction(ConfirmPasswordInputChange(TEST_INPUT)) }
} }
@Test @Test
fun `password hint input change should send PasswordHintChange action`() { fun `password hint input change should send PasswordHintChange action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns emptyFlow()
every { trySendAction(PasswordHintChange("input")) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
)
}
composeTestRule composeTestRule
.onNodeWithText("Master password hint (optional)") .onNodeWithText("Master password hint (optional)")
.performTextInput(TEST_INPUT) .performTextInput(TEST_INPUT)
@ -337,25 +178,14 @@ class CreateAccountScreenTest : BaseComposeTest() {
@Test @Test
fun `clicking OK on the error dialog should send ErrorDialogDismiss action`() { fun `clicking OK on the error dialog should send ErrorDialogDismiss action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) { mutableStateFlow.update {
every { stateFlow } returns MutableStateFlow( it.copy(
DEFAULT_STATE.copy(
dialog = CreateAccountDialog.Error( dialog = CreateAccountDialog.Error(
BasicDialogState.Shown( BasicDialogState.Shown(
title = "title".asText(), title = "title".asText(),
message = "message".asText(), message = "message".asText(),
), ),
), ),
),
)
every { eventFlow } returns emptyFlow()
every { trySendAction(CreateAccountAction.ErrorDialogDismiss) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
) )
} }
composeTestRule composeTestRule
@ -367,19 +197,8 @@ class CreateAccountScreenTest : BaseComposeTest() {
@Test @Test
fun `clicking No on the HIBP dialog should send ErrorDialogDismiss action`() { fun `clicking No on the HIBP dialog should send ErrorDialogDismiss action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) { mutableStateFlow.update {
every { stateFlow } returns MutableStateFlow( it.copy(dialog = CreateAccountDialog.HaveIBeenPwned)
DEFAULT_STATE.copy(dialog = CreateAccountDialog.HaveIBeenPwned),
)
every { eventFlow } returns emptyFlow()
every { trySendAction(CreateAccountAction.ErrorDialogDismiss) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
)
} }
composeTestRule composeTestRule
.onAllNodesWithText("No") .onAllNodesWithText("No")
@ -390,19 +209,8 @@ class CreateAccountScreenTest : BaseComposeTest() {
@Test @Test
fun `clicking Yes on the HIBP dialog should send ContinueWithBreachedPasswordClick action`() { fun `clicking Yes on the HIBP dialog should send ContinueWithBreachedPasswordClick action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) { mutableStateFlow.update {
every { stateFlow } returns MutableStateFlow( it.copy(dialog = CreateAccountDialog.HaveIBeenPwned)
DEFAULT_STATE.copy(dialog = CreateAccountDialog.HaveIBeenPwned),
)
every { eventFlow } returns emptyFlow()
every { trySendAction(CreateAccountAction.ErrorDialogDismiss) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
)
} }
composeTestRule composeTestRule
.onAllNodesWithText("Yes") .onAllNodesWithText("Yes")
@ -413,25 +221,14 @@ class CreateAccountScreenTest : BaseComposeTest() {
@Test @Test
fun `when BasicDialogState is Shown should show dialog`() { fun `when BasicDialogState is Shown should show dialog`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) { mutableStateFlow.update {
every { stateFlow } returns MutableStateFlow( it.copy(
DEFAULT_STATE.copy(
dialog = CreateAccountDialog.Error( dialog = CreateAccountDialog.Error(
BasicDialogState.Shown( BasicDialogState.Shown(
title = "title".asText(), title = "title".asText(),
message = "message".asText(), message = "message".asText(),
), ),
), ),
),
)
every { eventFlow } returns emptyFlow()
every { trySendAction(CreateAccountAction.ErrorDialogDismiss) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
) )
} }
composeTestRule.onNode(isDialog()).assertIsDisplayed() composeTestRule.onNode(isDialog()).assertIsDisplayed()
@ -439,18 +236,6 @@ class CreateAccountScreenTest : BaseComposeTest() {
@Test @Test
fun `password strength should change as state changes`() { fun `password strength should change as state changes`() {
val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns emptyFlow()
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
)
}
mutableStateFlow.update { mutableStateFlow.update {
DEFAULT_STATE.copy(passwordStrengthState = PasswordStrengthState.WEAK_1) DEFAULT_STATE.copy(passwordStrengthState = PasswordStrengthState.WEAK_1)
} }
@ -479,23 +264,10 @@ class CreateAccountScreenTest : BaseComposeTest() {
@Test @Test
fun `toggling one password field visibility should toggle the other`() { fun `toggling one password field visibility should toggle the other`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns emptyFlow()
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
)
}
// should start with 2 Show buttons: // should start with 2 Show buttons:
composeTestRule composeTestRule
.onAllNodesWithContentDescription("Show") .onAllNodesWithContentDescription("Show")
.assertCountEquals(2) .assertCountEquals(2)[0]
.get(0)
.performClick() .performClick()
// after clicking there should be no Show buttons: // after clicking there should be no Show buttons:
@ -506,8 +278,7 @@ class CreateAccountScreenTest : BaseComposeTest() {
// and there should be 2 hide buttons now, and we'll click the second one: // and there should be 2 hide buttons now, and we'll click the second one:
composeTestRule composeTestRule
.onAllNodesWithContentDescription("Hide") .onAllNodesWithContentDescription("Hide")
.assertCountEquals(2) .assertCountEquals(2)[1]
.get(1)
.performClick() .performClick()
// then there should be two show buttons again // then there should be two show buttons again

View file

@ -15,10 +15,10 @@ import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed
import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed
import com.x8bit.bitwarden.ui.util.assertNoDialogExists import com.x8bit.bitwarden.ui.util.assertNoDialogExists
@ -41,7 +41,7 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
class LoginScreenTest : BaseComposeTest() { class LoginScreenTest : BaseComposeTest() {
private val intentHandler = mockk<IntentHandler>(relaxed = true) { private val intentManager = mockk<IntentManager>(relaxed = true) {
every { startCustomTabsActivity(any()) } returns Unit every { startCustomTabsActivity(any()) } returns Unit
} }
private var onNavigateBackCalled = false private var onNavigateBackCalled = false
@ -62,7 +62,7 @@ class LoginScreenTest : BaseComposeTest() {
onNavigateToEnterpriseSignOn = { onNavigateToEnterpriseSignOnCalled = true }, onNavigateToEnterpriseSignOn = { onNavigateToEnterpriseSignOnCalled = true },
onNavigateToLoginWithDevice = { onNavigateToLoginWithDeviceCalled = true }, onNavigateToLoginWithDevice = { onNavigateToLoginWithDeviceCalled = true },
viewModel = viewModel, viewModel = viewModel,
intentHandler = intentHandler, intentManager = intentManager,
) )
} }
} }
@ -262,10 +262,10 @@ class LoginScreenTest : BaseComposeTest() {
} }
@Test @Test
fun `NavigateToCaptcha should call intentHandler startCustomTabsActivity`() { fun `NavigateToCaptcha should call intentManager startCustomTabsActivity`() {
val mockUri = mockk<Uri>() val mockUri = mockk<Uri>()
mutableEventFlow.tryEmit(LoginEvent.NavigateToCaptcha(mockUri)) mutableEventFlow.tryEmit(LoginEvent.NavigateToCaptcha(mockUri))
verify { intentHandler.startCustomTabsActivity(mockUri) } verify { intentManager.startCustomTabsActivity(mockUri) }
} }
@Test @Test

View file

@ -11,43 +11,55 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.core.net.toUri 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.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.asText
import io.mockk.Runs import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test import org.junit.Test
class AboutScreenTest : BaseComposeTest() { class AboutScreenTest : BaseComposeTest() {
private var haveCalledNavigateBack = false
private val mutableStateFlow = MutableStateFlow( private val mutableStateFlow = MutableStateFlow(
AboutState( AboutState(
version = "Version: 1.0.0 (1)".asText(), version = "Version: 1.0.0 (1)".asText(),
isSubmitCrashLogsEnabled = false, isSubmitCrashLogsEnabled = false,
), ),
) )
private val mutableEventFlow = bufferedMutableSharedFlow<AboutEvent>()
@Test
fun `on back click should send BackClick`() {
val viewModel: AboutViewModel = mockk { val viewModel: AboutViewModel = mockk {
every { stateFlow } returns mutableStateFlow every { stateFlow } returns mutableStateFlow
every { eventFlow } returns emptyFlow() every { eventFlow } returns mutableEventFlow
every { trySendAction(AboutAction.BackClick) } returns Unit every { trySendAction(any()) } just runs
} }
private val intentManager: IntentManager = mockk {
every { launchUri(any()) } just runs
}
@Before
fun setup() {
composeTestRule.setContent { composeTestRule.setContent {
AboutScreen( AboutScreen(
viewModel = viewModel, viewModel = viewModel,
onNavigateBack = { }, intentManager = intentManager,
onNavigateBack = { haveCalledNavigateBack = true },
) )
} }
}
@Test
fun `on back click should send BackClick`() {
composeTestRule.onNodeWithContentDescription("Back").performClick() composeTestRule.onNodeWithContentDescription("Back").performClick()
verify { viewModel.trySendAction(AboutAction.BackClick) } verify { viewModel.trySendAction(AboutAction.BackClick) }
} }
@ -55,17 +67,6 @@ class AboutScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `on bitwarden help center click should display confirmation dialog and confirm click should emit HelpCenterClick`() { fun `on bitwarden help center click should display confirmation dialog and confirm click should emit HelpCenterClick`() {
val viewModel: AboutViewModel = mockk {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns emptyFlow()
every { trySendAction(AboutAction.HelpCenterClick) } returns Unit
}
composeTestRule.setContent {
AboutScreen(
viewModel = viewModel,
onNavigateBack = { },
)
}
composeTestRule.onNode(isDialog()).assertDoesNotExist() composeTestRule.onNode(isDialog()).assertDoesNotExist()
composeTestRule.onNodeWithText("Bitwarden Help Center").performClick() composeTestRule.onNodeWithText("Bitwarden Help Center").performClick()
composeTestRule.onNode(isDialog()).assertExists() composeTestRule.onNode(isDialog()).assertExists()
@ -82,17 +83,6 @@ class AboutScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `on bitwarden web vault click should display confirmation dialog and confirm click should emit WebVaultClick`() { fun `on bitwarden web vault click should display confirmation dialog and confirm click should emit WebVaultClick`() {
val viewModel: AboutViewModel = mockk {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns emptyFlow()
every { trySendAction(AboutAction.WebVaultClick) } returns Unit
}
composeTestRule.setContent {
AboutScreen(
viewModel = viewModel,
onNavigateBack = { },
)
}
composeTestRule.onNode(isDialog()).assertDoesNotExist() composeTestRule.onNode(isDialog()).assertDoesNotExist()
composeTestRule.onNodeWithText("Bitwarden web vault").performClick() composeTestRule.onNodeWithText("Bitwarden web vault").performClick()
composeTestRule.onNode(isDialog()).assertExists() composeTestRule.onNode(isDialog()).assertExists()
@ -109,17 +99,6 @@ class AboutScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `on learn about organizations click should display confirmation dialog and confirm click should emit LearnAboutOrganizationsClick`() { fun `on learn about organizations click should display confirmation dialog and confirm click should emit LearnAboutOrganizationsClick`() {
val viewModel: AboutViewModel = mockk {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns emptyFlow()
every { trySendAction(AboutAction.LearnAboutOrganizationsClick) } returns Unit
}
composeTestRule.setContent {
AboutScreen(
viewModel = viewModel,
onNavigateBack = { },
)
}
composeTestRule.onNode(isDialog()).assertDoesNotExist() composeTestRule.onNode(isDialog()).assertDoesNotExist()
composeTestRule.onNodeWithText("Learn about organizations").performClick() composeTestRule.onNodeWithText("Learn about organizations").performClick()
composeTestRule.onNode(isDialog()).assertExists() composeTestRule.onNode(isDialog()).assertExists()
@ -135,97 +114,37 @@ class AboutScreenTest : BaseComposeTest() {
@Test @Test
fun `on NavigateBack should call onNavigateBack`() { fun `on NavigateBack should call onNavigateBack`() {
var haveCalledNavigateBack = false mutableEventFlow.tryEmit(AboutEvent.NavigateBack)
val viewModel = mockk<AboutViewModel> {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns flowOf(AboutEvent.NavigateBack)
}
composeTestRule.setContent {
AboutScreen(
viewModel = viewModel,
onNavigateBack = { haveCalledNavigateBack = true },
)
}
assertTrue(haveCalledNavigateBack) assertTrue(haveCalledNavigateBack)
} }
@Test @Test
fun `on NavigateToHelpCenter should call launchUri on IntentHandler`() { fun `on NavigateToHelpCenter should call launchUri on IntentManager`() {
val intentHandler = mockk<IntentHandler> { mutableEventFlow.tryEmit(AboutEvent.NavigateToHelpCenter)
every { launchUri(any()) } just Runs
}
val viewModel = mockk<AboutViewModel> {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns flowOf(AboutEvent.NavigateToHelpCenter)
}
composeTestRule.setContent {
AboutScreen(
viewModel = viewModel,
onNavigateBack = { },
intentHandler = intentHandler,
)
}
verify { verify {
intentHandler.launchUri("https://bitwarden.com/help".toUri()) intentManager.launchUri("https://bitwarden.com/help".toUri())
} }
} }
@Test @Test
fun `on NavigateToLearnAboutOrganizations should call launchUri on IntentHandler`() { fun `on NavigateToLearnAboutOrganizations should call launchUri on IntentManager`() {
val intentHandler = mockk<IntentHandler> { mutableEventFlow.tryEmit(AboutEvent.NavigateToLearnAboutOrganizations)
every { launchUri(any()) } just Runs
}
val viewModel = mockk<AboutViewModel> {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns flowOf(AboutEvent.NavigateToLearnAboutOrganizations)
}
composeTestRule.setContent {
AboutScreen(
viewModel = viewModel,
onNavigateBack = { },
intentHandler = intentHandler,
)
}
verify { verify {
intentHandler.launchUri("https://bitwarden.com/help/about-organizations".toUri()) intentManager.launchUri("https://bitwarden.com/help/about-organizations".toUri())
} }
} }
@Test @Test
fun `on NavigateToWebVault should call launchUri on IntentHandler`() { fun `on NavigateToWebVault should call launchUri on IntentManager`() {
val intentHandler = mockk<IntentHandler> { mutableEventFlow.tryEmit(AboutEvent.NavigateToWebVault)
every { launchUri(any()) } just Runs
}
val viewModel = mockk<AboutViewModel> {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns flowOf(AboutEvent.NavigateToWebVault)
}
composeTestRule.setContent {
AboutScreen(
viewModel = viewModel,
onNavigateBack = { },
intentHandler = intentHandler,
)
}
verify { verify {
intentHandler.launchUri("https://vault.bitwarden.com".toUri()) intentManager.launchUri("https://vault.bitwarden.com".toUri())
} }
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `on rate the app click should display confirmation dialog and confirm click should emit RateAppClick`() { fun `on rate the app click should display confirmation dialog and confirm click should emit RateAppClick`() {
val viewModel: AboutViewModel = mockk {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns emptyFlow()
every { trySendAction(AboutAction.RateAppClick) } returns Unit
}
composeTestRule.setContent {
AboutScreen(
viewModel = viewModel,
onNavigateBack = { },
)
}
composeTestRule.onNode(isDialog()).assertDoesNotExist() composeTestRule.onNode(isDialog()).assertDoesNotExist()
composeTestRule.onNodeWithText("Rate the app").performClick() composeTestRule.onNodeWithText("Rate the app").performClick()
composeTestRule.onNode(isDialog()).assertExists() composeTestRule.onNode(isDialog()).assertExists()
@ -242,17 +161,6 @@ class AboutScreenTest : BaseComposeTest() {
@Test @Test
fun `on submit crash logs toggle should send SubmitCrashLogsClick`() { fun `on submit crash logs toggle should send SubmitCrashLogsClick`() {
val enabled = true val enabled = true
val viewModel: AboutViewModel = mockk {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns emptyFlow()
every { trySendAction(AboutAction.SubmitCrashLogsClick(enabled)) } returns Unit
}
composeTestRule.setContent {
AboutScreen(
viewModel = viewModel,
onNavigateBack = { },
)
}
composeTestRule.onNodeWithText("Submit crash logs").performClick() composeTestRule.onNodeWithText("Submit crash logs").performClick()
verify { verify {
viewModel.trySendAction(AboutAction.SubmitCrashLogsClick(enabled)) viewModel.trySendAction(AboutAction.SubmitCrashLogsClick(enabled))
@ -260,16 +168,6 @@ class AboutScreenTest : BaseComposeTest() {
} }
fun `on submit crash logs should be toggled on or off according to the state`() { fun `on submit crash logs should be toggled on or off according to the state`() {
val viewModel = mockk<AboutViewModel>(relaxed = true) {
every { eventFlow } returns emptyFlow()
every { stateFlow } returns mutableStateFlow
}
composeTestRule.setContent {
AboutScreen(
viewModel = viewModel,
onNavigateBack = { },
)
}
composeTestRule.onNodeWithText("Submit crash logs").assertIsOff() composeTestRule.onNodeWithText("Submit crash logs").assertIsOff()
mutableStateFlow.update { it.copy(isSubmitCrashLogsEnabled = true) } mutableStateFlow.update { it.copy(isSubmitCrashLogsEnabled = true) }
composeTestRule.onNodeWithText("Submit crash logs").assertIsOn() composeTestRule.onNodeWithText("Submit crash logs").assertIsOn()
@ -277,17 +175,6 @@ class AboutScreenTest : BaseComposeTest() {
@Test @Test
fun `on version info click should send VersionClick`() { fun `on version info click should send VersionClick`() {
val viewModel: AboutViewModel = mockk {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns emptyFlow()
every { trySendAction(AboutAction.VersionClick) } returns Unit
}
composeTestRule.setContent {
AboutScreen(
viewModel = viewModel,
onNavigateBack = { },
)
}
composeTestRule.onNodeWithText("Version: 1.0.0 (1)").performClick() composeTestRule.onNodeWithText("Version: 1.0.0 (1)").performClick()
verify { verify {
viewModel.trySendAction(AboutAction.VersionClick) viewModel.trySendAction(AboutAction.VersionClick)
@ -296,16 +183,6 @@ class AboutScreenTest : BaseComposeTest() {
@Test @Test
fun `version should update according to the state`() = runTest { fun `version should update according to the state`() = runTest {
val viewModel = mockk<AboutViewModel> {
every { eventFlow } returns emptyFlow()
every { stateFlow } returns mutableStateFlow
}
composeTestRule.setContent {
AboutScreen(
viewModel = viewModel,
onNavigateBack = { },
)
}
composeTestRule.onNodeWithText("Version: 1.0.0 (1)").assertIsDisplayed() composeTestRule.onNodeWithText("Version: 1.0.0 (1)").assertIsDisplayed()
mutableStateFlow.update { it.copy(version = "Version: 1.1.0 (2)".asText()) } mutableStateFlow.update { it.copy(version = "Version: 1.1.0 (2)".asText()) }

View file

@ -21,8 +21,8 @@ import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest 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.asText
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.util.assertNoDialogExists import com.x8bit.bitwarden.ui.util.assertNoDialogExists
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
@ -41,7 +41,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
private var onNavigateBackCalled = false private var onNavigateBackCalled = false
private var onNavigateToDeleteAccountCalled = false private var onNavigateToDeleteAccountCalled = false
private val intentHandler = mockk<IntentHandler> { private val intentManager = mockk<IntentManager> {
every { launchUri(any()) } just runs every { launchUri(any()) } just runs
} }
private val mutableEventFlow = bufferedMutableSharedFlow<AccountSecurityEvent>() private val mutableEventFlow = bufferedMutableSharedFlow<AccountSecurityEvent>()
@ -58,7 +58,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
onNavigateBack = { onNavigateBackCalled = true }, onNavigateBack = { onNavigateBackCalled = true },
onNavigateToDeleteAccount = { onNavigateToDeleteAccountCalled = true }, onNavigateToDeleteAccount = { onNavigateToDeleteAccountCalled = true },
viewModel = viewModel, viewModel = viewModel,
intentHandler = intentHandler, intentManager = intentManager,
) )
} }
} }
@ -997,10 +997,10 @@ class AccountSecurityScreenTest : BaseComposeTest() {
} }
@Test @Test
fun `on NavigateToFingerprintPhrase should call launchUri on intentHandler`() { fun `on NavigateToFingerprintPhrase should call launchUri on intentManager`() {
mutableEventFlow.tryEmit(AccountSecurityEvent.NavigateToFingerprintPhrase) mutableEventFlow.tryEmit(AccountSecurityEvent.NavigateToFingerprintPhrase)
verify { verify {
intentHandler.launchUri("http://bitwarden.com/help/fingerprint-phrase".toUri()) intentManager.launchUri("http://bitwarden.com/help/fingerprint-phrase".toUri())
} }
} }

View file

@ -20,8 +20,8 @@ import androidx.compose.ui.test.performScrollToNode
import androidx.core.net.toUri import androidx.core.net.toUri
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest 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.asText
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.util.assertNoDialogExists import com.x8bit.bitwarden.ui.util.assertNoDialogExists
import com.x8bit.bitwarden.ui.util.isProgressBar import com.x8bit.bitwarden.ui.util.isProgressBar
import io.mockk.every import io.mockk.every
@ -41,7 +41,7 @@ class SendScreenTest : BaseComposeTest() {
private var onNavigateToNewSendCalled = false private var onNavigateToNewSendCalled = false
private var onNavigateToEditSendId: String? = null private var onNavigateToEditSendId: String? = null
private val intentHandler = mockk<IntentHandler> { private val intentManager = mockk<IntentManager> {
every { launchUri(any()) } just runs every { launchUri(any()) } just runs
every { shareText(any()) } just runs every { shareText(any()) } just runs
} }
@ -59,7 +59,7 @@ class SendScreenTest : BaseComposeTest() {
viewModel = viewModel, viewModel = viewModel,
onNavigateToAddSend = { onNavigateToNewSendCalled = true }, onNavigateToAddSend = { onNavigateToNewSendCalled = true },
onNavigateToEditSend = { onNavigateToEditSendId = it }, onNavigateToEditSend = { onNavigateToEditSendId = it },
intentHandler = intentHandler, intentManager = intentManager,
) )
} }
} }
@ -78,19 +78,19 @@ class SendScreenTest : BaseComposeTest() {
} }
@Test @Test
fun `on NavigateToAboutSend should call launchUri on intentHandler`() { fun `on NavigateToAboutSend should call launchUri on intentManager`() {
mutableEventFlow.tryEmit(SendEvent.NavigateToAboutSend) mutableEventFlow.tryEmit(SendEvent.NavigateToAboutSend)
verify { verify {
intentHandler.launchUri("https://bitwarden.com/products/send".toUri()) intentManager.launchUri("https://bitwarden.com/products/send".toUri())
} }
} }
@Test @Test
fun `on ShowShareSheet should call shareText on IntentHandler`() { fun `on ShowShareSheet should call shareText on IntentManager`() {
val text = "sharable stuff" val text = "sharable stuff"
mutableEventFlow.tryEmit(SendEvent.ShowShareSheet(text)) mutableEventFlow.tryEmit(SendEvent.ShowShareSheet(text))
verify { verify {
intentHandler.shareText(text) intentManager.shareText(text)
} }
} }

View file

@ -22,8 +22,8 @@ import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest 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.asText
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.model.AddSendType
import com.x8bit.bitwarden.ui.util.isEditableText import com.x8bit.bitwarden.ui.util.isEditableText
import com.x8bit.bitwarden.ui.util.isProgressBar import com.x8bit.bitwarden.ui.util.isProgressBar
@ -44,7 +44,7 @@ class AddSendScreenTest : BaseComposeTest() {
private var onNavigateBackCalled = false private var onNavigateBackCalled = false
private val intentHandler: IntentHandler = mockk { private val intentManager: IntentManager = mockk {
every { shareText(any()) } just runs every { shareText(any()) } just runs
} }
private val mutableEventFlow = bufferedMutableSharedFlow<AddSendEvent>() private val mutableEventFlow = bufferedMutableSharedFlow<AddSendEvent>()
@ -59,7 +59,7 @@ class AddSendScreenTest : BaseComposeTest() {
composeTestRule.setContent { composeTestRule.setContent {
AddSendScreen( AddSendScreen(
viewModel = viewModel, viewModel = viewModel,
intentHandler = intentHandler, intentManager = intentManager,
onNavigateBack = { onNavigateBackCalled = true }, onNavigateBack = { onNavigateBackCalled = true },
) )
} }
@ -72,11 +72,11 @@ class AddSendScreenTest : BaseComposeTest() {
} }
@Test @Test
fun `on ShowShareSheet should call shareText on IntentHandler`() { fun `on ShowShareSheet should call shareText on IntentManager`() {
val text = "sharable stuff" val text = "sharable stuff"
mutableEventFlow.tryEmit(AddSendEvent.ShowShareSheet(text)) mutableEventFlow.tryEmit(AddSendEvent.ShowShareSheet(text))
verify { verify {
intentHandler.shareText(text) intentManager.shareText(text)
} }
} }

View file

@ -25,8 +25,8 @@ import androidx.compose.ui.test.performTextInput
import androidx.core.net.toUri import androidx.core.net.toUri
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest 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.asText
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.util.assertScrollableNodeDoesNotExist import com.x8bit.bitwarden.ui.util.assertScrollableNodeDoesNotExist
import com.x8bit.bitwarden.ui.util.isProgressBar import com.x8bit.bitwarden.ui.util.isProgressBar
import com.x8bit.bitwarden.ui.util.onFirstNodeWithTextAfterScroll import com.x8bit.bitwarden.ui.util.onFirstNodeWithTextAfterScroll
@ -53,7 +53,7 @@ class VaultItemScreenTest : BaseComposeTest() {
private var onNavigateToVaultEditItemId: String? = null private var onNavigateToVaultEditItemId: String? = null
private var onNavigateToMoveToOrganizationItemId: String? = null private var onNavigateToMoveToOrganizationItemId: String? = null
private val intentHandler = mockk<IntentHandler>() private val intentManager = mockk<IntentManager>()
private val mutableEventFlow = bufferedMutableSharedFlow<VaultItemEvent>() private val mutableEventFlow = bufferedMutableSharedFlow<VaultItemEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
@ -70,7 +70,7 @@ class VaultItemScreenTest : BaseComposeTest() {
onNavigateBack = { onNavigateBackCalled = true }, onNavigateBack = { onNavigateBackCalled = true },
onNavigateToVaultAddEditItem = { onNavigateToVaultEditItemId = it }, onNavigateToVaultAddEditItem = { onNavigateToVaultEditItemId = it },
onNavigateToMoveToOrganization = { onNavigateToMoveToOrganizationItemId = it }, onNavigateToMoveToOrganization = { onNavigateToMoveToOrganizationItemId = it },
intentHandler = intentHandler, intentManager = intentManager,
) )
} }
} }
@ -109,12 +109,12 @@ class VaultItemScreenTest : BaseComposeTest() {
fun `NavigateToUri event should invoke launchUri`() { fun `NavigateToUri event should invoke launchUri`() {
val uriString = "http://www.example.com" val uriString = "http://www.example.com"
val uri = uriString.toUri() val uri = uriString.toUri()
every { intentHandler.launchUri(uri) } just runs every { intentManager.launchUri(uri) } just runs
mutableEventFlow.tryEmit(VaultItemEvent.NavigateToUri(uriString)) mutableEventFlow.tryEmit(VaultItemEvent.NavigateToUri(uriString))
verify(exactly = 1) { verify(exactly = 1) {
intentHandler.launchUri(uri) intentManager.launchUri(uri)
} }
} }

View file

@ -17,7 +17,7 @@ import androidx.compose.ui.test.performTextInput
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -40,7 +40,7 @@ class ManualCodeEntryScreenTests : BaseComposeTest() {
MutableStateFlow(ManualCodeEntryState("")) MutableStateFlow(ManualCodeEntryState(""))
private val fakePermissionManager: FakePermissionManager = FakePermissionManager() private val fakePermissionManager: FakePermissionManager = FakePermissionManager()
private val intentHandler = mockk<IntentHandler>(relaxed = true) private val intentManager = mockk<IntentManager>(relaxed = true)
private val viewModel = mockk<ManualCodeEntryViewModel>(relaxed = true) { private val viewModel = mockk<ManualCodeEntryViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow every { eventFlow } returns mutableEventFlow
@ -57,7 +57,7 @@ class ManualCodeEntryScreenTests : BaseComposeTest() {
onNavigateToScanQrCodeCalled = true onNavigateToScanQrCodeCalled = true
}, },
permissionsManager = fakePermissionManager, permissionsManager = fakePermissionManager,
intentHandler = intentHandler, intentManager = intentManager,
) )
} }
} }
@ -86,7 +86,7 @@ class ManualCodeEntryScreenTests : BaseComposeTest() {
) )
val intentSlot = slot<Intent>() val intentSlot = slot<Intent>()
verify { intentHandler.startActivity(capture(intentSlot)) } verify { intentManager.startActivity(capture(intentSlot)) }
assertEquals( assertEquals(
uri, uri,

View file

@ -18,9 +18,9 @@ import androidx.compose.ui.test.performScrollToNode
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest 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.asText
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed
import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed
import com.x8bit.bitwarden.ui.util.assertNoDialogExists import com.x8bit.bitwarden.ui.util.assertNoDialogExists
@ -58,7 +58,7 @@ class VaultScreenTest : BaseComposeTest() {
private var onNavigateToVaultEditItemId: String? = null private var onNavigateToVaultEditItemId: String? = null
private var onNavigateToVaultItemListingType: VaultItemListingType? = null private var onNavigateToVaultItemListingType: VaultItemListingType? = null
private var onDimBottomNavBarRequestCalled = false private var onDimBottomNavBarRequestCalled = false
private val intentHandler = mockk<IntentHandler>(relaxed = true) private val intentManager = mockk<IntentManager>(relaxed = true)
private val mutableEventFlow = bufferedMutableSharedFlow<VaultEvent>() private val mutableEventFlow = bufferedMutableSharedFlow<VaultEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
@ -77,7 +77,7 @@ class VaultScreenTest : BaseComposeTest() {
onNavigateToVaultEditItemScreen = { onNavigateToVaultEditItemId = it }, onNavigateToVaultEditItemScreen = { onNavigateToVaultEditItemId = it },
onNavigateToVaultItemListingScreen = { onNavigateToVaultItemListingType = it }, onNavigateToVaultItemListingScreen = { onNavigateToVaultItemListingType = it },
onDimBottomNavBarRequest = { onDimBottomNavBarRequestCalled = true }, onDimBottomNavBarRequest = { onDimBottomNavBarRequestCalled = true },
intentHandler = intentHandler, intentManager = intentManager,
) )
} }
} }
@ -618,9 +618,9 @@ class VaultScreenTest : BaseComposeTest() {
} }
@Test @Test
fun `NavigateOutOfApp event should call exitApplication on the IntentHandler`() { fun `NavigateOutOfApp event should call exitApplication on the IntentManager`() {
mutableEventFlow.tryEmit(VaultEvent.NavigateOutOfApp) mutableEventFlow.tryEmit(VaultEvent.NavigateOutOfApp)
verify { intentHandler.exitApplication() } verify { intentManager.exitApplication() }
} }
@Test @Test