BIT-927: Add auto-fill UI (#208)

This commit is contained in:
David Perez
2023-11-06 16:00:28 -06:00
committed by GitHub
parent d5879f1cd4
commit a9068b6937
8 changed files with 754 additions and 39 deletions

View File

@@ -1,46 +1,233 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.autofill
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn
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.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class AutoFillScreenTest : BaseComposeTest() {
@Test
fun `on back click should send BackClick`() {
val viewModel: AutoFillViewModel = mockk {
every { eventFlow } returns emptyFlow()
every { trySendAction(AutoFillAction.BackClick) } returns Unit
}
private var onNavigateBackCalled = false
private val mutableEventFlow = MutableSharedFlow<AutoFillEvent>(
extraBufferCapacity = Int.MAX_VALUE,
)
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val viewModel = mockk<AutoFillViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow
}
@Before
fun setUp() {
composeTestRule.setContent {
AutoFillScreen(
onNavigateBack = { onNavigateBackCalled = true },
viewModel = viewModel,
onNavigateBack = { },
)
}
}
@Test
fun `on auto fill services toggle should send AutoFillServicesClick`() {
composeTestRule
.onNodeWithText("Auto-fill services")
.performScrollTo()
.performClick()
verify { viewModel.trySendAction(AutoFillAction.AutoFillServicesClick(true)) }
}
@Test
fun `auto fill services should be toggled on or off according to state`() {
composeTestRule
.onNodeWithText("Auto-fill services")
.performScrollTo()
.assertIsOff()
mutableStateFlow.update { it.copy(isAutoFillServicesEnabled = true) }
composeTestRule
.onNodeWithText("Auto-fill services")
.performScrollTo()
.assertIsOn()
}
@Test
fun `on use inline auto fill toggle should send UseInlineAutofillClick`() {
composeTestRule
.onNodeWithText("Use inline autofill")
.performScrollTo()
.performClick()
verify { viewModel.trySendAction(AutoFillAction.UseInlineAutofillClick(true)) }
}
@Test
fun `use inline autofill should be toggled on or off according to state`() {
composeTestRule
.onNodeWithText("Use inline autofill")
.performScrollTo()
.assertIsOff()
mutableStateFlow.update { it.copy(isUseInlineAutoFillEnabled = true) }
composeTestRule
.onNodeWithText("Use inline autofill")
.performScrollTo()
.assertIsOn()
}
@Test
fun `on use accessibility toggle should send UseAccessibilityClick`() {
composeTestRule
.onNodeWithText("Use accessibility")
.performScrollTo()
.performClick()
verify { viewModel.trySendAction(AutoFillAction.UseAccessibilityClick(true)) }
}
@Test
fun `use accessibility should be toggled on or off according to state`() {
composeTestRule
.onNodeWithText("Use accessibility")
.performScrollTo()
.assertIsOff()
mutableStateFlow.update { it.copy(isUseAccessibilityEnabled = true) }
composeTestRule
.onNodeWithText("Use accessibility")
.performScrollTo()
.assertIsOn()
}
@Test
fun `on use draw over toggle should send UseDrawOverClick`() {
composeTestRule
.onNodeWithText("Use draw-over")
.performScrollTo()
.performClick()
verify { viewModel.trySendAction(AutoFillAction.UseDrawOverClick(true)) }
}
@Test
fun `use draw-over should be toggled on or off according to state`() {
composeTestRule
.onNodeWithText("Use draw-over")
.performScrollTo()
.assertIsOff()
mutableStateFlow.update { it.copy(isUseDrawOverEnabled = true) }
composeTestRule
.onNodeWithText("Use draw-over")
.performScrollTo()
.assertIsOn()
}
@Test
fun `on copy TOTP automatically toggle should send CopyTotpAutomaticallyClick`() {
composeTestRule
.onNodeWithText("Copy TOTP automatically")
.performScrollTo()
.performClick()
verify { viewModel.trySendAction(AutoFillAction.CopyTotpAutomaticallyClick(true)) }
}
@Test
fun `copy TOTP automatically should be toggled on or off according to state`() {
composeTestRule
.onNodeWithText("Copy TOTP automatically")
.performScrollTo()
.assertIsOff()
mutableStateFlow.update { it.copy(isCopyTotpAutomaticallyEnabled = true) }
composeTestRule
.onNodeWithText("Copy TOTP automatically")
.performScrollTo()
.assertIsOn()
}
@Test
fun `on ask to add login toggle should send AskToAddLoginClick`() {
composeTestRule
.onNodeWithText("Ask to add login")
.performScrollTo()
.performClick()
verify { viewModel.trySendAction(AutoFillAction.AskToAddLoginClick(true)) }
}
@Test
fun `ask to add login should be toggled on or off according to state`() {
composeTestRule
.onNodeWithText("Ask to add login")
.performScrollTo()
.assertIsOff()
mutableStateFlow.update { it.copy(isAskToAddLoginEnabled = true) }
composeTestRule
.onNodeWithText("Ask to add login")
.performScrollTo()
.assertIsOn()
}
@Test
fun `on default URI match detection toggle should display dialog`() {
composeTestRule
.onNodeWithText("Default URI match detection")
.performScrollTo()
.assert(!hasAnyAncestor(isDialog()))
.performClick()
composeTestRule
.onAllNodesWithText("Default URI match detection")
.filterToOne(hasAnyAncestor(isDialog()))
.assertExists()
}
@Test
fun `default URI match detection add login should be updated on or off according to state`() {
composeTestRule
.onNodeWithText("Default")
.assertExists()
composeTestRule
.onNodeWithText("Starts with")
.assertDoesNotExist()
mutableStateFlow.update {
it.copy(uriDetectionMethod = AutoFillState.UriDetectionMethod.STARTS_WITH)
}
composeTestRule
.onNodeWithText("Default")
.assertDoesNotExist()
composeTestRule
.onNodeWithText("Starts with")
.assertExists()
}
@Test
fun `on back click should send BackClick`() {
composeTestRule.onNodeWithContentDescription("Back").performClick()
verify { viewModel.trySendAction(AutoFillAction.BackClick) }
}
@Test
fun `on NavigateAbout should call onNavigateToAutoFill`() {
var haveCalledNavigateBack = false
val viewModel = mockk<AutoFillViewModel> {
every { eventFlow } returns flowOf(AutoFillEvent.NavigateBack)
}
composeTestRule.setContent {
AutoFillScreen(
viewModel = viewModel,
onNavigateBack = { haveCalledNavigateBack = true },
)
}
assertTrue(haveCalledNavigateBack)
fun `on NavigateBack should call onNavigateBack`() {
mutableEventFlow.tryEmit(AutoFillEvent.NavigateBack)
assertTrue(onNavigateBackCalled)
}
}
private val DEFAULT_STATE: AutoFillState = AutoFillState(
isAskToAddLoginEnabled = false,
isAutoFillServicesEnabled = false,
isCopyTotpAutomaticallyEnabled = false,
isUseAccessibilityEnabled = false,
isUseDrawOverEnabled = false,
isUseInlineAutoFillEnabled = false,
uriDetectionMethod = AutoFillState.UriDetectionMethod.DEFAULT,
)

View File

@@ -1,19 +1,147 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.autofill
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class AutoFillViewModelTest : BaseViewModelTest() {
@Test
fun `initial state should be correct when not set`() {
val viewModel = createViewModel(state = null)
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
}
@Test
fun `initial state should be correct when set`() {
val state = DEFAULT_STATE.copy(
isAutoFillServicesEnabled = true,
uriDetectionMethod = AutoFillState.UriDetectionMethod.REGULAR_EXPRESSION,
)
val viewModel = createViewModel(state = state)
assertEquals(state, viewModel.stateFlow.value)
}
@Test
fun `on AskToAddLoginClick should emit ShowToast`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(AutoFillAction.AskToAddLoginClick(true))
assertEquals(AutoFillEvent.ShowToast("Not yet implemented.".asText()), awaitItem())
}
assertEquals(
DEFAULT_STATE.copy(isAskToAddLoginEnabled = true),
viewModel.stateFlow.value,
)
}
@Test
fun `on AutoFillServicesClick should emit ShowToast`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(AutoFillAction.AutoFillServicesClick(true))
assertEquals(AutoFillEvent.ShowToast("Not yet implemented.".asText()), awaitItem())
}
assertEquals(
DEFAULT_STATE.copy(isAutoFillServicesEnabled = true),
viewModel.stateFlow.value,
)
}
@Test
fun `on BackClick should emit NavigateBack`() = runTest {
val viewModel = AutoFillViewModel()
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(AutoFillAction.BackClick)
assertEquals(AutoFillEvent.NavigateBack, awaitItem())
}
}
@Test
fun `on CopyTotpAutomaticallyClick should update the isCopyTotpAutomaticallyEnabled state`() =
runTest {
val viewModel = createViewModel()
val isEnabled = true
viewModel.eventFlow.test {
viewModel.trySendAction(AutoFillAction.CopyTotpAutomaticallyClick(isEnabled))
assertEquals(AutoFillEvent.ShowToast("Not yet implemented.".asText()), awaitItem())
}
assertEquals(
DEFAULT_STATE.copy(isCopyTotpAutomaticallyEnabled = isEnabled),
viewModel.stateFlow.value,
)
}
@Test
fun `on UseAccessibilityClick should emit ShowToast`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(AutoFillAction.UseAccessibilityClick(true))
assertEquals(AutoFillEvent.ShowToast("Not yet implemented.".asText()), awaitItem())
}
assertEquals(
DEFAULT_STATE.copy(isUseAccessibilityEnabled = true),
viewModel.stateFlow.value,
)
}
@Test
fun `on UseDrawOverClick should emit ShowToast`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(AutoFillAction.UseDrawOverClick(true))
assertEquals(AutoFillEvent.ShowToast("Not yet implemented.".asText()), awaitItem())
}
assertEquals(
DEFAULT_STATE.copy(isUseDrawOverEnabled = true),
viewModel.stateFlow.value,
)
}
@Test
fun `on UseInlineAutofillClick should emit ShowToast`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(AutoFillAction.UseInlineAutofillClick(true))
assertEquals(AutoFillEvent.ShowToast("Not yet implemented.".asText()), awaitItem())
}
assertEquals(
DEFAULT_STATE.copy(isUseInlineAutoFillEnabled = true),
viewModel.stateFlow.value,
)
}
@Test
fun `on UriDetectionMethodSelect should emit ShowToast`() = runTest {
val viewModel = createViewModel()
val method = AutoFillState.UriDetectionMethod.EXACT
viewModel.eventFlow.test {
viewModel.trySendAction(AutoFillAction.UriDetectionMethodSelect(method))
assertEquals(AutoFillEvent.ShowToast("Not yet implemented.".asText()), awaitItem())
}
assertEquals(
DEFAULT_STATE.copy(uriDetectionMethod = method),
viewModel.stateFlow.value,
)
}
private fun createViewModel(
state: AutoFillState? = DEFAULT_STATE,
): AutoFillViewModel = AutoFillViewModel(
savedStateHandle = SavedStateHandle().apply { set("state", state) },
)
}
private val DEFAULT_STATE: AutoFillState = AutoFillState(
isAskToAddLoginEnabled = false,
isAutoFillServicesEnabled = false,
isCopyTotpAutomaticallyEnabled = false,
isUseAccessibilityEnabled = false,
isUseDrawOverEnabled = false,
isUseInlineAutoFillEnabled = false,
uriDetectionMethod = AutoFillState.UriDetectionMethod.DEFAULT,
)