Add NumberInputField

This commit is contained in:
Wolf-Martell Montwé 2023-06-01 17:45:00 +02:00
parent 608cf6d3da
commit 440fa4957c
No known key found for this signature in database
GPG key ID: 6D45B21512ACBF72
8 changed files with 282 additions and 142 deletions

View file

@ -23,7 +23,9 @@ fun TextFieldOutlinedNumber(
MaterialOutlinedTextField(
value = value?.toString() ?: "",
onValueChange = {
onValueChange(it.toLongOrNull())
onValueChange(
it.takeIf { it.isNotBlank() }?.toLongOrNull(),
)
},
modifier = modifier,
enabled = isEnabled,

View file

@ -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,

View file

@ -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>
}

View file

@ -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,
)
}
}
}

View file

@ -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,
)
}
}
}

View file

@ -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),
)
}

View file

@ -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
}

View file

@ -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
}