Add NumberInputField
This commit is contained in:
parent
608cf6d3da
commit
440fa4957c
8 changed files with 282 additions and 142 deletions
|
@ -23,7 +23,9 @@ fun TextFieldOutlinedNumber(
|
|||
MaterialOutlinedTextField(
|
||||
value = value?.toString() ?: "",
|
||||
onValueChange = {
|
||||
onValueChange(it.toLongOrNull())
|
||||
onValueChange(
|
||||
it.takeIf { it.isNotBlank() }?.toLongOrNull(),
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
enabled = isEnabled,
|
||||
|
|
|
@ -3,8 +3,6 @@ package app.k9mail.core.ui.compose.designsystem.molecule.input
|
|||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlinedNumber
|
||||
|
@ -21,12 +19,10 @@ fun NumberInput(
|
|||
errorMessage: String? = null,
|
||||
contentPadding: PaddingValues = inputContentPadding(),
|
||||
) {
|
||||
val inputError = remember { mutableStateOf<String?>(null) }
|
||||
|
||||
InputLayout(
|
||||
modifier = modifier,
|
||||
contentPadding = contentPadding,
|
||||
errorMessage = errorMessage ?: inputError.value,
|
||||
errorMessage = errorMessage,
|
||||
) {
|
||||
TextFieldOutlinedNumber(
|
||||
value = value,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package app.k9mail.feature.account.setup.domain.input
|
||||
|
||||
import app.k9mail.core.common.domain.usecase.validation.ValidationError
|
||||
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
/**
|
||||
* InputField is an interface defining the state of an input field.
|
||||
|
@ -42,4 +43,6 @@ interface InputField<T> {
|
|||
fun hasError(): Boolean {
|
||||
return error != null
|
||||
}
|
||||
|
||||
fun updateFromValidationResult(result: ValidationResult): InputField<T>
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package app.k9mail.feature.account.setup.domain.input
|
||||
|
||||
import app.k9mail.core.common.domain.usecase.validation.ValidationError
|
||||
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
data class NumberInputField(
|
||||
override val value: Long? = null,
|
||||
override val error: ValidationError? = null,
|
||||
override val isValid: Boolean = false,
|
||||
) : InputField<Long?> {
|
||||
|
||||
override fun updateValue(value: Long?): NumberInputField {
|
||||
return NumberInputField(
|
||||
value = value,
|
||||
error = null,
|
||||
isValid = false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateError(error: ValidationError?): NumberInputField {
|
||||
return NumberInputField(
|
||||
value = value,
|
||||
error = error,
|
||||
isValid = false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateValidity(isValid: Boolean): NumberInputField {
|
||||
if (isValid == this.isValid) return this
|
||||
|
||||
return NumberInputField(
|
||||
value = value,
|
||||
error = null,
|
||||
isValid = isValid,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateFromValidationResult(result: ValidationResult): NumberInputField {
|
||||
return when (result) {
|
||||
is ValidationResult.Success -> copy(
|
||||
error = null,
|
||||
isValid = true,
|
||||
)
|
||||
|
||||
is ValidationResult.Failure -> copy(
|
||||
error = result.error,
|
||||
isValid = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -34,18 +34,18 @@ data class StringInputField(
|
|||
isValid = isValid,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun StringInputField.fromValidationResult(result: ValidationResult): StringInputField {
|
||||
return when (result) {
|
||||
is ValidationResult.Success -> copy(
|
||||
error = null,
|
||||
isValid = true,
|
||||
)
|
||||
override fun updateFromValidationResult(result: ValidationResult): StringInputField {
|
||||
return when (result) {
|
||||
is ValidationResult.Success -> copy(
|
||||
error = null,
|
||||
isValid = true,
|
||||
)
|
||||
|
||||
is ValidationResult.Failure -> copy(
|
||||
error = result.error,
|
||||
isValid = false,
|
||||
)
|
||||
is ValidationResult.Failure -> copy(
|
||||
error = result.error,
|
||||
isValid = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package app.k9mail.feature.account.setup.ui.options
|
|||
|
||||
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import app.k9mail.feature.account.setup.domain.input.fromValidationResult
|
||||
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Event.OnAccountNameChanged
|
||||
|
@ -83,9 +82,9 @@ internal class AccountOptionsViewModel(
|
|||
|
||||
updateState {
|
||||
it.copy(
|
||||
accountName = state.value.accountName.fromValidationResult(accountNameResult),
|
||||
displayName = state.value.displayName.fromValidationResult(displayNameResult),
|
||||
emailSignature = state.value.emailSignature.fromValidationResult(emailSignatureResult),
|
||||
accountName = state.value.accountName.updateFromValidationResult(accountNameResult),
|
||||
displayName = state.value.displayName.updateFromValidationResult(displayNameResult),
|
||||
emailSignature = state.value.emailSignature.updateFromValidationResult(emailSignatureResult),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
package app.k9mail.feature.account.setup.domain.input
|
||||
|
||||
import app.k9mail.core.common.domain.usecase.validation.ValidationError
|
||||
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
|
||||
import assertk.Assert
|
||||
import assertk.all
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import assertk.assertions.prop
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
data class InputFieldTestData<T>(
|
||||
val name: String,
|
||||
val initialState: InputField<T>,
|
||||
val initialValue: T,
|
||||
val initialValueEmpty: T,
|
||||
val initialError: ValidationError?,
|
||||
val initialIsValid: Boolean,
|
||||
val createInitialInput: (value: T, error: ValidationError?, isValid: Boolean) -> InputField<T>,
|
||||
val updatedValue: T,
|
||||
)
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class InputFieldTest(
|
||||
val data: InputFieldTestData<Any>,
|
||||
) {
|
||||
|
||||
@Test
|
||||
fun `should set default values`() {
|
||||
assertThat(data.initialState).all {
|
||||
hasValue(data.initialValueEmpty)
|
||||
hasNoError()
|
||||
isNotValid()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reset error and isValid when value changed`() {
|
||||
val initialInput = data.createInitialInput(
|
||||
data.initialValue,
|
||||
TestValidationError,
|
||||
true,
|
||||
)
|
||||
|
||||
val result = initialInput.updateValue(data.updatedValue)
|
||||
|
||||
assertThat(result).all {
|
||||
hasValue(data.updatedValue)
|
||||
hasNoError()
|
||||
isNotValid()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reset isValid when error set`() {
|
||||
val initialInput = data.createInitialInput(
|
||||
data.initialValue,
|
||||
null,
|
||||
true,
|
||||
)
|
||||
|
||||
val result = initialInput.updateError(TestValidationError)
|
||||
|
||||
assertThat(result).all {
|
||||
hasValue(data.initialValue)
|
||||
hasError(TestValidationError)
|
||||
isNotValid()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reset error when valid`() {
|
||||
val initialInput = data.createInitialInput(
|
||||
data.initialValue,
|
||||
TestValidationError,
|
||||
false,
|
||||
)
|
||||
|
||||
val result = initialInput.updateValidity(isValid = true)
|
||||
|
||||
assertThat(result).all {
|
||||
hasValue(data.initialValue)
|
||||
hasNoError()
|
||||
isValid()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not reset error when invalid`() {
|
||||
val initialInput = data.createInitialInput(
|
||||
data.initialValue,
|
||||
TestValidationError,
|
||||
false,
|
||||
)
|
||||
|
||||
val result = initialInput.updateValidity(isValid = false)
|
||||
|
||||
assertThat(result).all {
|
||||
hasValue(data.initialValue)
|
||||
hasError(TestValidationError)
|
||||
isNotValid()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should change error when error changed`() {
|
||||
val initialInput = data.createInitialInput(
|
||||
data.initialValue,
|
||||
TestValidationError,
|
||||
false,
|
||||
)
|
||||
|
||||
val result = initialInput.updateError(TestValidationError2)
|
||||
|
||||
assertThat(result).all {
|
||||
hasValue(data.initialValue)
|
||||
hasError(TestValidationError2)
|
||||
isNotValid()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should map from success ValidationResult`() {
|
||||
val initialInput = data.createInitialInput(
|
||||
data.initialValue,
|
||||
TestValidationError,
|
||||
false,
|
||||
)
|
||||
|
||||
val result = initialInput.updateFromValidationResult(ValidationResult.Success)
|
||||
|
||||
assertThat(result).all {
|
||||
hasValue(data.initialValue)
|
||||
hasNoError()
|
||||
isValid()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should map from failure ValidationResult`() {
|
||||
val initialInput = data.createInitialInput(
|
||||
data.initialValue,
|
||||
null,
|
||||
true,
|
||||
)
|
||||
|
||||
val result = initialInput.updateFromValidationResult(ValidationResult.Failure(TestValidationError))
|
||||
|
||||
assertThat(result).all {
|
||||
hasValue(data.initialValue)
|
||||
hasError(TestValidationError)
|
||||
isNotValid()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Assert<InputField<Any>>.hasValue(value: Any) {
|
||||
prop("value") { InputField<*>::value.call(it) }.isEqualTo(value)
|
||||
}
|
||||
|
||||
private fun Assert<InputField<Any>>.hasError(error: ValidationError) {
|
||||
prop("error") { InputField<*>::error.call(it) }.isEqualTo(error)
|
||||
}
|
||||
|
||||
private fun Assert<InputField<Any>>.hasNoError() {
|
||||
prop("error") { InputField<*>::error.call(it) }.isNull()
|
||||
}
|
||||
|
||||
private fun Assert<InputField<Any>>.isValid() {
|
||||
prop("isValid") { InputField<*>::isValid.call(it) }.isTrue()
|
||||
}
|
||||
|
||||
private fun Assert<InputField<Any>>.isNotValid() {
|
||||
prop("isValid") { InputField<*>::isValid.call(it) }.isFalse()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun data(): List<InputFieldTestData<*>> = listOf(
|
||||
InputFieldTestData(
|
||||
name = "StringInputField",
|
||||
createInitialInput = { value, error, isValid -> StringInputField(value, error, isValid) },
|
||||
initialState = StringInputField(),
|
||||
initialValue = "input",
|
||||
initialValueEmpty = "",
|
||||
initialError = null,
|
||||
initialIsValid = false,
|
||||
updatedValue = "new value",
|
||||
),
|
||||
InputFieldTestData(
|
||||
name = "NumberInputField",
|
||||
createInitialInput = { value, error, isValid -> NumberInputField(value, error, isValid) },
|
||||
initialState = NumberInputField(),
|
||||
initialValue = 123L,
|
||||
initialValueEmpty = null,
|
||||
initialError = null,
|
||||
initialIsValid = false,
|
||||
updatedValue = 456L,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private object TestValidationError : ValidationError
|
||||
private object TestValidationError2 : ValidationError
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
package app.k9mail.feature.account.setup.domain.input
|
||||
|
||||
import app.k9mail.core.common.domain.usecase.validation.ValidationError
|
||||
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
|
||||
import assertk.all
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import assertk.assertions.prop
|
||||
import org.junit.Test
|
||||
|
||||
class StringInputFieldTest {
|
||||
|
||||
@Test
|
||||
fun `should set default values`() {
|
||||
val stringInputState = StringInputField()
|
||||
|
||||
assertThat(stringInputState).all {
|
||||
prop(StringInputField::value).isEqualTo("")
|
||||
prop(StringInputField::error).isNull()
|
||||
prop(StringInputField::isValid).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reset errorMessage and isValid when value changed`() {
|
||||
val initialInputState = StringInputField(
|
||||
error = TestValidationError,
|
||||
isValid = false,
|
||||
)
|
||||
|
||||
val result = initialInputState.updateValue("new value")
|
||||
|
||||
assertThat(result).all {
|
||||
prop(StringInputField::value).isEqualTo("new value")
|
||||
prop(StringInputField::error).isNull()
|
||||
prop(StringInputField::isValid).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reset isValid when error set`() {
|
||||
val initialInputState = StringInputField(value = "input", isValid = true)
|
||||
|
||||
val result = initialInputState.updateError(TestValidationError)
|
||||
|
||||
assertThat(result).all {
|
||||
prop(StringInputField::value).isEqualTo("input")
|
||||
prop(StringInputField::error).isEqualTo(TestValidationError)
|
||||
prop(StringInputField::isValid).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reset errorMessage when valid`() {
|
||||
val initialInputState = StringInputField(
|
||||
value = "input",
|
||||
error = TestValidationError,
|
||||
)
|
||||
|
||||
val result = initialInputState.updateValidity(isValid = true)
|
||||
|
||||
assertThat(result).all {
|
||||
prop(StringInputField::value).isEqualTo("input")
|
||||
prop(StringInputField::error).isNull()
|
||||
prop(StringInputField::isValid).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not reset errorMessage when invalid`() {
|
||||
val initialInputState = StringInputField(
|
||||
value = "input",
|
||||
error = TestValidationError,
|
||||
)
|
||||
|
||||
val result = initialInputState.updateValidity(isValid = false)
|
||||
|
||||
assertThat(result).all {
|
||||
prop(StringInputField::value).isEqualTo("input")
|
||||
prop(StringInputField::error).isEqualTo(TestValidationError)
|
||||
prop(StringInputField::isValid).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should map from success ValidationResult`() {
|
||||
val initialInputState = StringInputField(
|
||||
value = "input",
|
||||
error = TestValidationError,
|
||||
)
|
||||
|
||||
val result = initialInputState.fromValidationResult(ValidationResult.Success)
|
||||
|
||||
assertThat(result).all {
|
||||
prop(StringInputField::value).isEqualTo("input")
|
||||
prop(StringInputField::error).isNull()
|
||||
prop(StringInputField::isValid).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should map from failure ValidationResult`() {
|
||||
val initialInputState = StringInputField(
|
||||
value = "input",
|
||||
error = null,
|
||||
)
|
||||
|
||||
val result = initialInputState.fromValidationResult(ValidationResult.Failure(TestValidationError))
|
||||
|
||||
assertThat(result).all {
|
||||
prop(StringInputField::value).isEqualTo("input")
|
||||
prop(StringInputField::error).isEqualTo(TestValidationError)
|
||||
prop(StringInputField::isValid).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
private object TestValidationError : ValidationError
|
||||
}
|
Loading…
Reference in a new issue