Skip to content

Commit

Permalink
Make sure to only trigger validation after the first input
Browse files Browse the repository at this point in the history
  • Loading branch information
pauljohanneskraft committed Nov 19, 2024
1 parent 58dc269 commit 858c75f
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ class FocusValidationRulesTest {
assertLastState(false)
assertPasswordMessageExists(true)
assertEmptyMessageExists(true)
enterEmail("[email protected]")
assertEmptyMessageExists(false)
assertPasswordMessageExists(true)
enterPassword("password")
assertEmptyMessageExists(false)
assertPasswordMessageExists(false)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import edu.stanford.spezi.core.design.component.StringResource
import edu.stanford.spezi.core.design.views.validation.Validate
import edu.stanford.spezi.core.design.views.validation.ValidationRule
Expand All @@ -16,11 +17,16 @@ import edu.stanford.spezi.core.design.views.validation.nonEmpty
import edu.stanford.spezi.core.design.views.validation.state.ReceiveValidation
import edu.stanford.spezi.core.design.views.validation.state.ValidationContext
import edu.stanford.spezi.core.design.views.validation.views.VerifiableTextField
import edu.stanford.spezi.core.utils.extensions.testIdentifier

enum class Field {
INPUT, NON_EMPTY_INPUT
}

enum class FocusValidationRulesTestIdentifier {
EMAIL_TEXTFIELD, PASSWORD_TEXTFIELD
}

@Composable
fun FocusValidationRules() {
val input = remember { mutableStateOf("") }
Expand Down Expand Up @@ -50,12 +56,20 @@ fun FocusValidationRules() {
Switch(switchFocus.value, onCheckedChange = { switchFocus.value = it })
}

Validate(input.value, rules = listOf(ValidationRule.minimalPassword)) {
VerifiableTextField(StringResource(Field.INPUT.name), input)
Validate(nonEmptyInput.value, rules = listOf(ValidationRule.nonEmpty)) {
VerifiableTextField(
StringResource(Field.NON_EMPTY_INPUT.name),
nonEmptyInput,
Modifier.testIdentifier(FocusValidationRulesTestIdentifier.EMAIL_TEXTFIELD)
)
}

Validate(nonEmptyInput.value, rules = listOf(ValidationRule.nonEmpty)) {
VerifiableTextField(StringResource(Field.NON_EMPTY_INPUT.name), nonEmptyInput)
Validate(input.value, rules = listOf(ValidationRule.minimalPassword)) {
VerifiableTextField(
StringResource(Field.INPUT.name),
input,
Modifier.testIdentifier(FocusValidationRulesTestIdentifier.PASSWORD_TEXTFIELD)
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import edu.stanford.spezi.core.design.validation.composables.FocusValidationRulesTestIdentifier
import edu.stanford.spezi.core.testing.onNodeWithIdentifier

class FocusValidationRulesSimulator(
private val composeTestRule: ComposeTestRule,
Expand Down Expand Up @@ -45,4 +48,16 @@ class FocusValidationRulesSimulator(
.onNodeWithText("Last state: ${if (valid) "valid" else "invalid"}")
.assertExists()
}

fun enterEmail(text: String) {
composeTestRule
.onNodeWithIdentifier(FocusValidationRulesTestIdentifier.EMAIL_TEXTFIELD)
.performTextInput(text)
}

fun enterPassword(text: String) {
composeTestRule
.onNodeWithIdentifier(FocusValidationRulesTestIdentifier.PASSWORD_TEXTFIELD)
.performTextInput(text)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package edu.stanford.spezi.core.design.views.validation
import androidx.compose.runtime.mutableStateOf
import edu.stanford.spezi.core.design.views.validation.configuration.DEFAULT_VALIDATION_DEBOUNCE_DURATION
import edu.stanford.spezi.core.design.views.validation.state.FailedValidationResult
import edu.stanford.spezi.core.logging.speziLogger
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
Expand Down Expand Up @@ -41,6 +42,8 @@ internal class ValidationEngineImpl(
SUBMIT, MANUAL
}

private val logger by speziLogger()

private var validationResultsState = mutableStateOf(emptyList<FailedValidationResult>())

override val validationResults get() = validationResultsState.value
Expand Down Expand Up @@ -68,14 +71,15 @@ internal class ValidationEngineImpl(

private var debounceJob: Job? = null

@Suppress("detekt:LoopWithTooManyJumpStatements")
private fun computeFailedValidations(input: String): List<FailedValidationResult> {
val results = mutableListOf<FailedValidationResult>()

@Suppress("detekt:LoopWithTooManyJumpStatements")
for (rule in rules) {
val result = rule.validate(input) ?: break
results.add(result)
// TODO: Logging
rule.validate(input)?.let { result ->
results.add(result)
logger.w { "Validation for input $input failed with reason: ${result.message}" }
} ?: continue
if (rule.effect == CascadingValidationEffect.INTERCEPT) break
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
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 edu.stanford.spezi.core.design.component.StringResource
import edu.stanford.spezi.core.design.views.validation.configuration.DEFAULT_VALIDATION_DEBOUNCE_DURATION
Expand Down Expand Up @@ -53,8 +55,13 @@ fun Validate(
)
}

var isFirstInput by remember { mutableStateOf(true) }
LaunchedEffect(input) {
engine.submit(input, debounce = true)
if (isFirstInput) {
isFirstInput = false
} else {
engine.submit(input, debounce = true)
}
}

LaunchedEffect(validationDebounce) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
Expand All @@ -16,6 +17,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import edu.stanford.spezi.core.design.component.StringResource
import edu.stanford.spezi.core.design.theme.Spacings
import edu.stanford.spezi.core.design.theme.SpeziTheme
import edu.stanford.spezi.core.design.theme.ThemePreviews
import edu.stanford.spezi.core.design.views.validation.Validate
Expand Down Expand Up @@ -101,28 +103,26 @@ fun VerifiableTextField(
val validationEngine = LocalValidationEngine.current
val isSecure = remember(type) { type == TextFieldType.SECURE }

Column(modifier) {
// TODO: Check equality with iOS
TextField(
value = value,
onValueChange = onValueChanged,
label = label,
keyboardOptions = KeyboardOptions(
keyboardType = if (isSecure) KeyboardType.Password else KeyboardType.Text,
autoCorrect = !disableAutocorrection
),
visualTransformation = if (isSecure) PasswordVisualTransformation() else VisualTransformation.None,
modifier = Modifier.fillMaxWidth(),
)

Row {
ValidationResultsComposable(validationEngine?.displayedValidationResults ?: emptyList())

Spacer(Modifier.fillMaxWidth())

footer()
}
}
// TODO: Check equality with iOS
TextField(
value = value,
onValueChange = onValueChanged,
label = label,
keyboardOptions = KeyboardOptions(
keyboardType = if (isSecure) KeyboardType.Password else KeyboardType.Text,
autoCorrect = !disableAutocorrection
),
supportingText = {
Row(Modifier.padding(vertical = Spacings.small)) {
ValidationResultsComposable(validationEngine?.displayedValidationResults ?: emptyList())
Spacer(Modifier.fillMaxWidth())
footer()
}
},
isError = validationEngine?.isDisplayingValidationErrors ?: true,
visualTransformation = if (isSecure) PasswordVisualTransformation() else VisualTransformation.None,
modifier = modifier.fillMaxWidth(),
)
}

@ThemePreviews
Expand Down

0 comments on commit 858c75f

Please sign in to comment.