From a2c5c778d305ff46c1a0fe2beaa9127a1be01c77 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Thu, 24 Apr 2025 23:16:59 +0300 Subject: [PATCH 1/9] feat: date time picker validation improvements --- .../flow/component/datepicker/DatePicker.java | 2 +- .../validation/BasicValidationPage.java | 16 +- .../validation/BinderValidationPage.java | 2 + .../validation/BasicValidationIT.java | 137 ++++++++++-- .../validation/BinderValidationIT.java | 15 +- .../datetimepicker/DateTimePicker.java | 205 +++++++++++++++--- .../validation/BasicValidationTest.java | 106 ++++++++- .../flow/component/timepicker/TimePicker.java | 2 +- 8 files changed, 410 insertions(+), 75 deletions(-) diff --git a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java index a8aeb2903a8..0fb445bd53f 100644 --- a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java +++ b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java @@ -687,7 +687,7 @@ protected boolean isInputValuePresent() { */ @Synchronize(property = "_inputElementValue", value = { "change", "unparsable-change" }) - private String getInputElementValue() { + protected String getInputElementValue() { return getElement().getProperty("_inputElementValue", ""); } diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationPage.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationPage.java index a2fcc85c9c9..192b62c0d46 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationPage.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationPage.java @@ -30,15 +30,17 @@ public class BasicValidationPage public static final String CLEAR_VALUE_BUTTON = "clear-value-button"; public static final String REQUIRED_ERROR_MESSAGE = "Field is required"; - public static final String BAD_INPUT_ERROR_MESSAGE = "Value has incorrect format"; - public static final String MIN_ERROR_MESSAGE = "Value is too small"; - public static final String MAX_ERROR_MESSAGE = "Value is too big"; + public static final String BAD_INPUT_ERROR_MESSAGE = "Invalid date format"; + public static final String INCOMPLETE_INPUT_ERROR_MESSAGE = "Must fill in both date and time"; + public static final String MIN_ERROR_MESSAGE = "Date is too early"; + public static final String MAX_ERROR_MESSAGE = "Date is too late"; public BasicValidationPage() { super(); testField.setI18n(new DateTimePicker.DateTimePickerI18n() .setRequiredErrorMessage(REQUIRED_ERROR_MESSAGE) + .setIncompleteInputErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE) .setBadInputErrorMessage(BAD_INPUT_ERROR_MESSAGE) .setMinErrorMessage(MIN_ERROR_MESSAGE) .setMaxErrorMessage(MAX_ERROR_MESSAGE)); @@ -63,6 +65,12 @@ public BasicValidationPage() { } protected DateTimePicker createTestField() { - return new DateTimePicker(); + return new DateTimePicker() { + @Override + protected void validate() { + super.validate(); + incrementServerValidationCounter(); + } + }; } } diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationPage.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationPage.java index b0bac82ca5d..983351f1bd3 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationPage.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationPage.java @@ -32,6 +32,7 @@ public class BinderValidationPage public static final String REQUIRED_ERROR_MESSAGE = "Field is required"; public static final String BAD_INPUT_ERROR_MESSAGE = "Value has incorrect format"; + public static final String INCOMPLETE_INPUT_ERROR_MESSAGE = "Value is incomplete"; public static final String MIN_ERROR_MESSAGE = "Value is too small"; public static final String MAX_ERROR_MESSAGE = "Value is too big"; public static final String UNEXPECTED_VALUE_ERROR_MESSAGE = "Value does not match the expected value"; @@ -63,6 +64,7 @@ public BinderValidationPage() { testField.setI18n(new DateTimePicker.DateTimePickerI18n() .setBadInputErrorMessage(BAD_INPUT_ERROR_MESSAGE) + .setIncompleteInputErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE) .setMinErrorMessage(MIN_ERROR_MESSAGE) .setMaxErrorMessage(MAX_ERROR_MESSAGE)); diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java index a917fc027c2..08accf0d27e 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java @@ -17,6 +17,7 @@ import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.BAD_INPUT_ERROR_MESSAGE; import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.CLEAR_VALUE_BUTTON; +import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.INCOMPLETE_INPUT_ERROR_MESSAGE; import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.MAX_ERROR_MESSAGE; import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.MAX_INPUT; import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.MIN_ERROR_MESSAGE; @@ -59,7 +60,7 @@ public void triggerBlur_assertValidity() { timeInput.sendKeys(Keys.TAB); assertServerValid(); assertClientValid(); - assertErrorMessage(""); + assertErrorMessage(null); } @Test @@ -68,9 +69,9 @@ public void required_triggerBlur_assertValidity() { dateInput.sendKeys(Keys.TAB); timeInput.sendKeys(Keys.TAB); - assertServerInvalid(); - assertClientInvalid(); - assertErrorMessage(REQUIRED_ERROR_MESSAGE); + assertServerValid(); + assertClientValid(); + assertErrorMessage(null); } @Test @@ -83,6 +84,12 @@ public void required_changeValue_assertValidity() { assertClientValid(); assertErrorMessage(""); + setInputValue(dateInput, "1/1/2000"); + setInputValue(timeInput, ""); + assertServerInvalid(); + assertServerInvalid(); + assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE); + setInputValue(dateInput, ""); assertServerInvalid(); assertClientInvalid(); @@ -93,8 +100,7 @@ public void required_changeValue_assertValidity() { assertClientInvalid(); assertErrorMessage(REQUIRED_ERROR_MESSAGE); - setInputValue(dateInput, "INVALID"); - setInputValue(timeInput, "INVALID"); + setFieldIInvalid(); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(BAD_INPUT_ERROR_MESSAGE); @@ -193,7 +199,7 @@ public void setValue_clearValue_assertValidity() { @Test public void badInput_changeValue_assertValidity() { - setInputValue(dateInput, "INVALID"); + setFieldIInvalid(); setInputValue(timeInput, "INVALID"); assertServerInvalid(); assertClientInvalid(); @@ -205,7 +211,7 @@ public void badInput_changeValue_assertValidity() { assertClientValid(); assertErrorMessage(""); - setInputValue(dateInput, "INVALID"); + setFieldIInvalid(); setInputValue(timeInput, "INVALID"); assertServerInvalid(); assertClientInvalid(); @@ -214,7 +220,7 @@ public void badInput_changeValue_assertValidity() { @Test public void badInput_setDateInputValue_blur_assertValidity() { - setInputValue(dateInput, "INVALID"); + setFieldIInvalid(); dateInput.sendKeys(Keys.TAB); timeInput.sendKeys(Keys.TAB); assertServerInvalid(); @@ -233,7 +239,7 @@ public void badInput_setTimeInputValue_blur_assertValidity() { @Test public void badInput_setValue_clearValue_assertValidity() { - setInputValue(dateInput, "INVALID"); + setFieldIInvalid(); setInputValue(timeInput, "INVALID"); assertServerInvalid(); assertClientInvalid(); @@ -247,7 +253,7 @@ public void badInput_setValue_clearValue_assertValidity() { @Test public void badInput_setDateInputValue_blur_clearValue_assertValidity() { - setInputValue(dateInput, "INVALID"); + setFieldIInvalid(); dateInput.sendKeys(Keys.TAB); timeInput.sendKeys(Keys.TAB); assertServerInvalid(); @@ -275,12 +281,105 @@ public void badInput_setTimeInputValue_blur_clearValue_assertValidity() { } @Test - public void detach_attach_preservesInvalidState() { - // Make field invalid - $("button").id(REQUIRED_BUTTON).click(); + public void incompleteInput_assertValidity() { + setInputValue(dateInput, "1/1/2000"); + setInputValue(timeInput, ""); + assertServerInvalid(); + assertClientInvalid(); + assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE); + } + + @Test + public void incompleteInput_changeToValidValue_assertValidity() { + setInputValue(dateInput, "1/1/2000"); + setInputValue(timeInput, ""); + + setInputValue(dateInput, "1/1/2001"); + setInputValue(timeInput, "10:00"); + assertServerValid(); + assertClientValid(); + assertErrorMessage(""); + } + + @Test + public void validInput_changeToIncompleteInput_assertValidity() { + setInputValue(dateInput, "1/1/2001"); + setInputValue(timeInput, "10:00"); + + setInputValue(dateInput, "1/1/2000"); + setInputValue(timeInput, ""); + assertServerInvalid(); + assertClientInvalid(); + assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE); + } + + @Test + public void incompleteInput_setDateInputValue_blur_assertValidity() { + setInputValue(dateInput, "1/1/2000"); + setInputValue(timeInput, ""); + dateInput.sendKeys(Keys.TAB); + timeInput.sendKeys(Keys.TAB); + assertServerInvalid(); + assertClientInvalid(); + assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE); + } + + @Test + public void incompleteInput_setTimeInputValue_blur_assertValidity() { + setInputValue(dateInput, ""); + setInputValue(timeInput, "10:00"); + timeInput.sendKeys(Keys.TAB); + assertServerInvalid(); + assertClientInvalid(); + assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE); + } + + @Test + public void incompleteInput_setValue_clearValue_assertValidity() { + setInputValue(dateInput, "1/1/2000"); + setInputValue(timeInput, ""); + timeInput.sendKeys(Keys.ENTER); + + $("button").id(CLEAR_VALUE_BUTTON).click(); + assertServerValid(); + assertClientValid(); + assertErrorMessage(""); + } + + @Override + protected void assertValidationCount(int expected) { + super.assertValidationCount(expected); + } + + @Test + public void incompleteInput_setDateInputValue_blur_clearValue_assertValidity() { + setInputValue(dateInput, "1/1/2000"); + setInputValue(timeInput, ""); dateInput.sendKeys(Keys.TAB); timeInput.sendKeys(Keys.TAB); + $("button").id(CLEAR_VALUE_BUTTON).click(); + assertServerValid(); + assertClientValid(); + assertErrorMessage(""); + } + + @Test + public void incompleteInput_setTimeInputValue_blur_clearValue_assertValidity() { + setInputValue(dateInput, ""); + setInputValue(timeInput, "10:00"); + timeInput.sendKeys(Keys.TAB); + + $("button").id(CLEAR_VALUE_BUTTON).click(); + assertServerValid(); + assertClientValid(); + assertErrorMessage(""); + } + + @Test + public void detach_attach_preservesInvalidState() { + setFieldIInvalid(); + detachAndReattachField(); assertServerInvalid(); @@ -309,16 +408,18 @@ public void detach_hide_attach_showAndInvalidate_preservesInvalidState() { @Test public void clientSideInvalidStateIsNotPropagatedToServer() { - // Make the field invalid - $("button").id(REQUIRED_BUTTON).click(); - dateInput.sendKeys(Keys.TAB); - timeInput.sendKeys(Keys.TAB); + setFieldIInvalid(); executeScript("arguments[0].invalid = false", testField); assertServerInvalid(); } + private void setFieldIInvalid() { + setInputValue(dateInput, "INVALID"); + setInputValue(timeInput, "INVALID"); + } + protected DateTimePickerElement getTestField() { return $(DateTimePickerElement.class).first(); } diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationIT.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationIT.java index 7ed8a57d1f9..0a059744371 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationIT.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationIT.java @@ -18,6 +18,7 @@ import static com.vaadin.flow.component.datetimepicker.validation.BinderValidationPage.BAD_INPUT_ERROR_MESSAGE; import static com.vaadin.flow.component.datetimepicker.validation.BinderValidationPage.CLEAR_VALUE_BUTTON; import static com.vaadin.flow.component.datetimepicker.validation.BinderValidationPage.EXPECTED_VALUE_INPUT; +import static com.vaadin.flow.component.datetimepicker.validation.BinderValidationPage.INCOMPLETE_INPUT_ERROR_MESSAGE; import static com.vaadin.flow.component.datetimepicker.validation.BinderValidationPage.MAX_ERROR_MESSAGE; import static com.vaadin.flow.component.datetimepicker.validation.BinderValidationPage.MAX_INPUT; import static com.vaadin.flow.component.datetimepicker.validation.BinderValidationPage.MIN_ERROR_MESSAGE; @@ -58,17 +59,17 @@ public void fieldIsInitiallyValid() { public void required_triggerDateInputBlur_assertValidity() { dateInput.sendKeys(Keys.TAB); timeInput.sendKeys(Keys.TAB); - assertServerInvalid(); - assertClientInvalid(); - assertErrorMessage(REQUIRED_ERROR_MESSAGE); + assertServerValid(); + assertClientValid(); + assertErrorMessage(null); } @Test public void required_triggerTimeInputBlur_assertValidity() { timeInput.sendKeys(Keys.TAB); - assertServerInvalid(); - assertClientInvalid(); - assertErrorMessage(REQUIRED_ERROR_MESSAGE); + assertServerValid(); + assertClientValid(); + assertErrorMessage(null); } @Test @@ -84,7 +85,7 @@ public void required_changeValue_assertValidity() { setInputValue(dateInput, ""); assertServerInvalid(); assertClientInvalid(); - assertErrorMessage(REQUIRED_ERROR_MESSAGE); + assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE); setInputValue(timeInput, ""); assertServerInvalid(); diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java index feb5928c4bc..70a8284a877 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java @@ -17,17 +17,21 @@ import java.io.Serializable; import java.time.Duration; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.temporal.ChronoUnit; import java.util.Locale; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Function; import com.vaadin.flow.component.AbstractField; import com.vaadin.flow.component.AbstractSinglePropertyField; import com.vaadin.flow.component.Focusable; import com.vaadin.flow.component.HasValue; +import com.vaadin.flow.component.Synchronize; import com.vaadin.flow.component.Tag; import com.vaadin.flow.component.datepicker.DatePicker.DatePickerI18n; import com.vaadin.flow.component.dependency.JsModule; @@ -65,6 +69,24 @@ protected void validate() { protected boolean isInputValuePresent() { return super.isInputValuePresent(); } + + // Synchronizes on "date-picker-value-programmatically-set" in addition to + // the original events + @Synchronize(property = "_inputElementValue", value = { "change", + "unparsable-change", "date-picker-value-programmatically-set" }) + @Override + public String getInputElementValue() { + return super.getInputElementValue(); + } + + @Override + public void setValue(LocalDate date) { + super.setValue(date); + // Synchronizes the input element value back to the server when value is + // set programmatically + getElement().executeJs( + "this.dispatchEvent(new CustomEvent('date-picker-value-programmatically-set'));"); + } } @Tag("vaadin-time-picker") @@ -79,6 +101,24 @@ protected void validate() { protected boolean isInputValuePresent() { return super.isInputValuePresent(); } + + // Synchronizes on "time-picker-value-programmatically-set" in addition to + // the original events + @Synchronize(property = "_inputElementValue", value = { "change", + "unparsable-change", "time-picker-value-programmatically-set" }) + @Override + public String getInputElementValue() { + return super.getInputElementValue(); + } + + @Override + public void setValue(LocalTime time) { + super.setValue(time); + // Synchronizes the input element value back to the server when value is + // set programmatically + getElement().executeJs( + "this.dispatchEvent(new CustomEvent('time-picker-value-programmatically-set'));"); + } } /** @@ -123,18 +163,55 @@ public class DateTimePicker private LocalDateTime max; private LocalDateTime min; - private Validator defaultValidator = (value, context) -> { - boolean fromComponent = context == null; + private final CopyOnWriteArrayList> validationStatusChangeListeners = new CopyOnWriteArrayList<>(); + + private boolean programmaticallySettingValue; + + private final Validator defaultValidator = (value, + context) -> { + var fromComponent = context == null; + var isEmpty = Objects.equals(value, getEmptyValue()); + var isDatePickerEmpty = datePicker.isEmpty(); + var isTimePickerEmpty = timePicker.isEmpty(); + + // Report error if any of the pickers has bad input + var hasBadDatePickerInput = isDatePickerEmpty + && datePicker.isInputValuePresent(); + var hasBadTimePickerInput = isTimePickerEmpty + && timePicker.isInputValuePresent(); - boolean hasBadDatePickerInput = Objects.equals(datePicker.getValue(), - datePicker.getEmptyValue()) && datePicker.isInputValuePresent(); - boolean hasBadTimePickerInput = Objects.equals(timePicker.getValue(), - timePicker.getEmptyValue()) && timePicker.isInputValuePresent(); if (hasBadDatePickerInput || hasBadTimePickerInput) { return ValidationResult.error(getI18nErrorMessage( DateTimePickerI18n::getBadInputErrorMessage)); } + // Report error if only date picker has a value, and it's outside the + // range. + if (isEmpty && !isDatePickerEmpty) { + var maxDate = max != null ? max.toLocalDate() : null; + var minDate = min != null ? min.toLocalDate() : null; + + var maxResult = ValidationUtil.validateMaxConstraint( + getI18nErrorMessage(DateTimePickerI18n::getMaxErrorMessage), + datePicker.getValue(), maxDate); + if (maxResult.isError()) { + return maxResult; + } + + var minResult = ValidationUtil.validateMinConstraint( + getI18nErrorMessage(DateTimePickerI18n::getMinErrorMessage), + datePicker.getValue(), minDate); + if (minResult.isError()) { + return minResult; + } + } + + // Report error if only one of the pickers has a value + if (isEmpty && (!isDatePickerEmpty || !isTimePickerEmpty)) { + return ValidationResult.error(getI18nErrorMessage( + DateTimePickerI18n::getIncompleteInputErrorMessage)); + } + // Do the required check only if the validator is called from the // component, and not from Binder. Binder has its own implementation // of required validation. @@ -149,14 +226,14 @@ public class DateTimePicker } } - ValidationResult maxResult = ValidationUtil.validateMaxConstraint( + var maxResult = ValidationUtil.validateMaxConstraint( getI18nErrorMessage(DateTimePickerI18n::getMaxErrorMessage), value, max); if (maxResult.isError()) { return maxResult; } - ValidationResult minResult = ValidationUtil.validateMinConstraint( + var minResult = ValidationUtil.validateMinConstraint( getI18nErrorMessage(DateTimePickerI18n::getMinErrorMessage), value, min); if (minResult.isError()) { @@ -237,9 +314,7 @@ public DateTimePicker(LocalDateTime initialDateTime) { // workaround for https://github.com/vaadin/flow/issues/3496 setInvalid(false); - addValueChangeListener(e -> validate()); - - addClientValidatedEventListener(e -> validate()); + addValidationListeners(); } /** @@ -327,6 +402,27 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) { setLocale(locale); } + private void addValidationListeners() { + getElement().addEventListener("change", e -> { + // No need to validate since it will be validated once the + // programmatically set value is updated on the client + if (!programmaticallySettingValue) { + validate(true); + } + }); + getElement().addEventListener("unparsable-change", e -> { + // No need to validate since it will be validated once the + // programmatically set value is updated on the client + if (!programmaticallySettingValue) { + validate(true); + } + }); + getElement().addEventListener("value-programmatically-set", e -> { + validate(true); + programmaticallySettingValue = false; + }); + } + /** * Sets the selected date and time value of the component. The value can be * cleared by setting null. @@ -343,23 +439,13 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) { */ @Override public void setValue(LocalDateTime value) { - LocalDateTime oldValue = getValue(); - value = sanitizeValue(value); + synchronizeChildComponentValues(value); super.setValue(value); - - boolean isInputValuePresent = timePicker.isInputValuePresent() - || datePicker.isInputValuePresent(); - boolean isValueRemainedEmpty = valueEquals(oldValue, getEmptyValue()) - && valueEquals(value, getEmptyValue()); - if (isValueRemainedEmpty && isInputValuePresent) { - // Clear the input elements from possible bad input. - synchronizeChildComponentValues(value); - fireEvent(new ClientValidatedEvent(this, false)); - } else { - synchronizeChildComponentValues(value); - } - + // Notify the server in order to use the formatted values in validation + programmaticallySettingValue = true; + getElement().executeJs( + "this.dispatchEvent(new CustomEvent('value-programmatically-set'));"); } /** @@ -749,6 +835,11 @@ public void removeThemeNames(String... themeNames) { synchronizeTheme(); } + private boolean isInputValuePresent() { + return datePicker.isInputValuePresent() + || timePicker.isInputValuePresent(); + } + @Override public Validator getDefaultValidator() { return defaultValidator; @@ -757,9 +848,8 @@ public Validator getDefaultValidator() { @Override public Registration addValidationStatusChangeListener( ValidationStatusChangeListener listener) { - return addClientValidatedEventListener(event -> listener - .validationStatusChanged(new ValidationStatusChangeEvent<>(this, - event.isValid()))); + return Registration.addAndRemove(validationStatusChangeListeners, + listener); } @Override @@ -780,6 +870,26 @@ protected void validate() { validationController.validate(getValue()); } + /** + * Delegates the call to {@link #validate()} and additionally fires + * {@link ValidationStatusChangeEvent} to notify Binder that it needs to + * revalidate since the component's own validity state may have changed. + *

+ * NOTE: There is no need to notify Binder separately when running + * validation on {@link ValueChangeEvent}, as Binder already listens to this + * event and revalidates automatically. + */ + private void validate(boolean shouldFireValidationStatusChangeEvent) { + validate(); + + if (shouldFireValidationStatusChangeEvent) { + ValidationStatusChangeEvent event = new ValidationStatusChangeEvent<>( + this, !isInvalid()); + validationStatusChangeListeners.forEach( + listener -> listener.validationStatusChanged(event)); + } + } + /** * Sets the minimum date and time in the date time picker. Dates and times * before that will be disabled in the popups. @@ -929,6 +1039,7 @@ public static class DateTimePickerI18n implements Serializable { private String dateLabel; private String timeLabel; private String badInputErrorMessage; + private String incompleteInputErrorMessage; private String requiredErrorMessage; private String minErrorMessage; private String maxErrorMessage; @@ -936,7 +1047,7 @@ public static class DateTimePickerI18n implements Serializable { /** * Gets the aria-label suffix for the date picker. *

- * The date picker's final aria-label is a concatanation of the + * The date picker's final aria-label is a concatenation of the * DateTimePicker's {@link #getAriaLabel()} or {@link #getLabel()} * methods and this suffix. * @@ -949,7 +1060,7 @@ public String getDateLabel() { /** * Sets the aria-label suffix for the date picker. *

- * The date picker's final aria-label is a concatanation of the + * The date picker's final aria-label is a concatenation of the * DateTimePicker's {@link #getAriaLabel()} or {@link #getLabel()} * methods and this suffix. * @@ -966,7 +1077,7 @@ public DateTimePickerI18n setDateLabel(String dateLabel) { /** * Gets the aria-label suffix for the time picker. *

- * The time picker's aria-label is a concatanation of the + * The time picker's aria-label is a concatenation of the * DateTimePicker's {@link #getAriaLabel()} or {@link #getLabel()} * methods and this suffix. * @@ -979,7 +1090,7 @@ public String getTimeLabel() { /** * Sets the aria-label suffix for the time picker. *

- * The time picker's aria-label is a concatanation of the + * The time picker's aria-label is a concatenation of the * DateTimePicker's {@link #getAriaLabel()} or {@link #getLabel()} * methods and this suffix. * @@ -1020,6 +1131,34 @@ public DateTimePickerI18n setBadInputErrorMessage(String errorMessage) { return this; } + /** + * Gets the error message displayed when either the date or time is + * empty. + * + * @return the error message or {@code null} if not set + */ + public String getIncompleteInputErrorMessage() { + return incompleteInputErrorMessage; + } + + /** + * Sets the error message to display when either the date or time is + * empty. + *

+ * Note, custom error messages set with + * {@link DateTimePicker#setErrorMessage(String)} take priority over + * i18n error messages. + * + * @param errorMessage + * the error message to set, or {@code null} to clear + * @return this instance for method chaining + */ + public DateTimePickerI18n setIncompleteInputErrorMessage( + String errorMessage) { + incompleteInputErrorMessage = errorMessage; + return this; + } + /** * Gets the error message displayed when the field is required but * empty. diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java index 6a7a88ea2b2..272b455bb3d 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java @@ -15,7 +15,9 @@ */ package com.vaadin.flow.component.datetimepicker.validation; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import org.junit.Assert; import org.junit.Test; @@ -32,21 +34,86 @@ public class BasicValidationTest extends AbstractBasicValidationTest { + @Test - public void badInput_validate_emptyErrorMessageDisplayed() { + public void badInputOnDatePicker_validate_emptyErrorMessageDisplayed() { getDatePicker().getElement().setProperty("_inputElementValue", "foo"); - fireValidatedDomEvent(); + fireUnparsableChangeDomEvent(); + Assert.assertEquals("", testField.getErrorMessage()); + } + + @Test + public void badInputOnTimePicker_validate_emptyErrorMessageDisplayed() { + getTimePicker().getElement().setProperty("_inputElementValue", "foo"); + fireUnparsableChangeDomEvent(); Assert.assertEquals("", testField.getErrorMessage()); } @Test - public void badInput_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() { + public void badInputOnDatePicker_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() { + var errorMessage = "Value has invalid format"; testField.setI18n(new DateTimePicker.DateTimePickerI18n() - .setBadInputErrorMessage("Value has invalid format")); + .setBadInputErrorMessage(errorMessage)); getDatePicker().getElement().setProperty("_inputElementValue", "foo"); - fireValidatedDomEvent(); - Assert.assertEquals("Value has invalid format", - testField.getErrorMessage()); + fireUnparsableChangeDomEvent(); + Assert.assertEquals(errorMessage, testField.getErrorMessage()); + } + + @Test + public void badInputOnTimePicker_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() { + var errorMessage = "Value has invalid format"; + testField.setI18n(new DateTimePicker.DateTimePickerI18n() + .setBadInputErrorMessage(errorMessage)); + getTimePicker().getElement().setProperty("_inputElementValue", "foo"); + fireUnparsableChangeDomEvent(); + Assert.assertEquals(errorMessage, testField.getErrorMessage()); + } + + @Test + public void incompleteInputOnDatePicker_validate_emptyErrorMessageDisplayed() { + var picker = getDatePicker(); + picker.setValue(LocalDate.now()); + fireUnparsableChangeDomEvent(); + Assert.assertEquals("", testField.getErrorMessage()); + } + + @Test + public void incompleteInputOnTimePicker_validate_emptyErrorMessageDisplayed() { + var picker = getTimePicker(); + picker.setValue(LocalTime.now()); + fireUnparsableChangeDomEvent(); + Assert.assertEquals("", testField.getErrorMessage()); + } + + @Test + public void incompleteInputOnDatePicker_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() { + var errorMessage = "Value is incomplete"; + testField.setI18n(new DateTimePicker.DateTimePickerI18n() + .setIncompleteInputErrorMessage(errorMessage)); + var picker = getDatePicker(); + picker.setValue(LocalDate.now()); + fireUnparsableChangeDomEvent(); + Assert.assertEquals(errorMessage, testField.getErrorMessage()); + } + + @Test + public void incompleteInputOnTimePicker_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() { + var errorMessage = "Value is incomplete"; + testField.setI18n(new DateTimePicker.DateTimePickerI18n() + .setIncompleteInputErrorMessage(errorMessage)); + var picker = getTimePicker(); + picker.setValue(LocalTime.now()); + fireUnparsableChangeDomEvent(); + Assert.assertEquals(errorMessage, testField.getErrorMessage()); + } + + @Test + public void setIncompleteInputErrorMessage_errorMessageIsSet() { + var errorMessage = "Value is incomplete"; + testField.setI18n(new DateTimePicker.DateTimePickerI18n() + .setIncompleteInputErrorMessage(errorMessage)); + Assert.assertEquals(errorMessage, + testField.getI18n().getIncompleteInputErrorMessage()); } @Test @@ -54,6 +121,7 @@ public void required_validate_emptyErrorMessageDisplayed() { testField.setRequiredIndicatorVisible(true); testField.setValue(LocalDateTime.now()); testField.setValue(null); + fireValueProgrammaticallySetDomEvent(); Assert.assertEquals("", testField.getErrorMessage()); } @@ -64,6 +132,7 @@ public void required_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() { .setRequiredErrorMessage("Field is required")); testField.setValue(LocalDateTime.now()); testField.setValue(null); + fireValueProgrammaticallySetDomEvent(); Assert.assertEquals("Field is required", testField.getErrorMessage()); } @@ -71,6 +140,7 @@ public void required_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() { public void min_validate_emptyErrorMessageDisplayed() { testField.setMin(LocalDateTime.now()); testField.setValue(LocalDateTime.now().minusDays(1)); + fireValueProgrammaticallySetDomEvent(); Assert.assertEquals("", testField.getErrorMessage()); } @@ -80,6 +150,7 @@ public void min_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() { testField.setI18n(new DateTimePicker.DateTimePickerI18n() .setMinErrorMessage("Value is too small")); testField.setValue(LocalDateTime.now().minusDays(1)); + fireValueProgrammaticallySetDomEvent(); Assert.assertEquals("Value is too small", testField.getErrorMessage()); } @@ -87,6 +158,7 @@ public void min_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() { public void max_validate_emptyErrorMessageDisplayed() { testField.setMax(LocalDateTime.now()); testField.setValue(LocalDateTime.now().plusDays(1)); + fireValueProgrammaticallySetDomEvent(); Assert.assertEquals("", testField.getErrorMessage()); } @@ -96,6 +168,7 @@ public void max_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() { testField.setI18n(new DateTimePicker.DateTimePickerI18n() .setMaxErrorMessage("Value is too big")); testField.setValue(LocalDateTime.now().plusDays(1)); + fireValueProgrammaticallySetDomEvent(); Assert.assertEquals("Value is too big", testField.getErrorMessage()); } @@ -107,6 +180,7 @@ public void setI18nAndCustomErrorMessage_validate_customErrorMessageDisplayed() testField.setErrorMessage("Custom error message"); testField.setValue(LocalDateTime.now()); testField.setValue(null); + fireValueProgrammaticallySetDomEvent(); Assert.assertEquals("Custom error message", testField.getErrorMessage()); } @@ -122,6 +196,7 @@ public void setI18nAndCustomErrorMessage_validate_removeCustomErrorMessage_valid testField.setErrorMessage(""); testField.setValue(LocalDateTime.now()); testField.setValue(null); + fireValueProgrammaticallySetDomEvent(); Assert.assertEquals("Field is required", testField.getErrorMessage()); } @@ -145,10 +220,19 @@ private TimePicker getTimePicker() { return (TimePicker) SlotUtils.getChildInSlot(testField, "time-picker"); } - private void fireValidatedDomEvent() { - DomEvent validatedDomEvent = new DomEvent(testField.getElement(), - "validated", Json.createObject()); + private void fireUnparsableChangeDomEvent() { + fireDomEvent("unparsable-change"); + } + + private void fireValueProgrammaticallySetDomEvent() { + fireDomEvent("value-programmatically-set"); + } + + private void fireDomEvent(String eventType) { + var domEvent = new DomEvent(testField.getElement(), eventType, + Json.createObject()); testField.getElement().getNode().getFeature(ElementListenerMap.class) - .fireEvent(validatedDomEvent); + .fireEvent(domEvent); } + } diff --git a/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java b/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java index 4a1da282f90..eba96c5610b 100644 --- a/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java +++ b/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java @@ -495,7 +495,7 @@ protected boolean isInputValuePresent() { */ @Synchronize(property = "_inputElementValue", value = { "change", "unparsable-change" }) - private String getInputElementValue() { + protected String getInputElementValue() { return getElement().getProperty("_inputElementValue", ""); } From 33d0d694210950149ece93263ef4e57b04604281 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Fri, 25 Apr 2025 10:01:01 +0300 Subject: [PATCH 2/9] refactor: add more tests and keep number of programmatically set values --- .../validation/BasicValidationPage.java | 12 ++ .../validation/BasicValidationIT.java | 140 ++++++++++++++++-- .../datetimepicker/DateTimePicker.java | 16 +- .../validation/BasicValidationTest.java | 7 +- 4 files changed, 158 insertions(+), 17 deletions(-) diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationPage.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationPage.java index 192b62c0d46..d604ddf20b0 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationPage.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationPage.java @@ -28,6 +28,8 @@ public class BasicValidationPage public static final String MIN_INPUT = "min-input"; public static final String MAX_INPUT = "max-input"; public static final String CLEAR_VALUE_BUTTON = "clear-value-button"; + public static final String SET_VALUE_PROGRAMMATICALLY = "set-value-programmatically"; + public static final String CLEAR_AND_SET_VALUE_PROGRAMMATICALLY = "clear-and-set-value-programmatically"; public static final String REQUIRED_ERROR_MESSAGE = "Field is required"; public static final String BAD_INPUT_ERROR_MESSAGE = "Invalid date format"; @@ -62,6 +64,16 @@ public BasicValidationPage() { add(createButton(CLEAR_VALUE_BUTTON, "Clear value", event -> { testField.clear(); })); + + add(createButton(SET_VALUE_PROGRAMMATICALLY, + "Set value programmatically", + event -> testField.setValue(LocalDateTime.now()))); + + add(createButton(CLEAR_AND_SET_VALUE_PROGRAMMATICALLY, + "Clear and set value programmatically", event -> { + testField.clear(); + testField.setValue(LocalDateTime.now()); + })); } protected DateTimePicker createTestField() { diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java index 08accf0d27e..9ee1e0a1deb 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java @@ -16,6 +16,7 @@ package com.vaadin.flow.component.datetimepicker.validation; import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.BAD_INPUT_ERROR_MESSAGE; +import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.CLEAR_AND_SET_VALUE_PROGRAMMATICALLY; import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.CLEAR_VALUE_BUTTON; import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.INCOMPLETE_INPUT_ERROR_MESSAGE; import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.MAX_ERROR_MESSAGE; @@ -24,6 +25,7 @@ import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.MIN_INPUT; import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.REQUIRED_BUTTON; import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.REQUIRED_ERROR_MESSAGE; +import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.SET_VALUE_PROGRAMMATICALLY; import org.junit.Before; import org.junit.Test; @@ -74,6 +76,17 @@ public void required_triggerBlur_assertValidity() { assertErrorMessage(null); } + @Test + public void required_changeInputTemporarily_triggerBlur_assertValidity() { + $("button").id(REQUIRED_BUTTON).click(); + dateInput.sendKeys("1", Keys.BACK_SPACE, Keys.ENTER); + dateInput.sendKeys(Keys.TAB); + timeInput.sendKeys(Keys.TAB); + assertServerValid(); + assertClientValid(); + assertErrorMessage(null); + } + @Test public void required_changeValue_assertValidity() { $("button").id(REQUIRED_BUTTON).click(); @@ -100,7 +113,7 @@ public void required_changeValue_assertValidity() { assertClientInvalid(); assertErrorMessage(REQUIRED_ERROR_MESSAGE); - setFieldIInvalid(); + setFieldInvalid(); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(BAD_INPUT_ERROR_MESSAGE); @@ -153,7 +166,6 @@ public void max_changeDateInputValue_assertValidity() { $("input").id(MAX_INPUT).sendKeys("2000-02-02T12:00", Keys.ENTER); setInputValue(dateInput, "3/3/2000"); - setInputValue(timeInput, "13:00"); assertClientInvalid(); assertServerInvalid(); assertErrorMessage(MAX_ERROR_MESSAGE); @@ -199,7 +211,7 @@ public void setValue_clearValue_assertValidity() { @Test public void badInput_changeValue_assertValidity() { - setFieldIInvalid(); + setFieldInvalid(); setInputValue(timeInput, "INVALID"); assertServerInvalid(); assertClientInvalid(); @@ -211,7 +223,7 @@ public void badInput_changeValue_assertValidity() { assertClientValid(); assertErrorMessage(""); - setFieldIInvalid(); + setFieldInvalid(); setInputValue(timeInput, "INVALID"); assertServerInvalid(); assertClientInvalid(); @@ -220,7 +232,7 @@ public void badInput_changeValue_assertValidity() { @Test public void badInput_setDateInputValue_blur_assertValidity() { - setFieldIInvalid(); + setFieldInvalid(); dateInput.sendKeys(Keys.TAB); timeInput.sendKeys(Keys.TAB); assertServerInvalid(); @@ -239,7 +251,7 @@ public void badInput_setTimeInputValue_blur_assertValidity() { @Test public void badInput_setValue_clearValue_assertValidity() { - setFieldIInvalid(); + setFieldInvalid(); setInputValue(timeInput, "INVALID"); assertServerInvalid(); assertClientInvalid(); @@ -253,7 +265,7 @@ public void badInput_setValue_clearValue_assertValidity() { @Test public void badInput_setDateInputValue_blur_clearValue_assertValidity() { - setFieldIInvalid(); + setFieldInvalid(); dateInput.sendKeys(Keys.TAB); timeInput.sendKeys(Keys.TAB); assertServerInvalid(); @@ -378,7 +390,7 @@ public void incompleteInput_setTimeInputValue_blur_clearValue_assertValidity() { @Test public void detach_attach_preservesInvalidState() { - setFieldIInvalid(); + setFieldInvalid(); detachAndReattachField(); @@ -408,14 +420,116 @@ public void detach_hide_attach_showAndInvalidate_preservesInvalidState() { @Test public void clientSideInvalidStateIsNotPropagatedToServer() { - setFieldIInvalid(); + setFieldInvalid(); executeScript("arguments[0].invalid = false", testField); assertServerInvalid(); } - private void setFieldIInvalid() { + @Test + public void triggerBlurWithNoChange_fieldNotValidated() { + dateInput.sendKeys(Keys.TAB); + timeInput.sendKeys(Keys.TAB); + assertValidationCount(0); + } + + @Test + public void initiallyEmpty_setValidValue_fieldValidatedOnce() { + dateInput.sendKeys("1/1/2000"); + dateInput.sendKeys(Keys.ENTER); + dateInput.sendKeys(Keys.TAB); + timeInput.sendKeys("10:00"); + timeInput.sendKeys(Keys.ENTER); + assertValidationCount(1); + } + + @Test + public void initiallyEmpty_setInvalidValue_fieldNotValidatedOnce() { + dateInput.sendKeys("Invalid"); + dateInput.sendKeys(Keys.ENTER); + assertValidationCount(1); + } + + @Test + public void initiallyValidValue_clearValue_fieldValidatedOnce() { + dateInput.sendKeys("1/1/2000"); + dateInput.sendKeys(Keys.ENTER); + dateInput.sendKeys(Keys.TAB); + timeInput.sendKeys("10:00"); + timeInput.sendKeys(Keys.ENTER); + assertValidationCount(1); + clearInputValue(); + assertValidationCount(1); + } + + @Test + public void initiallyValidValue_changeValue_fieldValidatedOnce() { + dateInput.sendKeys("1/1/2000"); + dateInput.sendKeys(Keys.ENTER); + dateInput.sendKeys(Keys.TAB); + timeInput.sendKeys("10:00"); + timeInput.sendKeys(Keys.ENTER); + assertValidationCount(1); + dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); + dateInput.sendKeys("1/1/2001"); + dateInput.sendKeys(Keys.ENTER); + assertValidationCount(1); + } + + @Test + public void initiallyInvalidValue_changeInvalidValue_fieldValidatedOnce() { + dateInput.sendKeys("Invalid"); + dateInput.sendKeys(Keys.ENTER); + assertValidationCount(1); + dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); + dateInput.sendKeys("Not a date"); + dateInput.sendKeys(Keys.ENTER); + assertValidationCount(1); + } + + @Test + public void initiallyInvalidValue_setValidValue_fieldValidatedOnce() { + dateInput.sendKeys("Invalid"); + dateInput.sendKeys(Keys.ENTER); + assertValidationCount(1); + dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); + dateInput.sendKeys("1/1/2000"); + timeInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); + timeInput.sendKeys("10:00"); + timeInput.sendKeys(Keys.ENTER); + assertValidationCount(1); + } + + @Test + public void initiallyInvalidValue_clearValue_fieldValidatedOnce() { + dateInput.sendKeys("Invalid"); + dateInput.sendKeys(Keys.ENTER); + assertValidationCount(1); + clearInputValue(); + assertValidationCount(1); + } + + @Test + public void max_setDateOutOfRange_fieldValidatedOnce() { + $("input").id(MAX_INPUT).sendKeys("2000-02-02T12:00", Keys.ENTER); + setInputValue(dateInput, "3/3/2000"); + assertValidationCount(1); + } + + @Test + public void setValueProgrammatically_fieldValidatedOnce() { + clickElementWithJs(SET_VALUE_PROGRAMMATICALLY); + assertValidationCount(1); + } + + @Test + public void clearAndSetValueProgrammatically_fieldValidatedOnce() { + clickElementWithJs(CLEAR_AND_SET_VALUE_PROGRAMMATICALLY); + assertValidationCount(1); + } + + private void setFieldInvalid() { setInputValue(dateInput, "INVALID"); setInputValue(timeInput, "INVALID"); } @@ -428,4 +542,10 @@ private void setInputValue(TestBenchElement input, String value) { input.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); input.sendKeys(value, Keys.ENTER); } + + private void clearInputValue() { + dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); + timeInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); + timeInput.sendKeys(Keys.ENTER); + } } diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java index 70a8284a877..3a148c8ecf1 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java @@ -165,7 +165,7 @@ public class DateTimePicker private final CopyOnWriteArrayList> validationStatusChangeListeners = new CopyOnWriteArrayList<>(); - private boolean programmaticallySettingValue; + private int pendingInputElementValueSyncs = 0; private final Validator defaultValidator = (value, context) -> { @@ -406,20 +406,24 @@ private void addValidationListeners() { getElement().addEventListener("change", e -> { // No need to validate since it will be validated once the // programmatically set value is updated on the client - if (!programmaticallySettingValue) { + if (pendingInputElementValueSyncs == 0) { validate(true); } }); getElement().addEventListener("unparsable-change", e -> { // No need to validate since it will be validated once the // programmatically set value is updated on the client - if (!programmaticallySettingValue) { + if (pendingInputElementValueSyncs == 0) { validate(true); } }); + getElement().addEventListener("value-programmatically-set", e -> { - validate(true); - programmaticallySettingValue = false; + // Validate only for the final input element value sync caused by + // programmatically setting values + if (--pendingInputElementValueSyncs == 0) { + validate(true); + } }); } @@ -443,7 +447,7 @@ public void setValue(LocalDateTime value) { synchronizeChildComponentValues(value); super.setValue(value); // Notify the server in order to use the formatted values in validation - programmaticallySettingValue = true; + pendingInputElementValueSyncs++; getElement().executeJs( "this.dispatchEvent(new CustomEvent('value-programmatically-set'));"); } diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java index 272b455bb3d..9e1c247a238 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java @@ -120,6 +120,7 @@ public void setIncompleteInputErrorMessage_errorMessageIsSet() { public void required_validate_emptyErrorMessageDisplayed() { testField.setRequiredIndicatorVisible(true); testField.setValue(LocalDateTime.now()); + fireValueProgrammaticallySetDomEvent(); testField.setValue(null); fireValueProgrammaticallySetDomEvent(); Assert.assertEquals("", testField.getErrorMessage()); @@ -131,6 +132,7 @@ public void required_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() { testField.setI18n(new DateTimePicker.DateTimePickerI18n() .setRequiredErrorMessage("Field is required")); testField.setValue(LocalDateTime.now()); + fireValueProgrammaticallySetDomEvent(); testField.setValue(null); fireValueProgrammaticallySetDomEvent(); Assert.assertEquals("Field is required", testField.getErrorMessage()); @@ -179,6 +181,7 @@ public void setI18nAndCustomErrorMessage_validate_customErrorMessageDisplayed() .setRequiredErrorMessage("Field is required")); testField.setErrorMessage("Custom error message"); testField.setValue(LocalDateTime.now()); + fireValueProgrammaticallySetDomEvent(); testField.setValue(null); fireValueProgrammaticallySetDomEvent(); Assert.assertEquals("Custom error message", @@ -192,9 +195,12 @@ public void setI18nAndCustomErrorMessage_validate_removeCustomErrorMessage_valid .setRequiredErrorMessage("Field is required")); testField.setErrorMessage("Custom error message"); testField.setValue(LocalDateTime.now()); + fireValueProgrammaticallySetDomEvent(); testField.setValue(null); + fireValueProgrammaticallySetDomEvent(); testField.setErrorMessage(""); testField.setValue(LocalDateTime.now()); + fireValueProgrammaticallySetDomEvent(); testField.setValue(null); fireValueProgrammaticallySetDomEvent(); Assert.assertEquals("Field is required", testField.getErrorMessage()); @@ -234,5 +240,4 @@ private void fireDomEvent(String eventType) { testField.getElement().getNode().getFeature(ElementListenerMap.class) .fireEvent(domEvent); } - } From 2fee6b8ffbab40b377f94ffb814204153f23a068 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Mon, 5 May 2025 23:08:59 +0300 Subject: [PATCH 3/9] refactor: avoid roundtrip and use input data from pickers --- .../flow/component/datepicker/DatePicker.java | 24 +++- .../validation/BasicValidationIT.java | 5 +- .../datetimepicker/DateTimePicker.java | 107 ++++-------------- .../validation/BasicValidationTest.java | 46 ++++---- .../flow/component/timepicker/TimePicker.java | 17 ++- 5 files changed, 81 insertions(+), 118 deletions(-) diff --git a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java index 0fb445bd53f..9bcd6dfa777 100644 --- a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java +++ b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java @@ -169,9 +169,9 @@ public class DatePicker private Validator defaultValidator = (value, context) -> { boolean fromComponent = context == null; - if (unparsableValue != null && fallbackParserErrorMessage != null) { + if (isInputUnparsable() && fallbackParserErrorMessage != null) { return ValidationResult.error(fallbackParserErrorMessage); - } else if (unparsableValue != null) { + } else if (isInputUnparsable()) { return ValidationResult.error(getI18nErrorMessage( DatePickerI18n::getBadInputErrorMessage)); } @@ -668,14 +668,28 @@ private void fireValidationStatusChangeEvent() { /** * Returns whether the input element has a value or not. + *

+ * For internal use only. * * @return true if the input element's value is populated, * false otherwise + * @deprecated Since v24.8 */ + @Deprecated(since = "24.8") protected boolean isInputValuePresent() { return !getInputElementValue().isEmpty(); } + /** + * Returns whether the input value is unparsable. + * + * @return true if the input element's value is populated and + * unparsable, false otherwise + */ + protected boolean isInputUnparsable() { + return unparsableValue != null; + } + /** * Gets the value of the input element. This value is updated on the server * when the web component dispatches a `change` or `unparsable-change` @@ -687,7 +701,7 @@ protected boolean isInputValuePresent() { */ @Synchronize(property = "_inputElementValue", value = { "change", "unparsable-change" }) - protected String getInputElementValue() { + private String getInputElementValue() { return getElement().getProperty("_inputElementValue", ""); } @@ -762,7 +776,7 @@ private Result runFallbackParser(String s) { @Override public void setValue(LocalDate value) { LocalDate oldValue = getValue(); - if (oldValue == null && value == null && unparsableValue != null) { + if (oldValue == null && value == null && isInputUnparsable()) { // When the value is programmatically cleared while the field // contains an unparsable input, ValueChangeEvent isn't fired, // so we need to call setModelValue manually to clear the bad @@ -797,7 +811,7 @@ protected void setModelValue(LocalDate newModelValue, boolean fromClient) { try { isFallbackParserRunning = true; - if (fallbackParser != null && unparsableValue != null) { + if (fallbackParser != null && isInputUnparsable()) { Result result = runFallbackParser(unparsableValue); if (result.isError()) { fallbackParserErrorMessage = result.getMessage() diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java index 9ee1e0a1deb..68e09189885 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java @@ -524,9 +524,9 @@ public void setValueProgrammatically_fieldValidatedOnce() { } @Test - public void clearAndSetValueProgrammatically_fieldValidatedOnce() { + public void clearAndSetValueProgrammatically_fieldValidatedTwice() { clickElementWithJs(CLEAR_AND_SET_VALUE_PROGRAMMATICALLY); - assertValidationCount(1); + assertValidationCount(2); } private void setFieldInvalid() { @@ -545,6 +545,7 @@ private void setInputValue(TestBenchElement input, String value) { private void clearInputValue() { dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); + dateInput.sendKeys(Keys.TAB); timeInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); timeInput.sendKeys(Keys.ENTER); } diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java index 3a148c8ecf1..c15aa6bb8af 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java @@ -17,9 +17,7 @@ import java.io.Serializable; import java.time.Duration; -import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.LocalTime; import java.time.temporal.ChronoUnit; import java.util.Locale; import java.util.Objects; @@ -31,7 +29,6 @@ import com.vaadin.flow.component.AbstractSinglePropertyField; import com.vaadin.flow.component.Focusable; import com.vaadin.flow.component.HasValue; -import com.vaadin.flow.component.Synchronize; import com.vaadin.flow.component.Tag; import com.vaadin.flow.component.datepicker.DatePicker.DatePickerI18n; import com.vaadin.flow.component.dependency.JsModule; @@ -66,26 +63,8 @@ protected void validate() { } @Override - protected boolean isInputValuePresent() { - return super.isInputValuePresent(); - } - - // Synchronizes on "date-picker-value-programmatically-set" in addition to - // the original events - @Synchronize(property = "_inputElementValue", value = { "change", - "unparsable-change", "date-picker-value-programmatically-set" }) - @Override - public String getInputElementValue() { - return super.getInputElementValue(); - } - - @Override - public void setValue(LocalDate date) { - super.setValue(date); - // Synchronizes the input element value back to the server when value is - // set programmatically - getElement().executeJs( - "this.dispatchEvent(new CustomEvent('date-picker-value-programmatically-set'));"); + protected boolean isInputUnparsable() { + return super.isInputUnparsable(); } } @@ -98,26 +77,8 @@ protected void validate() { } @Override - protected boolean isInputValuePresent() { - return super.isInputValuePresent(); - } - - // Synchronizes on "time-picker-value-programmatically-set" in addition to - // the original events - @Synchronize(property = "_inputElementValue", value = { "change", - "unparsable-change", "time-picker-value-programmatically-set" }) - @Override - public String getInputElementValue() { - return super.getInputElementValue(); - } - - @Override - public void setValue(LocalTime time) { - super.setValue(time); - // Synchronizes the input element value back to the server when value is - // set programmatically - getElement().executeJs( - "this.dispatchEvent(new CustomEvent('time-picker-value-programmatically-set'));"); + protected boolean isInputUnparsable() { + return super.isInputUnparsable(); } } @@ -165,29 +126,19 @@ public class DateTimePicker private final CopyOnWriteArrayList> validationStatusChangeListeners = new CopyOnWriteArrayList<>(); - private int pendingInputElementValueSyncs = 0; - private final Validator defaultValidator = (value, context) -> { var fromComponent = context == null; - var isEmpty = Objects.equals(value, getEmptyValue()); - var isDatePickerEmpty = datePicker.isEmpty(); - var isTimePickerEmpty = timePicker.isEmpty(); // Report error if any of the pickers has bad input - var hasBadDatePickerInput = isDatePickerEmpty - && datePicker.isInputValuePresent(); - var hasBadTimePickerInput = isTimePickerEmpty - && timePicker.isInputValuePresent(); - - if (hasBadDatePickerInput || hasBadTimePickerInput) { + if (isInputUnparsable()) { return ValidationResult.error(getI18nErrorMessage( DateTimePickerI18n::getBadInputErrorMessage)); } // Report error if only date picker has a value, and it's outside the // range. - if (isEmpty && !isDatePickerEmpty) { + if (Objects.equals(value, getEmptyValue()) && !datePicker.isEmpty()) { var maxDate = max != null ? max.toLocalDate() : null; var minDate = min != null ? min.toLocalDate() : null; @@ -207,7 +158,7 @@ public class DateTimePicker } // Report error if only one of the pickers has a value - if (isEmpty && (!isDatePickerEmpty || !isTimePickerEmpty)) { + if (isInputIncomplete()) { return ValidationResult.error(getI18nErrorMessage( DateTimePickerI18n::getIncompleteInputErrorMessage)); } @@ -403,28 +354,8 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) { } private void addValidationListeners() { - getElement().addEventListener("change", e -> { - // No need to validate since it will be validated once the - // programmatically set value is updated on the client - if (pendingInputElementValueSyncs == 0) { - validate(true); - } - }); - getElement().addEventListener("unparsable-change", e -> { - // No need to validate since it will be validated once the - // programmatically set value is updated on the client - if (pendingInputElementValueSyncs == 0) { - validate(true); - } - }); - - getElement().addEventListener("value-programmatically-set", e -> { - // Validate only for the final input element value sync caused by - // programmatically setting values - if (--pendingInputElementValueSyncs == 0) { - validate(true); - } - }); + getElement().addEventListener("change", e -> validate(true)); + getElement().addEventListener("unparsable-change", e -> validate(true)); } /** @@ -443,13 +374,14 @@ private void addValidationListeners() { */ @Override public void setValue(LocalDateTime value) { + var oldValue = getValue(); value = sanitizeValue(value); - synchronizeChildComponentValues(value); super.setValue(value); - // Notify the server in order to use the formatted values in validation - pendingInputElementValueSyncs++; - getElement().executeJs( - "this.dispatchEvent(new CustomEvent('value-programmatically-set'));"); + var shouldFireValidationStatusChangeEvent = oldValue == null + && value == null + && (isInputUnparsable() || isInputIncomplete()); + synchronizeChildComponentValues(value); + validate(shouldFireValidationStatusChangeEvent); } /** @@ -839,9 +771,12 @@ public void removeThemeNames(String... themeNames) { synchronizeTheme(); } - private boolean isInputValuePresent() { - return datePicker.isInputValuePresent() - || timePicker.isInputValuePresent(); + private boolean isInputUnparsable() { + return datePicker.isInputUnparsable() || timePicker.isInputUnparsable(); + } + + private boolean isInputIncomplete() { + return datePicker.isEmpty() != timePicker.isEmpty(); } @Override diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java index 9e1c247a238..aeeb0a33563 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java @@ -27,6 +27,7 @@ import com.vaadin.flow.component.shared.SlotUtils; import com.vaadin.flow.component.timepicker.TimePicker; import com.vaadin.flow.dom.DomEvent; +import com.vaadin.flow.dom.Element; import com.vaadin.flow.internal.nodefeature.ElementListenerMap; import com.vaadin.tests.validation.AbstractBasicValidationTest; @@ -55,6 +56,7 @@ public void badInputOnDatePicker_setI18nErrorMessage_validate_i18nErrorMessageDi testField.setI18n(new DateTimePicker.DateTimePickerI18n() .setBadInputErrorMessage(errorMessage)); getDatePicker().getElement().setProperty("_inputElementValue", "foo"); + fireDomEvent("unparsable-change", getDatePicker().getElement()); fireUnparsableChangeDomEvent(); Assert.assertEquals(errorMessage, testField.getErrorMessage()); } @@ -65,6 +67,7 @@ public void badInputOnTimePicker_setI18nErrorMessage_validate_i18nErrorMessageDi testField.setI18n(new DateTimePicker.DateTimePickerI18n() .setBadInputErrorMessage(errorMessage)); getTimePicker().getElement().setProperty("_inputElementValue", "foo"); + fireDomEvent("unparsable-change", getTimePicker().getElement()); fireUnparsableChangeDomEvent(); Assert.assertEquals(errorMessage, testField.getErrorMessage()); } @@ -120,9 +123,9 @@ public void setIncompleteInputErrorMessage_errorMessageIsSet() { public void required_validate_emptyErrorMessageDisplayed() { testField.setRequiredIndicatorVisible(true); testField.setValue(LocalDateTime.now()); - fireValueProgrammaticallySetDomEvent(); + fireChangeDomEvent(); testField.setValue(null); - fireValueProgrammaticallySetDomEvent(); + fireChangeDomEvent(); Assert.assertEquals("", testField.getErrorMessage()); } @@ -132,9 +135,9 @@ public void required_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() { testField.setI18n(new DateTimePicker.DateTimePickerI18n() .setRequiredErrorMessage("Field is required")); testField.setValue(LocalDateTime.now()); - fireValueProgrammaticallySetDomEvent(); + fireChangeDomEvent(); testField.setValue(null); - fireValueProgrammaticallySetDomEvent(); + fireChangeDomEvent(); Assert.assertEquals("Field is required", testField.getErrorMessage()); } @@ -142,7 +145,7 @@ public void required_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() { public void min_validate_emptyErrorMessageDisplayed() { testField.setMin(LocalDateTime.now()); testField.setValue(LocalDateTime.now().minusDays(1)); - fireValueProgrammaticallySetDomEvent(); + fireChangeDomEvent(); Assert.assertEquals("", testField.getErrorMessage()); } @@ -152,7 +155,7 @@ public void min_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() { testField.setI18n(new DateTimePicker.DateTimePickerI18n() .setMinErrorMessage("Value is too small")); testField.setValue(LocalDateTime.now().minusDays(1)); - fireValueProgrammaticallySetDomEvent(); + fireChangeDomEvent(); Assert.assertEquals("Value is too small", testField.getErrorMessage()); } @@ -160,7 +163,7 @@ public void min_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() { public void max_validate_emptyErrorMessageDisplayed() { testField.setMax(LocalDateTime.now()); testField.setValue(LocalDateTime.now().plusDays(1)); - fireValueProgrammaticallySetDomEvent(); + fireChangeDomEvent(); Assert.assertEquals("", testField.getErrorMessage()); } @@ -170,7 +173,7 @@ public void max_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() { testField.setI18n(new DateTimePicker.DateTimePickerI18n() .setMaxErrorMessage("Value is too big")); testField.setValue(LocalDateTime.now().plusDays(1)); - fireValueProgrammaticallySetDomEvent(); + fireChangeDomEvent(); Assert.assertEquals("Value is too big", testField.getErrorMessage()); } @@ -181,9 +184,9 @@ public void setI18nAndCustomErrorMessage_validate_customErrorMessageDisplayed() .setRequiredErrorMessage("Field is required")); testField.setErrorMessage("Custom error message"); testField.setValue(LocalDateTime.now()); - fireValueProgrammaticallySetDomEvent(); + fireChangeDomEvent(); testField.setValue(null); - fireValueProgrammaticallySetDomEvent(); + fireChangeDomEvent(); Assert.assertEquals("Custom error message", testField.getErrorMessage()); } @@ -195,14 +198,14 @@ public void setI18nAndCustomErrorMessage_validate_removeCustomErrorMessage_valid .setRequiredErrorMessage("Field is required")); testField.setErrorMessage("Custom error message"); testField.setValue(LocalDateTime.now()); - fireValueProgrammaticallySetDomEvent(); + fireChangeDomEvent(); testField.setValue(null); - fireValueProgrammaticallySetDomEvent(); + fireChangeDomEvent(); testField.setErrorMessage(""); testField.setValue(LocalDateTime.now()); - fireValueProgrammaticallySetDomEvent(); + fireChangeDomEvent(); testField.setValue(null); - fireValueProgrammaticallySetDomEvent(); + fireChangeDomEvent(); Assert.assertEquals("Field is required", testField.getErrorMessage()); } @@ -226,18 +229,17 @@ private TimePicker getTimePicker() { return (TimePicker) SlotUtils.getChildInSlot(testField, "time-picker"); } - private void fireUnparsableChangeDomEvent() { - fireDomEvent("unparsable-change"); + private void fireChangeDomEvent() { + fireDomEvent("change", testField.getElement()); } - private void fireValueProgrammaticallySetDomEvent() { - fireDomEvent("value-programmatically-set"); + private void fireUnparsableChangeDomEvent() { + fireDomEvent("unparsable-change", testField.getElement()); } - private void fireDomEvent(String eventType) { - var domEvent = new DomEvent(testField.getElement(), eventType, - Json.createObject()); - testField.getElement().getNode().getFeature(ElementListenerMap.class) + private void fireDomEvent(String eventType, Element element) { + var domEvent = new DomEvent(element, eventType, Json.createObject()); + element.getNode().getFeature(ElementListenerMap.class) .fireEvent(domEvent); } } diff --git a/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java b/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java index eba96c5610b..38b4037af40 100644 --- a/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java +++ b/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java @@ -148,7 +148,7 @@ public class TimePicker private Validator defaultValidator = (value, context) -> { boolean fromComponent = context == null; - if (unparsableValue != null) { + if (isInputUnparsable()) { return ValidationResult.error(getI18nErrorMessage( TimePickerI18n::getBadInputErrorMessage)); } @@ -364,7 +364,7 @@ public void setLabel(String label) { @Override public void setValue(LocalTime value) { LocalTime oldValue = getValue(); - if (oldValue == null && value == null && unparsableValue != null) { + if (oldValue == null && value == null && isInputUnparsable()) { // When the value is programmatically cleared while the field // contains an unparsable input, ValueChangeEvent isn't fired, // so we need to call setModelValue manually to clear the bad @@ -480,10 +480,21 @@ private void fireValidationStatusChangeEvent() { * @return true if the input element's value is populated, * false otherwise */ + @Deprecated(since = "24.8") protected boolean isInputValuePresent() { return !getInputElementValue().isEmpty(); } + /** + * Returns whether the input value is unparsable. + * + * @return true if the input element's value is populated and + * unparsable, false otherwise + */ + protected boolean isInputUnparsable() { + return unparsableValue != null; + } + /** * Gets the value of the input element. This value is updated on the server * when the web component dispatches a `change` or `unparsable-change` @@ -495,7 +506,7 @@ protected boolean isInputValuePresent() { */ @Synchronize(property = "_inputElementValue", value = { "change", "unparsable-change" }) - protected String getInputElementValue() { + private String getInputElementValue() { return getElement().getProperty("_inputElementValue", ""); } From 9f4b4fd996ccf667892ce0ec481e7c8923265549 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Mon, 5 May 2025 23:09:19 +0300 Subject: [PATCH 4/9] chore: add deprecated message --- .../java/com/vaadin/flow/component/timepicker/TimePicker.java | 1 + 1 file changed, 1 insertion(+) diff --git a/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java b/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java index 38b4037af40..01564b00f39 100644 --- a/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java +++ b/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java @@ -479,6 +479,7 @@ private void fireValidationStatusChangeEvent() { * * @return true if the input element's value is populated, * false otherwise + * @deprecated Since v24.8 */ @Deprecated(since = "24.8") protected boolean isInputValuePresent() { From 266c5fae0ff0b30e082c511b72ea7d80918d5bed Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Tue, 6 May 2025 09:03:42 +0300 Subject: [PATCH 5/9] test: convert validation on programmatically changed value tests to unit tests --- .../validation/BasicValidationPage.java | 12 ------- .../validation/BasicValidationIT.java | 14 -------- .../validation/BasicValidationTest.java | 32 +++++++++++++++++++ 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationPage.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationPage.java index d604ddf20b0..192b62c0d46 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationPage.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationPage.java @@ -28,8 +28,6 @@ public class BasicValidationPage public static final String MIN_INPUT = "min-input"; public static final String MAX_INPUT = "max-input"; public static final String CLEAR_VALUE_BUTTON = "clear-value-button"; - public static final String SET_VALUE_PROGRAMMATICALLY = "set-value-programmatically"; - public static final String CLEAR_AND_SET_VALUE_PROGRAMMATICALLY = "clear-and-set-value-programmatically"; public static final String REQUIRED_ERROR_MESSAGE = "Field is required"; public static final String BAD_INPUT_ERROR_MESSAGE = "Invalid date format"; @@ -64,16 +62,6 @@ public BasicValidationPage() { add(createButton(CLEAR_VALUE_BUTTON, "Clear value", event -> { testField.clear(); })); - - add(createButton(SET_VALUE_PROGRAMMATICALLY, - "Set value programmatically", - event -> testField.setValue(LocalDateTime.now()))); - - add(createButton(CLEAR_AND_SET_VALUE_PROGRAMMATICALLY, - "Clear and set value programmatically", event -> { - testField.clear(); - testField.setValue(LocalDateTime.now()); - })); } protected DateTimePicker createTestField() { diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java index 68e09189885..c9bd576f867 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java @@ -16,7 +16,6 @@ package com.vaadin.flow.component.datetimepicker.validation; import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.BAD_INPUT_ERROR_MESSAGE; -import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.CLEAR_AND_SET_VALUE_PROGRAMMATICALLY; import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.CLEAR_VALUE_BUTTON; import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.INCOMPLETE_INPUT_ERROR_MESSAGE; import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.MAX_ERROR_MESSAGE; @@ -25,7 +24,6 @@ import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.MIN_INPUT; import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.REQUIRED_BUTTON; import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.REQUIRED_ERROR_MESSAGE; -import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.SET_VALUE_PROGRAMMATICALLY; import org.junit.Before; import org.junit.Test; @@ -517,18 +515,6 @@ public void max_setDateOutOfRange_fieldValidatedOnce() { assertValidationCount(1); } - @Test - public void setValueProgrammatically_fieldValidatedOnce() { - clickElementWithJs(SET_VALUE_PROGRAMMATICALLY); - assertValidationCount(1); - } - - @Test - public void clearAndSetValueProgrammatically_fieldValidatedTwice() { - clickElementWithJs(CLEAR_AND_SET_VALUE_PROGRAMMATICALLY); - assertValidationCount(2); - } - private void setFieldInvalid() { setInputValue(dateInput, "INVALID"); setInputValue(timeInput, "INVALID"); diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java index aeeb0a33563..cf341828937 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java @@ -18,6 +18,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.Assert; import org.junit.Test; @@ -216,6 +217,23 @@ public void setInvalid_nestedPickersAreInvalid() { Assert.assertTrue(getTimePicker().isInvalid()); } + @Test + public void setValueProgrammatically_fieldValidatedOnce() { + var dateTimePicker = new TestDateTimePicker(); + dateTimePicker.setValue(LocalDateTime.now()); + Assert.assertEquals(1, dateTimePicker.getValidationCount()); + } + + @Test + public void clearValueProgrammatically_fieldValidatedOnce() { + var dateTimePicker = new TestDateTimePicker(); + dateTimePicker.setValue(LocalDateTime.now()); + var validationCount = dateTimePicker.getValidationCount(); + dateTimePicker.setValue(null); + Assert.assertEquals(validationCount + 1, + dateTimePicker.getValidationCount()); + } + @Override protected DateTimePicker createTestField() { return new DateTimePicker(); @@ -242,4 +260,18 @@ private void fireDomEvent(String eventType, Element element) { element.getNode().getFeature(ElementListenerMap.class) .fireEvent(domEvent); } + + private class TestDateTimePicker extends DateTimePicker { + private final AtomicInteger validationCount = new AtomicInteger(0); + + @Override + protected void validate() { + super.validate(); + validationCount.incrementAndGet(); + } + + public int getValidationCount() { + return validationCount.get(); + } + } } From 7eb7a2574955224b322dfa90cdcf7bad62fa5cba Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Tue, 6 May 2025 10:09:10 +0300 Subject: [PATCH 6/9] test: assert validation count on validation result tests --- .../validation/BasicValidationIT.java | 210 ++++++------------ 1 file changed, 66 insertions(+), 144 deletions(-) diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java index c9bd576f867..52dff5b280e 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java @@ -61,6 +61,7 @@ public void triggerBlur_assertValidity() { assertServerValid(); assertClientValid(); assertErrorMessage(null); + assertValidationCount(0); } @Test @@ -72,6 +73,7 @@ public void required_triggerBlur_assertValidity() { assertServerValid(); assertClientValid(); assertErrorMessage(null); + assertValidationCount(0); } @Test @@ -83,80 +85,84 @@ public void required_changeInputTemporarily_triggerBlur_assertValidity() { assertServerValid(); assertClientValid(); assertErrorMessage(null); + assertValidationCount(0); } @Test public void required_changeValue_assertValidity() { $("button").id(REQUIRED_BUTTON).click(); - setInputValue(dateInput, "1/1/2000"); - setInputValue(timeInput, "12:00"); + setValue("1/1/2000", "12:00"); assertServerValid(); assertClientValid(); assertErrorMessage(""); + assertValidationCount(1); - setInputValue(dateInput, "1/1/2000"); setInputValue(timeInput, ""); assertServerInvalid(); assertServerInvalid(); assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE); + assertValidationCount(1); setInputValue(dateInput, ""); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(REQUIRED_ERROR_MESSAGE); + assertValidationCount(1); setInputValue(timeInput, ""); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(REQUIRED_ERROR_MESSAGE); + assertValidationCount(0); - setFieldInvalid(); + setInputValue(timeInput, "INVALID"); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(BAD_INPUT_ERROR_MESSAGE); + assertValidationCount(1); - setInputValue(dateInput, ""); setInputValue(timeInput, ""); timeInput.sendKeys(Keys.TAB); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(REQUIRED_ERROR_MESSAGE); + assertValidationCount(1); } @Test public void min_changeValue_assertValidity() { $("input").id(MIN_INPUT).sendKeys("2000-02-02T12:00", Keys.ENTER); - setInputValue(dateInput, "1/1/2000"); - setInputValue(timeInput, "11:00"); + setValue("1/1/2000", "11:00"); assertClientInvalid(); assertServerInvalid(); assertErrorMessage(MIN_ERROR_MESSAGE); + assertValidationCount(2); setInputValue(dateInput, "2/2/2000"); - setInputValue(timeInput, "11:00"); assertClientInvalid(); assertServerInvalid(); assertErrorMessage(MIN_ERROR_MESSAGE); + assertValidationCount(1); - setInputValue(dateInput, "2/2/2000"); setInputValue(timeInput, "12:00"); assertClientValid(); assertServerValid(); assertErrorMessage(""); + assertValidationCount(1); - setInputValue(dateInput, "2/2/2000"); setInputValue(timeInput, "13:00"); assertClientValid(); assertServerValid(); assertErrorMessage(""); + assertValidationCount(1); - setInputValue(dateInput, "3/3/2000"); - setInputValue(timeInput, "11:00"); + setValue("3/3/2000", "11:00"); assertClientValid(); assertServerValid(); assertErrorMessage(""); + assertValidationCount(2); } @Test @@ -167,75 +173,78 @@ public void max_changeDateInputValue_assertValidity() { assertClientInvalid(); assertServerInvalid(); assertErrorMessage(MAX_ERROR_MESSAGE); + assertValidationCount(1); - setInputValue(dateInput, "2/2/2000"); - setInputValue(timeInput, "13:00"); + setValue("2/2/2000", "13:00"); assertClientInvalid(); assertServerInvalid(); assertErrorMessage(MAX_ERROR_MESSAGE); + assertValidationCount(2); - setInputValue(dateInput, "2/2/2000"); setInputValue(timeInput, "12:00"); assertClientValid(); assertServerValid(); assertErrorMessage(""); + assertValidationCount(1); - setInputValue(dateInput, "2/2/2000"); setInputValue(timeInput, "11:00"); assertClientValid(); assertServerValid(); assertErrorMessage(""); + assertValidationCount(1); - setInputValue(dateInput, "1/1/2000"); - setInputValue(timeInput, "13:00"); + setValue("1/1/2000", "13:00"); assertClientValid(); assertServerValid(); assertErrorMessage(""); + assertValidationCount(2); } @Test public void setValue_clearValue_assertValidity() { - setInputValue(dateInput, "1/1/2000"); - setInputValue(timeInput, "10:00"); + setValue("1/1/2000", "10:00"); assertServerValid(); assertClientValid(); assertErrorMessage(""); + assertValidationCount(1); $("button").id(CLEAR_VALUE_BUTTON).click(); assertServerValid(); assertClientValid(); assertErrorMessage(""); + assertValidationCount(1); } @Test public void badInput_changeValue_assertValidity() { - setFieldInvalid(); - setInputValue(timeInput, "INVALID"); + setValue("1/1/2000", "INVALID"); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(BAD_INPUT_ERROR_MESSAGE); + assertValidationCount(1); - setInputValue(dateInput, "1/1/2000"); setInputValue(timeInput, "10:00"); assertServerValid(); assertClientValid(); assertErrorMessage(""); + assertValidationCount(1); - setFieldInvalid(); - setInputValue(timeInput, "INVALID"); + setInputValue(dateInput, "INVALID"); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(BAD_INPUT_ERROR_MESSAGE); + assertValidationCount(1); } @Test public void badInput_setDateInputValue_blur_assertValidity() { - setFieldInvalid(); + setInputValue(dateInput, "INVALID"); dateInput.sendKeys(Keys.TAB); timeInput.sendKeys(Keys.TAB); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(BAD_INPUT_ERROR_MESSAGE); + assertValidationCount(1); } @Test @@ -245,35 +254,39 @@ public void badInput_setTimeInputValue_blur_assertValidity() { assertServerInvalid(); assertClientInvalid(); assertErrorMessage(BAD_INPUT_ERROR_MESSAGE); + assertValidationCount(1); } @Test public void badInput_setValue_clearValue_assertValidity() { - setFieldInvalid(); - setInputValue(timeInput, "INVALID"); + setInputValue(dateInput, "INVALID"); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(BAD_INPUT_ERROR_MESSAGE); + assertValidationCount(1); $("button").id(CLEAR_VALUE_BUTTON).click(); assertServerValid(); assertClientValid(); assertErrorMessage(""); + assertValidationCount(1); } @Test public void badInput_setDateInputValue_blur_clearValue_assertValidity() { - setFieldInvalid(); + setInputValue(dateInput, "INVALID"); dateInput.sendKeys(Keys.TAB); timeInput.sendKeys(Keys.TAB); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(BAD_INPUT_ERROR_MESSAGE); + assertValidationCount(1); $("button").id(CLEAR_VALUE_BUTTON).click(); assertServerValid(); assertClientValid(); assertErrorMessage(""); + assertValidationCount(1); } @Test @@ -283,77 +296,80 @@ public void badInput_setTimeInputValue_blur_clearValue_assertValidity() { assertServerInvalid(); assertClientInvalid(); assertErrorMessage(BAD_INPUT_ERROR_MESSAGE); + assertValidationCount(1); $("button").id(CLEAR_VALUE_BUTTON).click(); assertServerValid(); assertClientValid(); assertErrorMessage(""); + assertValidationCount(1); } @Test public void incompleteInput_assertValidity() { setInputValue(dateInput, "1/1/2000"); - setInputValue(timeInput, ""); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE); + assertValidationCount(1); } @Test public void incompleteInput_changeToValidValue_assertValidity() { setInputValue(dateInput, "1/1/2000"); - setInputValue(timeInput, ""); + resetValidationCount(); - setInputValue(dateInput, "1/1/2001"); - setInputValue(timeInput, "10:00"); + setValue("1/1/2001", "10:00"); assertServerValid(); assertClientValid(); assertErrorMessage(""); + assertValidationCount(2); } @Test public void validInput_changeToIncompleteInput_assertValidity() { - setInputValue(dateInput, "1/1/2001"); - setInputValue(timeInput, "10:00"); + setValue("1/1/2001", "10:00"); + resetValidationCount(); - setInputValue(dateInput, "1/1/2000"); setInputValue(timeInput, ""); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE); + assertValidationCount(1); } @Test public void incompleteInput_setDateInputValue_blur_assertValidity() { setInputValue(dateInput, "1/1/2000"); - setInputValue(timeInput, ""); dateInput.sendKeys(Keys.TAB); timeInput.sendKeys(Keys.TAB); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE); + assertValidationCount(1); } @Test public void incompleteInput_setTimeInputValue_blur_assertValidity() { - setInputValue(dateInput, ""); setInputValue(timeInput, "10:00"); timeInput.sendKeys(Keys.TAB); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE); + assertValidationCount(1); } @Test public void incompleteInput_setValue_clearValue_assertValidity() { setInputValue(dateInput, "1/1/2000"); - setInputValue(timeInput, ""); timeInput.sendKeys(Keys.ENTER); + resetValidationCount(); $("button").id(CLEAR_VALUE_BUTTON).click(); assertServerValid(); assertClientValid(); assertErrorMessage(""); + assertValidationCount(1); } @Override @@ -364,31 +380,33 @@ protected void assertValidationCount(int expected) { @Test public void incompleteInput_setDateInputValue_blur_clearValue_assertValidity() { setInputValue(dateInput, "1/1/2000"); - setInputValue(timeInput, ""); dateInput.sendKeys(Keys.TAB); timeInput.sendKeys(Keys.TAB); + resetValidationCount(); $("button").id(CLEAR_VALUE_BUTTON).click(); assertServerValid(); assertClientValid(); assertErrorMessage(""); + assertValidationCount(1); } @Test public void incompleteInput_setTimeInputValue_blur_clearValue_assertValidity() { - setInputValue(dateInput, ""); setInputValue(timeInput, "10:00"); timeInput.sendKeys(Keys.TAB); + resetValidationCount(); $("button").id(CLEAR_VALUE_BUTTON).click(); assertServerValid(); assertClientValid(); assertErrorMessage(""); + assertValidationCount(1); } @Test public void detach_attach_preservesInvalidState() { - setFieldInvalid(); + setInputValue(dateInput, "INVALID"); detachAndReattachField(); @@ -418,121 +436,25 @@ public void detach_hide_attach_showAndInvalidate_preservesInvalidState() { @Test public void clientSideInvalidStateIsNotPropagatedToServer() { - setFieldInvalid(); + setInputValue(dateInput, "INVALID"); executeScript("arguments[0].invalid = false", testField); assertServerInvalid(); } - @Test - public void triggerBlurWithNoChange_fieldNotValidated() { - dateInput.sendKeys(Keys.TAB); - timeInput.sendKeys(Keys.TAB); - assertValidationCount(0); - } - - @Test - public void initiallyEmpty_setValidValue_fieldValidatedOnce() { - dateInput.sendKeys("1/1/2000"); - dateInput.sendKeys(Keys.ENTER); - dateInput.sendKeys(Keys.TAB); - timeInput.sendKeys("10:00"); - timeInput.sendKeys(Keys.ENTER); - assertValidationCount(1); - } - - @Test - public void initiallyEmpty_setInvalidValue_fieldNotValidatedOnce() { - dateInput.sendKeys("Invalid"); - dateInput.sendKeys(Keys.ENTER); - assertValidationCount(1); - } - - @Test - public void initiallyValidValue_clearValue_fieldValidatedOnce() { - dateInput.sendKeys("1/1/2000"); - dateInput.sendKeys(Keys.ENTER); - dateInput.sendKeys(Keys.TAB); - timeInput.sendKeys("10:00"); - timeInput.sendKeys(Keys.ENTER); - assertValidationCount(1); - clearInputValue(); - assertValidationCount(1); + protected DateTimePickerElement getTestField() { + return $(DateTimePickerElement.class).first(); } - @Test - public void initiallyValidValue_changeValue_fieldValidatedOnce() { - dateInput.sendKeys("1/1/2000"); - dateInput.sendKeys(Keys.ENTER); + private void setValue(String dateValue, String timeValue) { + setInputValue(dateInput, dateValue); dateInput.sendKeys(Keys.TAB); - timeInput.sendKeys("10:00"); - timeInput.sendKeys(Keys.ENTER); - assertValidationCount(1); - dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); - dateInput.sendKeys("1/1/2001"); - dateInput.sendKeys(Keys.ENTER); - assertValidationCount(1); - } - - @Test - public void initiallyInvalidValue_changeInvalidValue_fieldValidatedOnce() { - dateInput.sendKeys("Invalid"); - dateInput.sendKeys(Keys.ENTER); - assertValidationCount(1); - dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); - dateInput.sendKeys("Not a date"); - dateInput.sendKeys(Keys.ENTER); - assertValidationCount(1); - } - - @Test - public void initiallyInvalidValue_setValidValue_fieldValidatedOnce() { - dateInput.sendKeys("Invalid"); - dateInput.sendKeys(Keys.ENTER); - assertValidationCount(1); - dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); - dateInput.sendKeys("1/1/2000"); - timeInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); - timeInput.sendKeys("10:00"); - timeInput.sendKeys(Keys.ENTER); - assertValidationCount(1); - } - - @Test - public void initiallyInvalidValue_clearValue_fieldValidatedOnce() { - dateInput.sendKeys("Invalid"); - dateInput.sendKeys(Keys.ENTER); - assertValidationCount(1); - clearInputValue(); - assertValidationCount(1); - } - - @Test - public void max_setDateOutOfRange_fieldValidatedOnce() { - $("input").id(MAX_INPUT).sendKeys("2000-02-02T12:00", Keys.ENTER); - setInputValue(dateInput, "3/3/2000"); - assertValidationCount(1); - } - - private void setFieldInvalid() { - setInputValue(dateInput, "INVALID"); - setInputValue(timeInput, "INVALID"); - } - - protected DateTimePickerElement getTestField() { - return $(DateTimePickerElement.class).first(); + setInputValue(timeInput, timeValue); } private void setInputValue(TestBenchElement input, String value) { input.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); input.sendKeys(value, Keys.ENTER); } - - private void clearInputValue() { - dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); - dateInput.sendKeys(Keys.TAB); - timeInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); - timeInput.sendKeys(Keys.ENTER); - } } From 02507d418467ee83295dfb4cb890751cc71317d6 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Tue, 6 May 2025 10:51:28 +0300 Subject: [PATCH 7/9] test: add validation count assertions to binder validation tests --- .../validation/BinderValidationPage.java | 8 ++- .../validation/BinderValidationIT.java | 61 +++++++++++-------- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationPage.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationPage.java index 983351f1bd3..2637bc9f908 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationPage.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationPage.java @@ -90,6 +90,12 @@ public BinderValidationPage() { } protected DateTimePicker createTestField() { - return new DateTimePicker(); + return new DateTimePicker() { + @Override + protected void validate() { + super.validate(); + incrementServerValidationCounter(); + } + }; } } diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationIT.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationIT.java index 0a059744371..2185825c3b0 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationIT.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationIT.java @@ -62,6 +62,7 @@ public void required_triggerDateInputBlur_assertValidity() { assertServerValid(); assertClientValid(); assertErrorMessage(null); + assertValidationCount(0); } @Test @@ -70,6 +71,7 @@ public void required_triggerTimeInputBlur_assertValidity() { assertServerValid(); assertClientValid(); assertErrorMessage(null); + assertValidationCount(0); } @Test @@ -77,33 +79,35 @@ public void required_changeValue_assertValidity() { $("input").id(EXPECTED_VALUE_INPUT).sendKeys("2000-01-01T12:00", Keys.ENTER); - setInputValue(dateInput, "1/1/2000"); - setInputValue(timeInput, "12:00"); + setValue("1/1/2000", "12:00"); assertServerValid(); assertClientValid(); + assertValidationCount(1); setInputValue(dateInput, ""); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE); + assertValidationCount(1); setInputValue(timeInput, ""); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(REQUIRED_ERROR_MESSAGE); + assertValidationCount(1); setInputValue(dateInput, "INVALID"); - setInputValue(timeInput, "INVALID"); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(BAD_INPUT_ERROR_MESSAGE); + assertValidationCount(1); setInputValue(dateInput, ""); - setInputValue(timeInput, ""); timeInput.sendKeys(Keys.TAB); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(REQUIRED_ERROR_MESSAGE); + assertValidationCount(1); } @Test @@ -112,34 +116,35 @@ public void min_changeValue_assertValidity() { $("input").id(EXPECTED_VALUE_INPUT).sendKeys("2000-03-03T11:00", Keys.ENTER); - setInputValue(dateInput, "1/1/2000"); - setInputValue(timeInput, "11:00"); + setValue("1/1/2000", "11:00"); assertClientInvalid(); assertServerInvalid(); assertErrorMessage(MIN_ERROR_MESSAGE); + assertValidationCount(2); setInputValue(dateInput, "2/2/2000"); - setInputValue(timeInput, "11:00"); assertClientInvalid(); assertServerInvalid(); assertErrorMessage(MIN_ERROR_MESSAGE); + assertValidationCount(1); - setInputValue(dateInput, "2/2/2000"); setInputValue(timeInput, "12:00"); assertClientInvalid(); assertServerInvalid(); assertErrorMessage(UNEXPECTED_VALUE_ERROR_MESSAGE); + assertValidationCount(1); - setInputValue(dateInput, "2/2/2000"); setInputValue(timeInput, "13:00"); assertClientInvalid(); assertServerInvalid(); assertErrorMessage(UNEXPECTED_VALUE_ERROR_MESSAGE); + assertValidationCount(1); setInputValue(dateInput, "3/3/2000"); setInputValue(timeInput, "11:00"); assertClientValid(); assertServerValid(); + assertValidationCount(2); } @Test @@ -148,34 +153,34 @@ public void max_changeValue_assertValidity() { $("input").id(EXPECTED_VALUE_INPUT).sendKeys("2000-01-01T13:00", Keys.ENTER); - setInputValue(dateInput, "3/3/2000"); - setInputValue(timeInput, "13:00"); + setValue("3/3/2000", "13:00"); assertClientInvalid(); assertServerInvalid(); assertErrorMessage(MAX_ERROR_MESSAGE); + assertValidationCount(2); setInputValue(dateInput, "2/2/2000"); - setInputValue(timeInput, "13:00"); assertClientInvalid(); assertServerInvalid(); assertErrorMessage(MAX_ERROR_MESSAGE); + assertValidationCount(1); - setInputValue(dateInput, "2/2/2000"); setInputValue(timeInput, "12:00"); assertClientInvalid(); assertServerInvalid(); assertErrorMessage(UNEXPECTED_VALUE_ERROR_MESSAGE); + assertValidationCount(1); - setInputValue(dateInput, "2/2/2000"); setInputValue(timeInput, "11:00"); assertClientInvalid(); assertServerInvalid(); assertErrorMessage(UNEXPECTED_VALUE_ERROR_MESSAGE); + assertValidationCount(1); - setInputValue(dateInput, "1/1/2000"); - setInputValue(timeInput, "13:00"); + setValue("1/1/2000", "13:00"); assertClientValid(); assertServerValid(); + assertValidationCount(2); } @Test @@ -183,15 +188,16 @@ public void setValue_clearValue_assertValidity() { $("input").id(EXPECTED_VALUE_INPUT).sendKeys("2000-01-01T10:00", Keys.ENTER); - setInputValue(dateInput, "1/1/2000"); - setInputValue(timeInput, "10:00"); + setValue("1/1/2000", "10:00"); assertServerValid(); assertClientValid(); + assertValidationCount(1); $("button").id(CLEAR_VALUE_BUTTON).click(); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(REQUIRED_ERROR_MESSAGE); + assertValidationCount(1); } @Test @@ -199,42 +205,49 @@ public void badInput_changeValue_assertValidity() { $("input").id(EXPECTED_VALUE_INPUT).sendKeys("2000-01-01T10:00", Keys.ENTER); - setInputValue(dateInput, "INVALID"); - setInputValue(timeInput, "INVALID"); + setValue("1/1/2000", "INVALID"); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(BAD_INPUT_ERROR_MESSAGE); + assertValidationCount(1); - setInputValue(dateInput, "1/1/2000"); setInputValue(timeInput, "10:00"); assertServerValid(); assertClientValid(); + assertValidationCount(1); setInputValue(dateInput, "INVALID"); - setInputValue(timeInput, "INVALID"); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(BAD_INPUT_ERROR_MESSAGE); + assertValidationCount(1); } @Test public void badInput_setValue_clearValue_assertValidity() { - setInputValue(dateInput, "INVALID"); - setInputValue(timeInput, "INVALID"); + setValue("INVALID", "INVALID"); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(BAD_INPUT_ERROR_MESSAGE); + assertValidationCount(2); $("button").id(CLEAR_VALUE_BUTTON).click(); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(REQUIRED_ERROR_MESSAGE); + assertValidationCount(1); } protected DateTimePickerElement getTestField() { return $(DateTimePickerElement.class).first(); } + private void setValue(String dateValue, String timeValue) { + setInputValue(dateInput, dateValue); + dateInput.sendKeys(Keys.TAB); + setInputValue(timeInput, timeValue); + } + private void setInputValue(TestBenchElement input, String value) { input.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE); input.sendKeys(value, Keys.ENTER); From e99f76dbc07457b22e409a995652b63657835eab Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Tue, 6 May 2025 11:18:20 +0300 Subject: [PATCH 8/9] fix: provide correct validation state in value change listener --- .../flow/component/datetimepicker/DateTimePicker.java | 2 +- .../validation/BasicValidationTest.java | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java index c15aa6bb8af..ebba988ddd8 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java @@ -376,12 +376,12 @@ private void addValidationListeners() { public void setValue(LocalDateTime value) { var oldValue = getValue(); value = sanitizeValue(value); - super.setValue(value); var shouldFireValidationStatusChangeEvent = oldValue == null && value == null && (isInputUnparsable() || isInputIncomplete()); synchronizeChildComponentValues(value); validate(shouldFireValidationStatusChangeEvent); + super.setValue(value); } /** diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java index cf341828937..90d7032654b 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java @@ -18,6 +18,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Assert; @@ -234,6 +235,16 @@ public void clearValueProgrammatically_fieldValidatedOnce() { dateTimePicker.getValidationCount()); } + @Test + public void setValueProgrammatically_invalidStateIsUpdatedInValueChangeListener() { + var isInvalid = new AtomicBoolean(); + testField.addValueChangeListener( + e -> isInvalid.set(e.getSource().isInvalid())); + testField.setMax(LocalDateTime.now()); + testField.setValue(LocalDateTime.now().plusDays(1)); + Assert.assertTrue(isInvalid.get()); + } + @Override protected DateTimePicker createTestField() { return new DateTimePicker(); From f79669b0e172e7df87de216a59a24c4cbe5d0f83 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Wed, 7 May 2025 08:22:57 +0300 Subject: [PATCH 9/9] refactor: revert set value order change and validate on value change --- .../flow/component/datetimepicker/DateTimePicker.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java index ebba988ddd8..fac4ee30054 100644 --- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java +++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java @@ -354,7 +354,7 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) { } private void addValidationListeners() { - getElement().addEventListener("change", e -> validate(true)); + addValueChangeListener(e -> validate()); getElement().addEventListener("unparsable-change", e -> validate(true)); } @@ -379,9 +379,11 @@ public void setValue(LocalDateTime value) { var shouldFireValidationStatusChangeEvent = oldValue == null && value == null && (isInputUnparsable() || isInputIncomplete()); - synchronizeChildComponentValues(value); - validate(shouldFireValidationStatusChangeEvent); super.setValue(value); + synchronizeChildComponentValues(value); + if (shouldFireValidationStatusChangeEvent) { + validate(true); + } } /**