BIT-289 Remove deprecated BitwardenTextField (#53)

This commit is contained in:
Andrew Haisting 2023-09-19 14:05:51 -05:00 committed by Álison Fernandes
parent 4a49781ae1
commit dd2ad70b52
5 changed files with 261 additions and 52 deletions

View file

@ -12,6 +12,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -19,16 +21,23 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
/** /**
* Top level composable for the create account screen. * Top level composable for the create account screen.
*/ */
@Suppress("LongMethod")
@Composable @Composable
fun CreateAccountScreen( fun CreateAccountScreen(
viewModel: CreateAccountViewModel = hiltViewModel(), viewModel: CreateAccountViewModel = hiltViewModel(),
) { ) {
val state by viewModel.stateFlow.collectAsState()
val context = LocalContext.current val context = LocalContext.current
EventsEffect(viewModel) { event -> EventsEffect(viewModel) { event ->
when (event) { when (event) {
@ -61,7 +70,7 @@ fun CreateAccountScreen(
Text( Text(
modifier = Modifier modifier = Modifier
.clickable { .clickable {
viewModel.trySendAction(CreateAccountAction.SubmitClick) viewModel.trySendAction(SubmitClick)
} }
.padding(16.dp), .padding(16.dp),
text = stringResource(id = R.string.submit), text = stringResource(id = R.string.submit),
@ -69,9 +78,25 @@ fun CreateAccountScreen(
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) )
} }
BitwardenTextField(label = stringResource(id = R.string.email_address)) BitwardenTextField(
BitwardenTextField(label = stringResource(id = R.string.master_password)) label = stringResource(id = R.string.email_address),
BitwardenTextField(label = stringResource(id = R.string.retype_master_password)) value = state.emailInput,
BitwardenTextField(label = stringResource(id = R.string.master_password_hint)) onValueChange = { viewModel.trySendAction(EmailInputChange(it)) },
)
BitwardenTextField(
label = stringResource(id = R.string.master_password),
value = state.passwordInput,
onValueChange = { viewModel.trySendAction(PasswordInputChange(it)) },
)
BitwardenTextField(
label = stringResource(id = R.string.retype_master_password),
value = state.confirmPasswordInput,
onValueChange = { viewModel.trySendAction(ConfirmPasswordInputChange(it)) },
)
BitwardenTextField(
label = stringResource(id = R.string.master_password_hint),
value = state.passwordHintInput,
onValueChange = { viewModel.trySendAction(PasswordHintChange(it)) },
)
} }
} }

View file

@ -1,24 +1,72 @@
package com.x8bit.bitwarden.ui.auth.feature.createaccount package com.x8bit.bitwarden.ui.auth.feature.createaccount
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize
import javax.inject.Inject import javax.inject.Inject
private const val KEY_STATE = "state"
/** /**
* Models logic for the create account screen. * Models logic for the create account screen.
*/ */
@HiltViewModel @HiltViewModel
class CreateAccountViewModel @Inject constructor() : class CreateAccountViewModel @Inject constructor(
BaseViewModel<CreateAccountState, CreateAccountEvent, CreateAccountAction>( savedStateHandle: SavedStateHandle,
initialState = CreateAccountState, ) : BaseViewModel<CreateAccountState, CreateAccountEvent, CreateAccountAction>(
initialState = savedStateHandle[KEY_STATE]
?: CreateAccountState(
emailInput = "",
passwordInput = "",
confirmPasswordInput = "",
passwordHintInput = "",
),
) { ) {
init {
// As state updates, write to saved state handle:
stateFlow
.onEach { savedStateHandle[KEY_STATE] = it }
.launchIn(viewModelScope)
}
override fun handleAction(action: CreateAccountAction) { override fun handleAction(action: CreateAccountAction) {
when (action) { when (action) {
is CreateAccountAction.SubmitClick -> handleSubmitClick() is SubmitClick -> handleSubmitClick()
is ConfirmPasswordInputChange -> handleConfirmPasswordInputChanged(action)
is EmailInputChange -> handleEmailInputChanged(action)
is PasswordHintChange -> handlePasswordHintChanged(action)
is PasswordInputChange -> handlePasswordInputChanged(action)
} }
} }
private fun handleEmailInputChanged(action: EmailInputChange) {
mutableStateFlow.update { it.copy(emailInput = action.input) }
}
private fun handlePasswordHintChanged(action: PasswordHintChange) {
mutableStateFlow.update { it.copy(passwordHintInput = action.input) }
}
private fun handlePasswordInputChanged(action: PasswordInputChange) {
mutableStateFlow.update { it.copy(passwordInput = action.input) }
}
private fun handleConfirmPasswordInputChanged(action: ConfirmPasswordInputChange) {
mutableStateFlow.update { it.copy(confirmPasswordInput = action.input) }
}
private fun handleSubmitClick() { private fun handleSubmitClick() {
sendEvent(CreateAccountEvent.ShowToast("TODO: Handle Submit Click")) sendEvent(CreateAccountEvent.ShowToast("TODO: Handle Submit Click"))
} }
@ -27,7 +75,13 @@ class CreateAccountViewModel @Inject constructor() :
/** /**
* UI state for the create account screen. * UI state for the create account screen.
*/ */
data object CreateAccountState @Parcelize
data class CreateAccountState(
val emailInput: String,
val passwordInput: String,
val confirmPasswordInput: String,
val passwordHintInput: String,
) : Parcelable
/** /**
* Models events for the create account screen. * Models events for the create account screen.
@ -48,4 +102,24 @@ sealed class CreateAccountAction {
* User clicked submit. * User clicked submit.
*/ */
data object SubmitClick : CreateAccountAction() data object SubmitClick : CreateAccountAction()
/**
* Email input changed.
*/
data class EmailInputChange(val input: String) : CreateAccountAction()
/**
* Password input changed.
*/
data class PasswordInputChange(val input: String) : CreateAccountAction()
/**
* Confirm password input changed.
*/
data class ConfirmPasswordInputChange(val input: String) : CreateAccountAction()
/**
* Password hint input changed.
*/
data class PasswordHintChange(val input: String) : CreateAccountAction()
} }

View file

@ -5,46 +5,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
/**
* Component that allows the user to input text. This composable will manage the state of
* the user's input.
*
* @param label label for the text field.
* @param initialValue initial input text.
* @param onTextChange callback that is triggered when the input of the text field changes.
*
* TODO: remove deprecated version: BIT-289
*/
@Deprecated(
message = "Use overloaded BitwardenTextField that takes an input instead of an initialText.",
)
@Composable
fun BitwardenTextField(
label: String,
initialValue: String = "",
onTextChange: (String) -> Unit = {},
) {
var input by remember { mutableStateOf(initialValue) }
TextField(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
label = { Text(text = label) },
value = input,
onValueChange = {
input = it
onTextChange.invoke(it)
},
)
}
/** /**
* Component that allows the user to input text. This composable will manage the state of * Component that allows the user to input text. This composable will manage the state of
* the user's input. * the user's input.

View file

@ -2,10 +2,17 @@ package com.x8bit.bitwarden.ui.auth.feature.createaccount
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import org.junit.Test import org.junit.Test
@ -14,13 +21,82 @@ class CreateAccountScreenTest : BaseComposeTest() {
@Test @Test
fun `submit click should send SubmitClick action`() { fun `submit click should send SubmitClick action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) { val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns emptyFlow() every { eventFlow } returns emptyFlow()
every { trySendAction(CreateAccountAction.SubmitClick) } returns Unit every { trySendAction(SubmitClick) } returns Unit
} }
composeTestRule.setContent { composeTestRule.setContent {
CreateAccountScreen(viewModel) CreateAccountScreen(viewModel)
} }
composeTestRule.onNodeWithText("Submit").performClick() composeTestRule.onNodeWithText("Submit").performClick()
verify { viewModel.trySendAction(CreateAccountAction.SubmitClick) } verify { viewModel.trySendAction(SubmitClick) }
}
@Test
fun `email input change should send EmailInputChange action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns emptyFlow()
every { trySendAction(EmailInputChange("input")) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(viewModel)
}
composeTestRule.onNodeWithText("Email address").performTextInput(TEST_INPUT)
verify { viewModel.trySendAction(EmailInputChange(TEST_INPUT)) }
}
@Test
fun `password input change should send PasswordInputChange action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns emptyFlow()
every { trySendAction(PasswordInputChange("input")) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(viewModel)
}
composeTestRule.onNodeWithText("Master password").performTextInput(TEST_INPUT)
verify { viewModel.trySendAction(PasswordInputChange(TEST_INPUT)) }
}
@Test
fun `confirm password input change should send ConfirmPasswordInputChange action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns emptyFlow()
every { trySendAction(ConfirmPasswordInputChange("input")) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(viewModel)
}
composeTestRule.onNodeWithText("Re-type master password").performTextInput(TEST_INPUT)
verify { viewModel.trySendAction(ConfirmPasswordInputChange(TEST_INPUT)) }
}
@Test
fun `password hint input change should send PasswordHintChange action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns emptyFlow()
every { trySendAction(PasswordHintChange("input")) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(viewModel)
}
composeTestRule
.onNodeWithText("Master password hint (optional)")
.performTextInput(TEST_INPUT)
verify { viewModel.trySendAction(PasswordHintChange(TEST_INPUT)) }
}
companion object {
private const val TEST_INPUT = "input"
private val DEFAULT_STATE = CreateAccountState(
emailInput = "",
passwordInput = "",
confirmPasswordInput = "",
passwordHintInput = "",
)
} }
} }

View file

@ -1,18 +1,89 @@
package com.x8bit.bitwarden.ui.auth.feature.createaccount package com.x8bit.bitwarden.ui.auth.feature.createaccount
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test import app.cash.turbine.test
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class CreateAccountViewModelTest : BaseViewModelTest() { class CreateAccountViewModelTest : BaseViewModelTest() {
@Test
fun `initial state should be correct`() {
val viewModel = CreateAccountViewModel(SavedStateHandle())
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
}
@Test
fun `initial state should pull from saved state handle when present`() {
val savedState = CreateAccountState(
emailInput = "email",
passwordInput = "password",
confirmPasswordInput = "confirmPassword",
passwordHintInput = "hint",
)
val handle = SavedStateHandle(mapOf("state" to savedState))
val viewModel = CreateAccountViewModel(handle)
assertEquals(savedState, viewModel.stateFlow.value)
}
@Test @Test
fun `SubmitClick should emit ShowToast`() = runTest { fun `SubmitClick should emit ShowToast`() = runTest {
val viewModel = CreateAccountViewModel() val viewModel = CreateAccountViewModel(SavedStateHandle())
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.actionChannel.trySend(CreateAccountAction.SubmitClick) viewModel.actionChannel.trySend(SubmitClick)
assert(awaitItem() is CreateAccountEvent.ShowToast) assert(awaitItem() is CreateAccountEvent.ShowToast)
} }
} }
@Test
fun `ConfirmPasswordInputChange update passwordInput`() = runTest {
val viewModel = CreateAccountViewModel(SavedStateHandle())
viewModel.actionChannel.trySend(ConfirmPasswordInputChange("input"))
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE.copy(confirmPasswordInput = "input"), awaitItem())
}
}
@Test
fun `EmailInputChange update passwordInput`() = runTest {
val viewModel = CreateAccountViewModel(SavedStateHandle())
viewModel.actionChannel.trySend(EmailInputChange("input"))
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE.copy(emailInput = "input"), awaitItem())
}
}
@Test
fun `PasswordHintChange update passwordInput`() = runTest {
val viewModel = CreateAccountViewModel(SavedStateHandle())
viewModel.actionChannel.trySend(PasswordHintChange("input"))
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE.copy(passwordHintInput = "input"), awaitItem())
}
}
@Test
fun `PasswordInputChange update passwordInput`() = runTest {
val viewModel = CreateAccountViewModel(SavedStateHandle())
viewModel.actionChannel.trySend(PasswordInputChange("input"))
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE.copy(passwordInput = "input"), awaitItem())
}
}
companion object {
private val DEFAULT_STATE = CreateAccountState(
passwordInput = "",
emailInput = "",
confirmPasswordInput = "",
passwordHintInput = "",
)
}
} }