mirror of
https://github.com/bitwarden/android.git
synced 2025-02-17 12:30:00 +03:00
BIT-1488: Track changes in autofill status and send users to settings (#663)
This commit is contained in:
parent
3851f88828
commit
94108bcb5d
11 changed files with 324 additions and 7 deletions
|
@ -1,5 +1,7 @@
|
|||
package com.x8bit.bitwarden.data.autofill.di
|
||||
|
||||
import android.content.Context
|
||||
import android.view.autofill.AutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FillResponseBuilder
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FillResponseBuilderImpl
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilder
|
||||
|
@ -14,7 +16,9 @@ import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
|||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides dependencies within the autofill package.
|
||||
|
@ -22,6 +26,13 @@ import dagger.hilt.components.SingletonComponent
|
|||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AutofillModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAutofillManager(
|
||||
@ApplicationContext context: Context,
|
||||
): AutofillManager = context.getSystemService(AutofillManager::class.java)
|
||||
|
||||
@Provides
|
||||
fun providesAutofillParser(): AutofillParser = AutofillParserImpl()
|
||||
|
||||
|
|
|
@ -67,6 +67,16 @@ interface SettingsRepository {
|
|||
*/
|
||||
var isApprovePasswordlessLoginsEnabled: Boolean
|
||||
|
||||
/**
|
||||
* Emits updates whenever there is a change in the app's status for supporting autofill.
|
||||
*/
|
||||
val isAutofillEnabledStateFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Disables autofill if it is currently enabled.
|
||||
*/
|
||||
fun disableAutofill()
|
||||
|
||||
/**
|
||||
* Sets default values for various settings for the given [userId] if necessary. This is
|
||||
* typically used when logging into a new account.
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
import android.view.autofill.AutofillManager
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
|
@ -12,7 +14,10 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
@ -21,6 +26,8 @@ import kotlinx.coroutines.launch
|
|||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class SettingsRepositoryImpl(
|
||||
private val autofillManager: AutofillManager,
|
||||
private val appForegroundManager: AppForegroundManager,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
|
@ -30,6 +37,13 @@ class SettingsRepositoryImpl(
|
|||
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
private val isAutofillEnabledAndSupported: Boolean
|
||||
get() = autofillManager.isEnabled &&
|
||||
autofillManager.hasEnabledAutofillServices() &&
|
||||
autofillManager.isAutofillSupported
|
||||
|
||||
private val mutableIsAutofillEnabledStateFlow = MutableStateFlow(isAutofillEnabledAndSupported)
|
||||
|
||||
override var appLanguage: AppLanguage
|
||||
get() = settingsDiskSource.appLanguage ?: AppLanguage.DEFAULT
|
||||
set(value) {
|
||||
|
@ -134,6 +148,20 @@ class SettingsRepositoryImpl(
|
|||
isApprovePasswordlessLoginsEnabled = value,
|
||||
)
|
||||
}
|
||||
override val isAutofillEnabledStateFlow: StateFlow<Boolean> =
|
||||
mutableIsAutofillEnabledStateFlow.asStateFlow()
|
||||
|
||||
init {
|
||||
observeAutofillEnabledChanges()
|
||||
}
|
||||
|
||||
override fun disableAutofill() {
|
||||
autofillManager.disableAutofillServices()
|
||||
|
||||
// Manually indicate that autofill is no longer supported without needing a foreground state
|
||||
// change.
|
||||
mutableIsAutofillEnabledStateFlow.value = false
|
||||
}
|
||||
|
||||
override fun setDefaultsIfNecessary(userId: String) {
|
||||
// Set Vault Settings defaults
|
||||
|
@ -260,6 +288,15 @@ class SettingsRepositoryImpl(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeAutofillEnabledChanges() {
|
||||
appForegroundManager
|
||||
.appForegroundStateFlow
|
||||
.onEach {
|
||||
mutableIsAutofillEnabledStateFlow.value = isAutofillEnabledAndSupported
|
||||
}
|
||||
.launchIn(unconfinedScope)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package com.x8bit.bitwarden.data.platform.repository.di
|
||||
|
||||
import android.view.autofill.AutofillManager
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl
|
||||
|
@ -38,12 +40,16 @@ object PlatformRepositoryModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideSettingsRepository(
|
||||
autofillManager: AutofillManager,
|
||||
appForegroundManager: AppForegroundManager,
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): SettingsRepository =
|
||||
SettingsRepositoryImpl(
|
||||
autofillManager = autofillManager,
|
||||
appForegroundManager = appForegroundManager,
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
|
|
|
@ -30,6 +30,9 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionDialog
|
||||
|
@ -37,6 +40,8 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionRow
|
|||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextRow
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
|
||||
|
||||
/**
|
||||
* Displays the auto-fill screen.
|
||||
|
@ -47,20 +52,38 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
|||
fun AutoFillScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: AutoFillViewModel = hiltViewModel(),
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsState()
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
var shouldShowAutofillFallbackDialog by rememberSaveable { mutableStateOf(false) }
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
AutoFillEvent.NavigateBack -> onNavigateBack.invoke()
|
||||
|
||||
AutoFillEvent.NavigateToAutofillSettings -> {
|
||||
val isSuccess = intentManager.startSystemAutofillSettingsActivity()
|
||||
|
||||
shouldShowAutofillFallbackDialog = !isSuccess
|
||||
}
|
||||
|
||||
is AutoFillEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.text(resources), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldShowAutofillFallbackDialog) {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = null,
|
||||
message = R.string.bitwarden_autofill_go_to_settings.asText(),
|
||||
),
|
||||
onDismissRequest = { shouldShowAutofillFallbackDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
|
|
|
@ -10,6 +10,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.Text
|
|||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
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 kotlinx.parcelize.Parcelize
|
||||
|
@ -29,7 +30,7 @@ class AutoFillViewModel @Inject constructor(
|
|||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: AutoFillState(
|
||||
isAskToAddLoginEnabled = false,
|
||||
isAutoFillServicesEnabled = false,
|
||||
isAutoFillServicesEnabled = settingsRepository.isAutofillEnabledStateFlow.value,
|
||||
isCopyTotpAutomaticallyEnabled = false,
|
||||
isUseInlineAutoFillEnabled = settingsRepository.isInlineAutofillEnabled,
|
||||
uriDetectionMethod = AutoFillState.UriDetectionMethod.DEFAULT,
|
||||
|
@ -40,6 +41,14 @@ class AutoFillViewModel @Inject constructor(
|
|||
stateFlow
|
||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
settingsRepository
|
||||
.isAutofillEnabledStateFlow
|
||||
.map {
|
||||
AutoFillAction.Internal.AutofillEnabledUpdateReceive(isAutofillEnabled = it)
|
||||
}
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: AutoFillAction): Unit = when (action) {
|
||||
|
@ -49,6 +58,9 @@ class AutoFillViewModel @Inject constructor(
|
|||
is AutoFillAction.CopyTotpAutomaticallyClick -> handleCopyTotpAutomaticallyClick(action)
|
||||
is AutoFillAction.UriDetectionMethodSelect -> handleUriDetectionMethodSelect(action)
|
||||
is AutoFillAction.UseInlineAutofillClick -> handleUseInlineAutofillClick(action)
|
||||
is AutoFillAction.Internal.AutofillEnabledUpdateReceive -> {
|
||||
handleAutofillEnabledUpdateReceive(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAskToAddLoginClick(action: AutoFillAction.AskToAddLoginClick) {
|
||||
|
@ -58,9 +70,11 @@ class AutoFillViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleAutoFillServicesClick(action: AutoFillAction.AutoFillServicesClick) {
|
||||
// TODO BIT-828: Persist selection
|
||||
sendEvent(AutoFillEvent.ShowToast("Not yet implemented.".asText()))
|
||||
mutableStateFlow.update { it.copy(isAutoFillServicesEnabled = action.isEnabled) }
|
||||
if (action.isEnabled) {
|
||||
sendEvent(AutoFillEvent.NavigateToAutofillSettings)
|
||||
} else {
|
||||
settingsRepository.disableAutofill()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBackClick() {
|
||||
|
@ -87,6 +101,14 @@ class AutoFillViewModel @Inject constructor(
|
|||
it.copy(uriDetectionMethod = action.uriDetectionMethod)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAutofillEnabledUpdateReceive(
|
||||
action: AutoFillAction.Internal.AutofillEnabledUpdateReceive,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isAutoFillServicesEnabled = action.isAutofillEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -123,6 +145,11 @@ sealed class AutoFillEvent {
|
|||
*/
|
||||
data object NavigateBack : AutoFillEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the system autofill settings selection screen.
|
||||
*/
|
||||
data object NavigateToAutofillSettings : AutoFillEvent()
|
||||
|
||||
/**
|
||||
* Displays a toast with the given [Text].
|
||||
*/
|
||||
|
@ -174,4 +201,17 @@ sealed class AutoFillAction {
|
|||
data class UseInlineAutofillClick(
|
||||
val isEnabled: Boolean,
|
||||
) : AutoFillAction()
|
||||
|
||||
/**
|
||||
* Internal actions.
|
||||
*/
|
||||
sealed class Internal : AutoFillAction() {
|
||||
|
||||
/**
|
||||
* An update for changes in the [isAutofillEnabled] value.
|
||||
*/
|
||||
data class AutofillEnabledUpdateReceive(
|
||||
val isAutofillEnabled: Boolean,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,12 @@ interface IntentManager {
|
|||
*/
|
||||
fun startCustomTabsActivity(uri: Uri)
|
||||
|
||||
/**
|
||||
* Attempts to start the system autofill settings activity. The return value indicates whether
|
||||
* or not this was successful.
|
||||
*/
|
||||
fun startSystemAutofillSettingsActivity(): Boolean
|
||||
|
||||
/**
|
||||
* Start an activity to view the given [uri] in an external browser.
|
||||
*/
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package com.x8bit.bitwarden.ui.platform.manager.intent
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResult
|
||||
|
@ -79,6 +81,18 @@ class IntentManagerImpl(
|
|||
.launchUrl(context, uri)
|
||||
}
|
||||
|
||||
override fun startSystemAutofillSettingsActivity(): Boolean =
|
||||
try {
|
||||
val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
|
||||
.apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
}
|
||||
context.startActivity(intent)
|
||||
true
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
false
|
||||
}
|
||||
|
||||
override fun launchUri(uri: Uri) {
|
||||
val newUri = if (uri.scheme == null) {
|
||||
uri.buildUpon().scheme("https").build()
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
import android.view.autofill.AutofillManager
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.DerivePinKeyResponse
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
|
@ -14,7 +17,12 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLang
|
|||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
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
|
||||
|
@ -23,11 +31,25 @@ import org.junit.jupiter.api.Assertions.assertTrue
|
|||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SettingsRepositoryTest {
|
||||
private val autofillManager: AutofillManager = mockk {
|
||||
every { hasEnabledAutofillServices() } answers { isAutofillEnabledAndSupported }
|
||||
every { isAutofillSupported } answers { isAutofillEnabledAndSupported }
|
||||
every { isEnabled } answers { isAutofillEnabledAndSupported }
|
||||
every { disableAutofillServices() } just runs
|
||||
}
|
||||
private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED)
|
||||
private val appForegroundManager: AppForegroundManager = mockk {
|
||||
every { appForegroundStateFlow } returns mutableAppForegroundStateFlow
|
||||
}
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
private val fakeSettingsDiskSource = FakeSettingsDiskSource()
|
||||
private val vaultSdkSource: VaultSdkSource = mockk()
|
||||
|
||||
private var isAutofillEnabledAndSupported = false
|
||||
|
||||
private val settingsRepository = SettingsRepositoryImpl(
|
||||
autofillManager = autofillManager,
|
||||
appForegroundManager = appForegroundManager,
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
settingsDiskSource = fakeSettingsDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
|
@ -389,6 +411,53 @@ class SettingsRepositoryTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `isAutofillEnabledStateFlow should emit updates if necessary when the app foreground state changes and disableAutofill is called`() =
|
||||
runTest {
|
||||
settingsRepository.isAutofillEnabledStateFlow.test {
|
||||
assertFalse(awaitItem())
|
||||
|
||||
// An update is received when both the autofill state and foreground state change
|
||||
isAutofillEnabledAndSupported = true
|
||||
mutableAppForegroundStateFlow.value = AppForegroundState.FOREGROUNDED
|
||||
assertTrue(awaitItem())
|
||||
|
||||
// An update is not received when only the foreground state changes
|
||||
mutableAppForegroundStateFlow.value = AppForegroundState.BACKGROUNDED
|
||||
expectNoEvents()
|
||||
|
||||
// An update is not received when only the autofill state changes
|
||||
isAutofillEnabledAndSupported = false
|
||||
expectNoEvents()
|
||||
|
||||
// An update is received after both states have changed
|
||||
mutableAppForegroundStateFlow.value = AppForegroundState.FOREGROUNDED
|
||||
assertFalse(awaitItem())
|
||||
|
||||
// Calling disableAutofill will result in an emission of false
|
||||
isAutofillEnabledAndSupported = true
|
||||
mutableAppForegroundStateFlow.value = AppForegroundState.BACKGROUNDED
|
||||
assertTrue(awaitItem())
|
||||
settingsRepository.disableAutofill()
|
||||
assertFalse(awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `disableAutofill should trigger an emission of false from isAutofillEnabledStateFlow and disable autofill with the OS`() {
|
||||
// Start in a state where autofill is enabled
|
||||
isAutofillEnabledAndSupported = true
|
||||
mutableAppForegroundStateFlow.value = AppForegroundState.FOREGROUNDED
|
||||
assertTrue(settingsRepository.isAutofillEnabledStateFlow.value)
|
||||
|
||||
settingsRepository.disableAutofill()
|
||||
|
||||
assertFalse(settingsRepository.isAutofillEnabledStateFlow.value)
|
||||
verify { autofillManager.disableAutofillServices() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPullToRefreshEnabledFlow should react to changes in SettingsDiskSource`() = runTest {
|
||||
val userId = "userId"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings.autofill
|
||||
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsOff
|
||||
import androidx.compose.ui.test.assertIsOn
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
|
@ -13,6 +14,8 @@ 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
|
||||
|
@ -24,6 +27,7 @@ import org.junit.Test
|
|||
|
||||
class AutoFillScreenTest : BaseComposeTest() {
|
||||
|
||||
private var isSystemSettingsRequestSuccess = false
|
||||
private var onNavigateBackCalled = false
|
||||
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<AutoFillEvent>()
|
||||
|
@ -32,6 +36,9 @@ class AutoFillScreenTest : BaseComposeTest() {
|
|||
every { eventFlow } returns mutableEventFlow
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
}
|
||||
private val intentManager: IntentManager = mockk {
|
||||
every { startSystemAutofillSettingsActivity() } answers { isSystemSettingsRequestSuccess }
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
|
@ -39,10 +46,63 @@ class AutoFillScreenTest : BaseComposeTest() {
|
|||
AutoFillScreen(
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
viewModel = viewModel,
|
||||
intentManager = intentManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on NavigateToAutofillSettings should attempt to navigate to system settings and not show the fallback dialog when result is a success`() {
|
||||
isSystemSettingsRequestSuccess = true
|
||||
|
||||
mutableEventFlow.tryEmit(AutoFillEvent.NavigateToAutofillSettings)
|
||||
|
||||
verify {
|
||||
intentManager.startSystemAutofillSettingsActivity()
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on NavigateToAutofillSettings should attempt to navigate to system settings and show the fallback dialog when result is not a success`() {
|
||||
isSystemSettingsRequestSuccess = false
|
||||
|
||||
mutableEventFlow.tryEmit(AutoFillEvent.NavigateToAutofillSettings)
|
||||
|
||||
verify {
|
||||
intentManager.startSystemAutofillSettingsActivity()
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
"We were unable to automatically open the Android autofill settings menu for " +
|
||||
"you. You can navigate to the autofill settings menu manually from Android " +
|
||||
"Settings>System>Languages and input>Advanced>Autofill service.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Ok")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on autofill settings fallback dialog Ok click should dismiss the dialog`() {
|
||||
isSystemSettingsRequestSuccess = false
|
||||
mutableEventFlow.tryEmit(AutoFillEvent.NavigateToAutofillSettings)
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Ok")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on auto fill services toggle should send AutoFillServicesClick`() {
|
||||
composeTestRule
|
||||
|
|
|
@ -10,15 +10,19 @@ 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.Test
|
||||
|
||||
class AutoFillViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val mutableIsAutofillEnabledStateFlow = MutableStateFlow(false)
|
||||
private val settingsRepository: SettingsRepository = mockk() {
|
||||
every { isInlineAutofillEnabled } returns true
|
||||
every { isInlineAutofillEnabled = any() } just runs
|
||||
every { isAutofillEnabledStateFlow } returns mutableIsAutofillEnabledStateFlow
|
||||
every { disableAutofill() } just runs
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -29,6 +33,7 @@ class AutoFillViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `initial state should be correct when set`() {
|
||||
mutableIsAutofillEnabledStateFlow.value = true
|
||||
val state = DEFAULT_STATE.copy(
|
||||
isAutoFillServicesEnabled = true,
|
||||
uriDetectionMethod = AutoFillState.UriDetectionMethod.REGULAR_EXPRESSION,
|
||||
|
@ -37,6 +42,26 @@ class AutoFillViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(state, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `changes in autofill enabled status should update the state`() {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
|
||||
mutableIsAutofillEnabledStateFlow.value = true
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(isAutoFillServicesEnabled = true),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
mutableIsAutofillEnabledStateFlow.value = false
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(isAutoFillServicesEnabled = false),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on AskToAddLoginClick should emit ShowToast`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
@ -51,14 +76,30 @@ class AutoFillViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on AutoFillServicesClick should emit ShowToast`() = runTest {
|
||||
fun `on AutoFillServicesClick with false should disable autofill`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(AutoFillAction.AutoFillServicesClick(false))
|
||||
verify {
|
||||
settingsRepository.disableAutofill()
|
||||
}
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on AutoFillServicesClick with true should emit NavigateToAutofillSettings`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AutoFillAction.AutoFillServicesClick(true))
|
||||
assertEquals(AutoFillEvent.ShowToast("Not yet implemented.".asText()), awaitItem())
|
||||
assertEquals(
|
||||
AutoFillEvent.NavigateToAutofillSettings,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(isAutoFillServicesEnabled = true),
|
||||
DEFAULT_STATE,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue