mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
PM-12733: Add error dialog to be displayed if TOTP code is blank (#4345)
This commit is contained in:
parent
ec8e934bf4
commit
96bd25eae5
5 changed files with 237 additions and 49 deletions
|
@ -20,43 +20,43 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a Bitwarden-styled dialog that is hidden or shown based on [visibilityState].
|
* Represents a Bitwarden-styled dialog.
|
||||||
*
|
*
|
||||||
* @param visibilityState the [BasicDialogState] used to populate the dialog.
|
* @param title The optional title to be displayed by the dialog.
|
||||||
* @param onDismissRequest called when the user has requested to dismiss the dialog, whether by
|
* @param message The message to be displayed under the [title] by the dialog.
|
||||||
* tapping "OK", tapping outside the dialog, or pressing the back button.
|
* @param onDismissRequest A lambda that is invoked when the user has requested to dismiss the
|
||||||
|
* dialog, whether by tapping "OK", tapping outside the dialog, or pressing the back button.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BitwardenBasicDialog(
|
fun BitwardenBasicDialog(
|
||||||
visibilityState: BasicDialogState,
|
title: String?,
|
||||||
|
message: String,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
): Unit = when (visibilityState) {
|
) {
|
||||||
BasicDialogState.Hidden -> Unit
|
|
||||||
is BasicDialogState.Shown -> {
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
BitwardenTextButton(
|
BitwardenTextButton(
|
||||||
label = stringResource(id = R.string.ok),
|
label = stringResource(id = R.string.ok),
|
||||||
onClick = onDismissRequest,
|
onClick = onDismissRequest,
|
||||||
modifier = Modifier.testTag("AcceptAlertButton"),
|
modifier = Modifier.testTag(tag = "AcceptAlertButton"),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
title = visibilityState.title?.let {
|
title = title?.let {
|
||||||
{
|
{
|
||||||
Text(
|
Text(
|
||||||
text = it(),
|
text = it,
|
||||||
style = BitwardenTheme.typography.headlineSmall,
|
style = BitwardenTheme.typography.headlineSmall,
|
||||||
modifier = Modifier.testTag("AlertTitleText"),
|
modifier = Modifier.testTag(tag = "AlertTitleText"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
text = visibilityState.message(),
|
text = message,
|
||||||
style = BitwardenTheme.typography.bodyMedium,
|
style = BitwardenTheme.typography.bodyMedium,
|
||||||
modifier = Modifier.testTag("AlertContentText"),
|
modifier = Modifier.testTag(tag = "AlertContentText"),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
shape = BitwardenTheme.shapes.dialog,
|
shape = BitwardenTheme.shapes.dialog,
|
||||||
|
@ -69,6 +69,27 @@ fun BitwardenBasicDialog(
|
||||||
testTag = "AlertPopup"
|
testTag = "AlertPopup"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Bitwarden-styled dialog that is hidden or shown based on [visibilityState].
|
||||||
|
*
|
||||||
|
* @param visibilityState the [BasicDialogState] used to populate the dialog.
|
||||||
|
* @param onDismissRequest called when the user has requested to dismiss the dialog, whether by
|
||||||
|
* tapping "OK", tapping outside the dialog, or pressing the back button.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun BitwardenBasicDialog(
|
||||||
|
visibilityState: BasicDialogState,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
): Unit = when (visibilityState) {
|
||||||
|
BasicDialogState.Hidden -> Unit
|
||||||
|
is BasicDialogState.Shown -> {
|
||||||
|
BitwardenBasicDialog(
|
||||||
|
title = visibilityState.title?.invoke(),
|
||||||
|
message = visibilityState.message(),
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ 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.components.appbar.BitwardenTopAppBar
|
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||||
|
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.dialog.BitwardenTwoButtonDialog
|
||||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||||
|
@ -108,6 +109,13 @@ fun ManualCodeEntryScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ManualCodeEntryDialogs(
|
||||||
|
state = state.dialog,
|
||||||
|
onDismissRequest = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(ManualCodeEntryAction.DialogDismiss) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
BitwardenScaffold(
|
BitwardenScaffold(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
topBar = {
|
topBar = {
|
||||||
|
@ -200,3 +208,21 @@ fun ManualCodeEntryScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ManualCodeEntryDialogs(
|
||||||
|
state: ManualCodeEntryState.DialogState?,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
) {
|
||||||
|
when (state) {
|
||||||
|
is ManualCodeEntryState.DialogState.Error -> {
|
||||||
|
BitwardenBasicDialog(
|
||||||
|
title = state.title?.invoke(),
|
||||||
|
message = state.message(),
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,10 +2,12 @@ package com.x8bit.bitwarden.ui.vault.feature.manualcodeentry
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
@ -23,13 +25,17 @@ class ManualCodeEntryViewModel @Inject constructor(
|
||||||
private val vaultRepository: VaultRepository,
|
private val vaultRepository: VaultRepository,
|
||||||
) : BaseViewModel<ManualCodeEntryState, ManualCodeEntryEvent, ManualCodeEntryAction>(
|
) : BaseViewModel<ManualCodeEntryState, ManualCodeEntryEvent, ManualCodeEntryAction>(
|
||||||
initialState = savedStateHandle[KEY_STATE]
|
initialState = savedStateHandle[KEY_STATE]
|
||||||
?: ManualCodeEntryState(code = ""),
|
?: ManualCodeEntryState(
|
||||||
|
code = "",
|
||||||
|
dialog = null,
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
override fun handleAction(action: ManualCodeEntryAction) {
|
override fun handleAction(action: ManualCodeEntryAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is ManualCodeEntryAction.CloseClick -> handleCloseClick()
|
is ManualCodeEntryAction.CloseClick -> handleCloseClick()
|
||||||
is ManualCodeEntryAction.CodeTextChange -> handleCodeTextChange(action)
|
is ManualCodeEntryAction.CodeTextChange -> handleCodeTextChange(action)
|
||||||
is ManualCodeEntryAction.CodeSubmit -> handleCodeSubmit()
|
is ManualCodeEntryAction.CodeSubmit -> handleCodeSubmit()
|
||||||
|
ManualCodeEntryAction.DialogDismiss -> handleDialogDismiss()
|
||||||
is ManualCodeEntryAction.ScanQrCodeTextClick -> handleScanQrCodeTextClick()
|
is ManualCodeEntryAction.ScanQrCodeTextClick -> handleScanQrCodeTextClick()
|
||||||
is ManualCodeEntryAction.SettingsClick -> handleSettingsClick()
|
is ManualCodeEntryAction.SettingsClick -> handleSettingsClick()
|
||||||
}
|
}
|
||||||
|
@ -46,10 +52,26 @@ class ManualCodeEntryViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCodeSubmit() {
|
private fun handleCodeSubmit() {
|
||||||
vaultRepository.emitTotpCodeResult(TotpCodeResult.Success(state.code.trim()))
|
val code = state.code.trim()
|
||||||
|
if (code.isEmpty()) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialog = ManualCodeEntryState.DialogState.Error(
|
||||||
|
title = R.string.an_error_has_occurred.asText(),
|
||||||
|
message = R.string.authenticator_key_read_error.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vaultRepository.emitTotpCodeResult(TotpCodeResult.Success(code))
|
||||||
sendEvent(ManualCodeEntryEvent.NavigateBack)
|
sendEvent(ManualCodeEntryEvent.NavigateBack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleDialogDismiss() {
|
||||||
|
mutableStateFlow.update { it.copy(dialog = null) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleScanQrCodeTextClick() {
|
private fun handleScanQrCodeTextClick() {
|
||||||
sendEvent(ManualCodeEntryEvent.NavigateToQrCodeScreen)
|
sendEvent(ManualCodeEntryEvent.NavigateToQrCodeScreen)
|
||||||
}
|
}
|
||||||
|
@ -65,7 +87,22 @@ class ManualCodeEntryViewModel @Inject constructor(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ManualCodeEntryState(
|
data class ManualCodeEntryState(
|
||||||
val code: String,
|
val code: String,
|
||||||
) : Parcelable
|
val dialog: DialogState?,
|
||||||
|
) : Parcelable {
|
||||||
|
/**
|
||||||
|
* Represents the current state of any dialogs on the screen.
|
||||||
|
*/
|
||||||
|
sealed class DialogState : Parcelable {
|
||||||
|
/**
|
||||||
|
* Represents a dismissible dialog with the given error [message].
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Error(
|
||||||
|
val title: Text?,
|
||||||
|
val message: Text,
|
||||||
|
) : DialogState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Models events for the [ManualCodeEntryScreen].
|
* Models events for the [ManualCodeEntryScreen].
|
||||||
|
@ -113,6 +150,11 @@ sealed class ManualCodeEntryAction {
|
||||||
*/
|
*/
|
||||||
data class CodeTextChange(val code: String) : ManualCodeEntryAction()
|
data class CodeTextChange(val code: String) : ManualCodeEntryAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User dismissed the dialog.
|
||||||
|
*/
|
||||||
|
data object DialogDismiss : ManualCodeEntryAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The text to switch to QR code scanning is clicked.
|
* The text to switch to QR code scanning is clicked.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -15,10 +15,13 @@ import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.compose.ui.test.performTextInput
|
import androidx.compose.ui.test.performTextInput
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
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.asText
|
||||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
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 com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.slot
|
import io.mockk.slot
|
||||||
|
@ -36,8 +39,7 @@ class ManualCodeEntryScreenTests : BaseComposeTest() {
|
||||||
private var onNavigateToScanQrCodeCalled = false
|
private var onNavigateToScanQrCodeCalled = false
|
||||||
|
|
||||||
private val mutableEventFlow = bufferedMutableSharedFlow<ManualCodeEntryEvent>()
|
private val mutableEventFlow = bufferedMutableSharedFlow<ManualCodeEntryEvent>()
|
||||||
private val mutableStateFlow =
|
private val mutableStateFlow = MutableStateFlow<ManualCodeEntryState>(DEFAULT_STATE)
|
||||||
MutableStateFlow(ManualCodeEntryState(""))
|
|
||||||
|
|
||||||
private val fakePermissionManager: FakePermissionManager = FakePermissionManager()
|
private val fakePermissionManager: FakePermissionManager = FakePermissionManager()
|
||||||
private val intentManager = mockk<IntentManager>(relaxed = true)
|
private val intentManager = mockk<IntentManager>(relaxed = true)
|
||||||
|
@ -131,6 +133,59 @@ class ManualCodeEntryScreenTests : BaseComposeTest() {
|
||||||
.assertIsNotDisplayed()
|
.assertIsNotDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `error dialog should be updated according to state`() {
|
||||||
|
composeTestRule.assertNoDialogExists()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialog = ManualCodeEntryState.DialogState.Error(
|
||||||
|
title = R.string.an_error_has_occurred.asText(),
|
||||||
|
message = R.string.authenticator_key_read_error.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "An error has occurred.")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Cannot read authenticator key.")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(dialog = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.assertNoDialogExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `error dialog Ok click should emit DialogDismiss`() {
|
||||||
|
composeTestRule.assertNoDialogExists()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialog = ManualCodeEntryState.DialogState.Error(
|
||||||
|
title = R.string.an_error_has_occurred.asText(),
|
||||||
|
message = R.string.authenticator_key_read_error.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Ok")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
viewModel.trySendAction(ManualCodeEntryAction.DialogDismiss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `settings dialog should call SettingsClick action on confirm click`() {
|
fun `settings dialog should call SettingsClick action on confirm click`() {
|
||||||
fakePermissionManager.checkPermissionResult = false
|
fakePermissionManager.checkPermissionResult = false
|
||||||
|
@ -202,3 +257,8 @@ class ManualCodeEntryScreenTests : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val DEFAULT_STATE: ManualCodeEntryState = ManualCodeEntryState(
|
||||||
|
code = "",
|
||||||
|
dialog = null,
|
||||||
|
)
|
||||||
|
|
|
@ -2,10 +2,12 @@ package com.x8bit.bitwarden.ui.vault.feature.manualcodeentry
|
||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
|
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.data.vault.repository.VaultRepository
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
@ -26,7 +28,7 @@ class ManualCodeEntryViewModelTests : BaseViewModelTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `CloseClick should emit NavigateBack`() = runTest {
|
fun `CloseClick should emit NavigateBack`() = runTest {
|
||||||
val viewModel = createViewModel(initialState = ManualCodeEntryState(""))
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.trySendAction(ManualCodeEntryAction.CloseClick)
|
viewModel.trySendAction(ManualCodeEntryAction.CloseClick)
|
||||||
|
@ -34,9 +36,30 @@ class ManualCodeEntryViewModelTests : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `CodeSubmit wihh blank code should display error dialog`() {
|
||||||
|
val initialState = DEFAULT_STATE.copy(code = " ")
|
||||||
|
val viewModel = createViewModel(initialState = initialState)
|
||||||
|
|
||||||
|
viewModel.trySendAction(ManualCodeEntryAction.CodeSubmit)
|
||||||
|
|
||||||
|
verify(exactly = 0) {
|
||||||
|
vaultRepository.emitTotpCodeResult(TotpCodeResult.Success("TestCode"))
|
||||||
|
}
|
||||||
|
assertEquals(
|
||||||
|
initialState.copy(
|
||||||
|
dialog = ManualCodeEntryState.DialogState.Error(
|
||||||
|
title = R.string.an_error_has_occurred.asText(),
|
||||||
|
message = R.string.authenticator_key_read_error.asText(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `CodeSubmit should emit new code and NavigateBack`() = runTest {
|
fun `CodeSubmit should emit new code and NavigateBack`() = runTest {
|
||||||
val viewModel = createViewModel(initialState = ManualCodeEntryState(" TestCode "))
|
val viewModel = createViewModel(initialState = DEFAULT_STATE.copy(code = " TestCode "))
|
||||||
|
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.trySendAction(ManualCodeEntryAction.CodeSubmit)
|
viewModel.trySendAction(ManualCodeEntryAction.CodeSubmit)
|
||||||
|
@ -50,20 +73,29 @@ class ManualCodeEntryViewModelTests : BaseViewModelTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `CodeTextChange should update state with new value`() = runTest {
|
fun `CodeTextChange should update state with new value`() = runTest {
|
||||||
val viewModel =
|
val viewModel = createViewModel(initialState = DEFAULT_STATE.copy(code = "TestCode"))
|
||||||
createViewModel(initialState = ManualCodeEntryState("TestCode"))
|
|
||||||
|
|
||||||
val expectedState = ManualCodeEntryState("NewCode")
|
val expectedState = DEFAULT_STATE.copy(code = "NewCode")
|
||||||
|
|
||||||
viewModel.trySendAction(ManualCodeEntryAction.CodeTextChange("NewCode"))
|
viewModel.trySendAction(ManualCodeEntryAction.CodeTextChange("NewCode"))
|
||||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `SettingsClick should emit NavigateToAppSettings and update state`() = runTest {
|
fun `DialogDismiss should clear the dialog state`() = runTest {
|
||||||
val viewModel = createViewModel(initialState = ManualCodeEntryState(""))
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
val expectedState = ManualCodeEntryState("")
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(ManualCodeEntryAction.CloseClick)
|
||||||
|
assertEquals(ManualCodeEntryEvent.NavigateBack, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SettingsClick should emit NavigateToAppSettings and update state`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
val expectedState = DEFAULT_STATE
|
||||||
|
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.trySendAction(ManualCodeEntryAction.SettingsClick)
|
viewModel.trySendAction(ManualCodeEntryAction.SettingsClick)
|
||||||
|
@ -75,7 +107,7 @@ class ManualCodeEntryViewModelTests : BaseViewModelTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ScanQrTextCodeClick should emit NavigateToQrCodeScreen`() = runTest {
|
fun `ScanQrTextCodeClick should emit NavigateToQrCodeScreen`() = runTest {
|
||||||
val viewModel = createViewModel(initialState = ManualCodeEntryState(""))
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.trySendAction(ManualCodeEntryAction.ScanQrCodeTextClick)
|
viewModel.trySendAction(ManualCodeEntryAction.ScanQrCodeTextClick)
|
||||||
|
@ -84,7 +116,9 @@ class ManualCodeEntryViewModelTests : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createViewModel(initialState: ManualCodeEntryState): ManualCodeEntryViewModel =
|
private fun createViewModel(
|
||||||
|
initialState: ManualCodeEntryState? = null,
|
||||||
|
): ManualCodeEntryViewModel =
|
||||||
ManualCodeEntryViewModel(
|
ManualCodeEntryViewModel(
|
||||||
vaultRepository = vaultRepository,
|
vaultRepository = vaultRepository,
|
||||||
savedStateHandle = SavedStateHandle(
|
savedStateHandle = SavedStateHandle(
|
||||||
|
@ -92,3 +126,8 @@ class ManualCodeEntryViewModelTests : BaseViewModelTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val DEFAULT_STATE: ManualCodeEntryState = ManualCodeEntryState(
|
||||||
|
code = "",
|
||||||
|
dialog = null,
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue