mirror of
https://github.com/bitwarden/android.git
synced 2025-03-16 19:28:44 +03:00
PM-10630 setup autofill UI and interactions set up (#3891)
This commit is contained in:
parent
b94a1adda9
commit
8dce8cd576
7 changed files with 812 additions and 0 deletions
|
@ -0,0 +1,30 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
|
||||
|
||||
private const val SETUP_AUTO_FILL_ROUTE = "setup_auto_fill"
|
||||
|
||||
/**
|
||||
* Navigate to the setup auto-fill screen.
|
||||
*/
|
||||
fun NavController.navigateToSetupAutoFillScreen(navOptions: NavOptions? = null) {
|
||||
this.navigate(SETUP_AUTO_FILL_ROUTE, navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the setup auto-fil screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.setupAutoFillDestination(
|
||||
onNavigateToCompleteSetup: () -> Unit,
|
||||
) {
|
||||
composableWithPushTransitions(
|
||||
route = SETUP_AUTO_FILL_ROUTE,
|
||||
) {
|
||||
SetupAutoFillScreen(
|
||||
onNavigateToCompleteSetup = onNavigateToCompleteSetup,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* View model for the Auto-fill setup screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class SetupAutoFillViewModel @Inject constructor(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
) :
|
||||
BaseViewModel<SetupAutoFillState, SetupAutoFillEvent, SetupAutoFillAction>(
|
||||
initialState = SetupAutoFillState(dialogState = null, autofillEnabled = false),
|
||||
) {
|
||||
|
||||
init {
|
||||
settingsRepository
|
||||
.isAutofillEnabledStateFlow
|
||||
.map {
|
||||
SetupAutoFillAction.Internal.AutofillEnabledUpdateReceive(isAutofillEnabled = it)
|
||||
}
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: SetupAutoFillAction) {
|
||||
when (action) {
|
||||
is SetupAutoFillAction.AutofillServiceChanged -> handleAutofillServiceChanged(action)
|
||||
SetupAutoFillAction.ContinueClick -> handleContinueClick()
|
||||
SetupAutoFillAction.DismissDialog -> handleDismissDialog()
|
||||
SetupAutoFillAction.TurnOnLaterClick -> handleTurnOnLaterClick()
|
||||
SetupAutoFillAction.AutoFillServiceFallback -> handleAutoFillServiceFallback()
|
||||
SetupAutoFillAction.TurnOnLaterConfirmClick -> handleTurnOnLaterConfirmClick()
|
||||
is SetupAutoFillAction.Internal.AutofillEnabledUpdateReceive -> {
|
||||
handleAutofillEnabledUpdateReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAutofillEnabledUpdateReceive(
|
||||
action: SetupAutoFillAction.Internal.AutofillEnabledUpdateReceive,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(autofillEnabled = action.isAutofillEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAutoFillServiceFallback() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = SetupAutoFillDialogState.AutoFillFallbackDialog)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTurnOnLaterClick() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = SetupAutoFillDialogState.TurnOnLaterDialog)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDismissDialog() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTurnOnLaterConfirmClick() {
|
||||
sendEvent(SetupAutoFillEvent.NavigateToCompleteSetup)
|
||||
}
|
||||
|
||||
private fun handleContinueClick() {
|
||||
sendEvent(SetupAutoFillEvent.NavigateToCompleteSetup)
|
||||
}
|
||||
|
||||
private fun handleAutofillServiceChanged(action: SetupAutoFillAction.AutofillServiceChanged) {
|
||||
if (action.autofillEnabled) {
|
||||
sendEvent(SetupAutoFillEvent.NavigateToAutofillSettings)
|
||||
} else {
|
||||
settingsRepository.disableAutofill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UI State for the Auto-fill setup screen.
|
||||
*/
|
||||
data class SetupAutoFillState(
|
||||
val dialogState: SetupAutoFillDialogState?,
|
||||
val autofillEnabled: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
* Dialog states for the Auto-fill setup screen.
|
||||
*/
|
||||
sealed class SetupAutoFillDialogState {
|
||||
/**
|
||||
* Represents the turn on later dialog.
|
||||
*/
|
||||
data object TurnOnLaterDialog : SetupAutoFillDialogState()
|
||||
|
||||
/**
|
||||
* Represents the autofill fallback dialog.
|
||||
*/
|
||||
data object AutoFillFallbackDialog : SetupAutoFillDialogState()
|
||||
}
|
||||
|
||||
/**
|
||||
* UI Events for the Auto-fill setup screen.
|
||||
*/
|
||||
sealed class SetupAutoFillEvent {
|
||||
/**
|
||||
* Navigate to the complete setup screen.
|
||||
*/
|
||||
data object NavigateToCompleteSetup : SetupAutoFillEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the autofill settings screen.
|
||||
*/
|
||||
data object NavigateToAutofillSettings : SetupAutoFillEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* UI Actions for the Auto-fill setup screen.
|
||||
*/
|
||||
sealed class SetupAutoFillAction {
|
||||
/**
|
||||
* Dismiss the current dialog.
|
||||
*/
|
||||
data object DismissDialog : SetupAutoFillAction()
|
||||
|
||||
/**
|
||||
* Move on to the next set-up step.
|
||||
*/
|
||||
data object ContinueClick : SetupAutoFillAction()
|
||||
|
||||
/**
|
||||
* Turn autofill on later has been clicked.
|
||||
*/
|
||||
data object TurnOnLaterClick : SetupAutoFillAction()
|
||||
|
||||
/**
|
||||
* Turn autofill on later has been confirmed.
|
||||
*/
|
||||
data object TurnOnLaterConfirmClick : SetupAutoFillAction()
|
||||
|
||||
/**
|
||||
* Autofill service selection has changed.
|
||||
*
|
||||
* @param autofillEnabled Whether autofill is enabled.
|
||||
*/
|
||||
data class AutofillServiceChanged(val autofillEnabled: Boolean) : SetupAutoFillAction()
|
||||
|
||||
/**
|
||||
* Autofill service fallback has occurred.
|
||||
*/
|
||||
data object AutoFillServiceFallback : SetupAutoFillAction()
|
||||
|
||||
/**
|
||||
* Internal actions not send through UI.
|
||||
*/
|
||||
sealed class Internal : SetupAutoFillAction() {
|
||||
/**
|
||||
* An update for changes in the [isAutofillEnabled] value.
|
||||
*/
|
||||
data class AutofillEnabledUpdateReceive(val isAutofillEnabled: Boolean) : Internal()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.handlers.rememberSetupAutoFillHandler
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenWideSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* Top level composable for the Auto-fill setup screen.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SetupAutoFillScreen(
|
||||
onNavigateToCompleteSetup: () -> Unit,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
viewModel: SetupAutoFillViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val handler = rememberSetupAutoFillHandler(viewModel = viewModel)
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
SetupAutoFillEvent.NavigateToCompleteSetup -> onNavigateToCompleteSetup()
|
||||
SetupAutoFillEvent.NavigateToAutofillSettings -> {
|
||||
val showFallback = !intentManager.startSystemAutofillSettingsActivity()
|
||||
if (showFallback) {
|
||||
handler.sendAutoFillServiceFallback.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
when (state.dialogState) {
|
||||
is SetupAutoFillDialogState.AutoFillFallbackDialog -> {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = null,
|
||||
message = R.string.bitwarden_autofill_go_to_settings.asText(),
|
||||
),
|
||||
onDismissRequest = handler.onDismissDialog,
|
||||
)
|
||||
}
|
||||
|
||||
is SetupAutoFillDialogState.TurnOnLaterDialog -> {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(R.string.turn_on_autofill_later),
|
||||
message = stringResource(R.string.return_to_complete_this_step_anytime_in_settings),
|
||||
confirmButtonText = stringResource(id = R.string.confirm),
|
||||
dismissButtonText = stringResource(id = R.string.cancel),
|
||||
onConfirmClick = handler.onConfirmTurnOnLaterClick,
|
||||
onDismissClick = handler.onDismissDialog,
|
||||
onDismissRequest = handler.onDismissDialog,
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.account_setup),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = null,
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
SetupAutoFillContent(
|
||||
autofillEnabled = state.autofillEnabled,
|
||||
onAutofillServiceChanged = { handler.onAutofillServiceChanged(it) },
|
||||
onContinueClick = handler.onContinueClick,
|
||||
onTurnOnLaterClick = handler.onTurnOnLaterClick,
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun SetupAutoFillContent(
|
||||
autofillEnabled: Boolean,
|
||||
onAutofillServiceChanged: (Boolean) -> Unit,
|
||||
onContinueClick: () -> Unit,
|
||||
onTurnOnLaterClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
// Animated Image placeholder TODO PM-10843
|
||||
Image(
|
||||
painter = rememberVectorPainter(id = R.drawable.account_setup),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.turn_on_autofill),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.use_autofill_to_log_into_your_accounts),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
BitwardenWideSwitch(
|
||||
label = stringResource(
|
||||
R.string.autofill_services,
|
||||
),
|
||||
isChecked = autofillEnabled,
|
||||
onCheckedChange = onAutofillServiceChanged,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.continue_text),
|
||||
onClick = onContinueClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
BitwardenTextButton(
|
||||
label = stringResource(R.string.turn_on_later),
|
||||
onClick = onTurnOnLaterClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun SetupAutoFillContentDisabled_preview() {
|
||||
BitwardenTheme {
|
||||
SetupAutoFillContent(
|
||||
autofillEnabled = false,
|
||||
onAutofillServiceChanged = {},
|
||||
onContinueClick = {},
|
||||
onTurnOnLaterClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun SetupAutoFillContentEnabled_preview() {
|
||||
BitwardenTheme {
|
||||
SetupAutoFillContent(
|
||||
autofillEnabled = true,
|
||||
onAutofillServiceChanged = {},
|
||||
onContinueClick = {},
|
||||
onTurnOnLaterClick = {},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup.handlers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupAutoFillAction
|
||||
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupAutoFillViewModel
|
||||
|
||||
/**
|
||||
* Handler for the Auto-fill setup screen.
|
||||
*/
|
||||
data class SetupAutoFillHandler(
|
||||
val onAutofillServiceChanged: (Boolean) -> Unit,
|
||||
val onContinueClick: () -> Unit,
|
||||
val onTurnOnLaterClick: () -> Unit,
|
||||
val onDismissDialog: () -> Unit,
|
||||
val onConfirmTurnOnLaterClick: () -> Unit,
|
||||
val sendAutoFillServiceFallback: () -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Convenience function for creating a [SetupAutoFillHandler] with a
|
||||
* [SetupAutoFillViewModel].
|
||||
*/
|
||||
fun create(viewModel: SetupAutoFillViewModel): SetupAutoFillHandler = SetupAutoFillHandler(
|
||||
onAutofillServiceChanged = {
|
||||
viewModel.trySendAction(
|
||||
SetupAutoFillAction.AutofillServiceChanged(
|
||||
it,
|
||||
),
|
||||
)
|
||||
},
|
||||
onContinueClick = { viewModel.trySendAction(SetupAutoFillAction.ContinueClick) },
|
||||
onTurnOnLaterClick = { viewModel.trySendAction(SetupAutoFillAction.TurnOnLaterClick) },
|
||||
onDismissDialog = { viewModel.trySendAction(SetupAutoFillAction.DismissDialog) },
|
||||
onConfirmTurnOnLaterClick = {
|
||||
viewModel.trySendAction(SetupAutoFillAction.TurnOnLaterConfirmClick)
|
||||
},
|
||||
sendAutoFillServiceFallback = {
|
||||
viewModel.trySendAction(SetupAutoFillAction.AutoFillServiceFallback)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function for creating a [SetupAutoFillHandler] in a [Composable] scope.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberSetupAutoFillHandler(viewModel: SetupAutoFillViewModel): SetupAutoFillHandler =
|
||||
remember(viewModel) {
|
||||
SetupAutoFillHandler.create(viewModel)
|
||||
}
|
|
@ -995,4 +995,9 @@ Do you want to switch to this account?</string>
|
|||
<string name="authenticator_sync">Authenticator Sync</string>
|
||||
<string name="allow_bitwarden_authenticator_syncing">Allow Bitwarden Authenticator Syncing</string>
|
||||
<string name="there_was_an_issue_validating_the_registration_token">There was an issue validating the registration token.</string>
|
||||
<string name="turn_on_autofill">Turn on autofill</string>
|
||||
<string name="use_autofill_to_log_into_your_accounts">Use autofill to log into your accounts with a single tap.</string>
|
||||
<string name="turn_on_later">Turn on later</string>
|
||||
<string name="turn_on_autofill_later">Turn on autofill later?</string>
|
||||
<string name="return_to_complete_this_step_anytime_in_settings">You can return to complete this step anytime in Settings.</string>
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
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
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SetupAutoFillViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val mutableAutoFillEnabledStateFlow = MutableStateFlow(false)
|
||||
private val settingsRepository = mockk<SettingsRepository>(relaxed = true) {
|
||||
every { isAutofillEnabledStateFlow } returns mutableAutoFillEnabledStateFlow
|
||||
every { disableAutofill() } just runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleAutofillEnabledUpdateReceive updates autofillEnabled state`() {
|
||||
val viewModel = createViewModel()
|
||||
assertFalse(viewModel.stateFlow.value.autofillEnabled)
|
||||
mutableAutoFillEnabledStateFlow.value = true
|
||||
|
||||
assertTrue(viewModel.stateFlow.value.autofillEnabled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleAutofillServiceChanged with autofillEnabled true navigates to autofill settings`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SetupAutoFillAction.AutofillServiceChanged(true))
|
||||
assertEquals(
|
||||
SetupAutoFillEvent.NavigateToAutofillSettings,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `handleAutofillServiceChanged with autofillEnabled false disables autofill`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
mutableAutoFillEnabledStateFlow.value = true
|
||||
assertTrue(viewModel.stateFlow.value.autofillEnabled)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SetupAutoFillAction.AutofillServiceChanged(false))
|
||||
expectNoEvents()
|
||||
}
|
||||
verify { settingsRepository.disableAutofill() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleContinueClick sends NavigateToCompleteSetup event`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SetupAutoFillAction.ContinueClick)
|
||||
assertEquals(SetupAutoFillEvent.NavigateToCompleteSetup, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleTurnOnLater click sets dialogState to TurnOnLaterDialog`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(SetupAutoFillAction.TurnOnLaterClick)
|
||||
assertEquals(
|
||||
SetupAutoFillDialogState.TurnOnLaterDialog,
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleTurnOnLaterConfirmClick sends NavigateToCompleteSetup event`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SetupAutoFillAction.TurnOnLaterConfirmClick)
|
||||
assertEquals(SetupAutoFillEvent.NavigateToCompleteSetup, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleDismissDialog sets dialogState to null`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(SetupAutoFillAction.TurnOnLaterClick)
|
||||
assertEquals(
|
||||
SetupAutoFillDialogState.TurnOnLaterDialog,
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
viewModel.trySendAction(SetupAutoFillAction.DismissDialog)
|
||||
assertNull(viewModel.stateFlow.value.dialogState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleAutoFillServiceFallback sets dialogState to AutoFillFallbackDialog`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(SetupAutoFillAction.AutoFillServiceFallback)
|
||||
assertEquals(
|
||||
SetupAutoFillDialogState.AutoFillFallbackDialog,
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createViewModel() = SetupAutoFillViewModel(settingsRepository)
|
||||
}
|
|
@ -0,0 +1,220 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class SetupAutofillScreenTest : BaseComposeTest() {
|
||||
|
||||
private var onNavigateToCompleteSetupCalled = false
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<SetupAutoFillEvent>()
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
|
||||
private val viewModel = mockk<SetupAutoFillViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
}
|
||||
|
||||
private val intentManager = mockk<IntentManager>(relaxed = true)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
composeTestRule.setContent {
|
||||
SetupAutoFillScreen(
|
||||
onNavigateToCompleteSetup = { onNavigateToCompleteSetupCalled = true },
|
||||
intentManager = intentManager,
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Turning on autofill should send AutofillServiceChanged with value of true`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Auto-fill services")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(SetupAutoFillAction.AutofillServiceChanged(true))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Turning off autofill should send AutofillServiceChanged with value of false`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(autofillEnabled = true)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("Auto-fill services", ignoreCase = true)
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(SetupAutoFillAction.AutofillServiceChanged(false))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Continue click should send correct action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Continue")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(SetupAutoFillAction.ContinueClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Turn on later click should send correct action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Turn on later")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(SetupAutoFillAction.TurnOnLaterClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToAutoFillSettings should start system autofill settings activity`() {
|
||||
every { intentManager.startSystemAutofillSettingsActivity() } returns true
|
||||
mutableEventFlow.tryEmit(SetupAutoFillEvent.NavigateToAutofillSettings)
|
||||
verify {
|
||||
intentManager.startSystemAutofillSettingsActivity()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `NavigateToAutoFillSettings should send AutoFillServiceFallback action when intent fails`() {
|
||||
every { intentManager.startSystemAutofillSettingsActivity() } returns false
|
||||
mutableEventFlow.tryEmit(SetupAutoFillEvent.NavigateToAutofillSettings)
|
||||
verify { viewModel.trySendAction(SetupAutoFillAction.AutoFillServiceFallback) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToCompleteSetup should call onNavigateToCompleteSetup`() {
|
||||
mutableEventFlow.tryEmit(SetupAutoFillEvent.NavigateToCompleteSetup)
|
||||
assertTrue(onNavigateToCompleteSetupCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Show autofill fallback dialog when dialog state is AutoFillFallbackDialog`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = SetupAutoFillDialogState.AutoFillFallbackDialog,
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(isDialog())
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
"We were unable to automatically open the Android autofill",
|
||||
substring = true,
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `When autofill fallback dialog is dismissed, sends action to dismiss dialog and is removed when state is null`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = SetupAutoFillDialogState.AutoFillFallbackDialog,
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(isDialog())
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Ok")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(SetupAutoFillAction.DismissDialog) }
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = null,
|
||||
)
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Show turn on later dialog when dialog state is TurnOnLaterDialog`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = SetupAutoFillDialogState.TurnOnLaterDialog,
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(isDialog())
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Turn on autofill later?")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `On confirm click on TurnOnLaterDialog, sends action to turn on later`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = SetupAutoFillDialogState.TurnOnLaterDialog,
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(isDialog())
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Confirm")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify { viewModel.trySendAction(SetupAutoFillAction.TurnOnLaterConfirmClick) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `When turn on later dialog is dismissed, sends action to dismiss dialog and is removed when state is null`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = SetupAutoFillDialogState.TurnOnLaterDialog,
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(isDialog())
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(SetupAutoFillAction.DismissDialog) }
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = null,
|
||||
)
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE = SetupAutoFillState(dialogState = null, autofillEnabled = false)
|
Loading…
Add table
Reference in a new issue