/* * Copyright 2000-2016 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.vaadin.data; import java.time.LocalDate; import java.util.List; import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import com.vaadin.data.Binder.Binding; import com.vaadin.data.Binder.BindingBuilder; import com.vaadin.data.BindingValidationStatus.Status; import com.vaadin.data.converter.StringToIntegerConverter; import com.vaadin.data.validator.EmailValidator; import com.vaadin.data.validator.StringLengthValidator; import com.vaadin.server.AbstractErrorMessage; import com.vaadin.ui.Button; import com.vaadin.ui.DateField; import com.vaadin.ui.Label; import com.vaadin.ui.Notification; import com.vaadin.ui.Slider; import com.vaadin.ui.TextField; /** * Book of Vaadin tests. * * @author Vaadin Ltd * */ public class BinderBookOfVaadinTest { private static class BookPerson { private String lastName; private String email, phone, title; private int yearOfBirth, salaryLevel; public BookPerson(int yearOfBirth, int salaryLevel) { this.yearOfBirth = yearOfBirth; this.salaryLevel = salaryLevel; } public BookPerson(BookPerson origin) { this(origin.yearOfBirth, origin.salaryLevel); lastName = origin.lastName; email = origin.email; phone = origin.phone; title = origin.title; } public BookPerson(String name, int yearOfBirth) { lastName = name; this.yearOfBirth = yearOfBirth; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public int getYearOfBirth() { return yearOfBirth; } public void setYearOfBirth(int yearOfBirth) { this.yearOfBirth = yearOfBirth; } public int getSalaryLevel() { return salaryLevel; } public void setSalaryLevel(int salaryLevel) { this.salaryLevel = salaryLevel; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } } public static class Trip { private LocalDate returnDate; public LocalDate getReturnDate() { return returnDate; } public void setReturnDate(LocalDate returnDate) { this.returnDate = returnDate; } } private Binder<BookPerson> binder; private TextField field; private TextField phoneField; private TextField emailField; @Before public void setUp() { binder = new Binder<>(); field = new TextField(); phoneField = new TextField(); emailField = new TextField(); // make sure the test is not locale dependent field.setLocale(Locale.US); phoneField.setLocale(Locale.US); emailField.setLocale(Locale.US); } @Test public void loadingFromBusinessObjects() { // this test is just to make sure the code snippet in the book compiles binder.readBean(new BookPerson(1969, 50000)); BinderValidationStatus<BookPerson> status = binder.validate(); if (status.hasErrors()) { Notification.show("Validation error count: " + status.getValidationErrors().size()); } } @Test public void handlingCheckedException() { // another test just to verify that book examples actually compile try { binder.writeBean(new BookPerson(2000, 50000)); } catch (ValidationException e) { Notification.show("Validation error count: " + e.getValidationErrors().size()); } } @Test public void simpleEmailValidator() { binder.forField(field) // Explicit validator instance .withValidator(new EmailValidator( "This doesn't look like a valid email address")) .bind(BookPerson::getEmail, BookPerson::setEmail); field.setValue("not-email"); BinderValidationStatus<?> status = binder.validate(); Assert.assertEquals(1, status.getFieldValidationErrors().size()); Assert.assertEquals("This doesn't look like a valid email address", status.getFieldValidationErrors().get(0).getMessage().get()); Assert.assertEquals("This doesn't look like a valid email address", ((AbstractErrorMessage) field.getErrorMessage()).getMessage()); field.setValue("abc@vaadin.com"); status = binder.validate(); Assert.assertEquals(0, status.getBeanValidationErrors().size()); Assert.assertNull(field.getErrorMessage()); } @Test public void nameLengthTest() { binder.forField(field) // Validator defined based on a lambda and an error message .withValidator(name -> name.length() >= 3, "Last name must contain at least three characters") .bind(BookPerson::getLastName, BookPerson::setLastName); field.setValue("a"); BinderValidationStatus<?> status = binder.validate(); Assert.assertEquals(1, status.getFieldValidationErrors().size()); Assert.assertEquals("Last name must contain at least three characters", status.getFieldValidationErrors().get(0).getMessage().get()); Assert.assertEquals("Last name must contain at least three characters", ((AbstractErrorMessage) field.getErrorMessage()).getMessage()); field.setValue("long last name"); status = binder.validate(); Assert.assertEquals(0, status.getFieldValidationErrors().size()); Assert.assertNull(field.getErrorMessage()); } @Test public void chainedEmailValidator() { binder.forField(field) // Explicit validator instance .withValidator(new EmailValidator( "This doesn't look like a valid email address")) .withValidator(email -> email.endsWith("@acme.com"), "Only acme.com email addresses are allowed") .bind(BookPerson::getEmail, BookPerson::setEmail); field.setValue("not-email"); BinderValidationStatus<?> status = binder.validate(); // Only one error per field should be reported Assert.assertEquals(1, status.getFieldValidationErrors().size()); Assert.assertEquals("This doesn't look like a valid email address", status.getFieldValidationErrors().get(0).getMessage().get()); Assert.assertEquals("This doesn't look like a valid email address", ((AbstractErrorMessage) field.getErrorMessage()).getMessage()); field.setValue("abc@vaadin.com"); status = binder.validate(); Assert.assertEquals(1, status.getFieldValidationErrors().size()); Assert.assertEquals("Only acme.com email addresses are allowed", status.getFieldValidationErrors().get(0).getMessage().get()); Assert.assertEquals("Only acme.com email addresses are allowed", ((AbstractErrorMessage) field.getErrorMessage()).getMessage()); field.setValue("abc@acme.com"); status = binder.validate(); Assert.assertEquals(0, status.getFieldValidationErrors().size()); Assert.assertNull(field.getErrorMessage()); } @Test public void converterBookOfVaadinExample1() { TextField yearOfBirthField = new TextField(); yearOfBirthField.setLocale(Locale.US); // Slider for integers between 1 and 10 Slider salaryLevelField = new Slider("Salary level", 1, 10); BindingBuilder<BookPerson, String> b1 = binder .forField(yearOfBirthField); BindingBuilder<BookPerson, Integer> b2 = b1.withConverter( new StringToIntegerConverter("Must enter a number")); b2.bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth); BindingBuilder<BookPerson, Double> salaryBinding1 = binder .forField(salaryLevelField); BindingBuilder<BookPerson, Integer> salaryBinding2 = salaryBinding1 .withConverter(Double::intValue, Integer::doubleValue); salaryBinding2.bind(BookPerson::getSalaryLevel, BookPerson::setSalaryLevel); // Test that the book code works BookPerson bookPerson = new BookPerson(1972, 4); binder.setBean(bookPerson); Assert.assertEquals(4.0, salaryLevelField.getValue().doubleValue(), 0); Assert.assertEquals("1,972", yearOfBirthField.getValue()); bookPerson.setSalaryLevel(8); binder.readBean(bookPerson); Assert.assertEquals(8.0, salaryLevelField.getValue().doubleValue(), 0); bookPerson.setYearOfBirth(123); binder.readBean(bookPerson); Assert.assertEquals("123", yearOfBirthField.getValue()); yearOfBirthField.setValue("2016"); salaryLevelField.setValue(1.0); Assert.assertEquals(2016, bookPerson.getYearOfBirth()); Assert.assertEquals(1, bookPerson.getSalaryLevel()); } @Test public void converterBookOfVaadinExample2() { TextField yearOfBirthField = new TextField(); binder.forField(yearOfBirthField) .withConverter(Integer::valueOf, String::valueOf, // Text to use instead of the NumberFormatException // message "Please enter a number") .bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth); binder.setBean(new BookPerson(1900, 5)); yearOfBirthField.setValue("abc"); binder.validate(); Assert.assertEquals("Please enter a number", yearOfBirthField.getComponentError().getFormattedHtmlMessage()); } @Test public void crossFieldValidation_validateUsingBinder() { Binder<Trip> binder = new Binder<>(); DateField departing = new DateField("Departing"); DateField returning = new DateField("Returning"); Binding<Trip, LocalDate> returnBinding = binder.forField(returning) .withValidator( returnDate -> !returnDate .isBefore(departing.getValue()), "Cannot return before departing") .bind(Trip::getReturnDate, Trip::setReturnDate); departing.addValueChangeListener(event -> returnBinding.validate()); LocalDate past = LocalDate.now(); LocalDate before = past.plusDays(1); LocalDate after = before.plusDays(1); departing.setValue(before); returning.setValue(after); BinderValidationStatus<Trip> status = binder.validate(); Assert.assertTrue(status.getBeanValidationErrors().isEmpty()); Assert.assertNull(departing.getComponentError()); Assert.assertNull(returning.getComponentError()); // update returning => validation is done against this field returning.setValue(past); status = binder.validate(); Assert.assertFalse(status.getFieldValidationErrors().isEmpty()); Assert.assertNotNull(returning.getComponentError()); Assert.assertNull(departing.getComponentError()); // set correct value back returning.setValue(before); status = binder.validate(); Assert.assertTrue(status.getFieldValidationErrors().isEmpty()); Assert.assertNull(departing.getComponentError()); Assert.assertNull(returning.getComponentError()); // update departing => validation is done because of listener added departing.setValue(after); status = binder.validate(); Assert.assertFalse(status.getFieldValidationErrors().isEmpty()); Assert.assertNotNull(returning.getComponentError()); Assert.assertNull(departing.getComponentError()); } @Test public void crossFieldValidation_validateUsingBinding() { Binder<Trip> binder = new Binder<>(); DateField departing = new DateField("Departing"); DateField returning = new DateField("Returning"); Binding<Trip, LocalDate> returnBinding = binder.forField(returning) .withValidator( returnDate -> !returnDate .isBefore(departing.getValue()), "Cannot return before departing") .bind(Trip::getReturnDate, Trip::setReturnDate); departing.addValueChangeListener(event -> returnBinding.validate()); LocalDate past = LocalDate.now(); LocalDate before = past.plusDays(1); LocalDate after = before.plusDays(1); departing.setValue(before); returning.setValue(after); BindingValidationStatus<LocalDate> result = returnBinding.validate(); Assert.assertFalse(result.isError()); Assert.assertNull(departing.getComponentError()); // update returning => validation is done against this field returning.setValue(past); result = returnBinding.validate(); Assert.assertTrue(result.isError()); Assert.assertNotNull(returning.getComponentError()); // set correct value back returning.setValue(before); result = returnBinding.validate(); Assert.assertFalse(result.isError()); Assert.assertNull(departing.getComponentError()); // update departing => validation is done because of listener added departing.setValue(after); result = returnBinding.validate(); Assert.assertTrue(result.isError()); Assert.assertNotNull(returning.getComponentError()); } @Test public void withStatusLabelExample() { Label emailStatus = new Label(); String msg = "This doesn't look like a valid email address"; binder.forField(field).withValidator(new EmailValidator(msg)) .withStatusLabel(emailStatus) .bind(BookPerson::getEmail, BookPerson::setEmail); field.setValue("foo"); binder.validate(); Assert.assertTrue(emailStatus.isVisible()); Assert.assertEquals(msg, emailStatus.getValue()); field.setValue("foo@vaadin.com"); binder.validate(); Assert.assertFalse(emailStatus.isVisible()); Assert.assertEquals("", emailStatus.getValue()); } @Test public void withBindingStatusHandlerExample() { Label nameStatus = new Label(); AtomicReference<BindingValidationStatus<?>> statusCapture = new AtomicReference<>(); String msg = "Full name must contain at least three characters"; binder.forField(field).withValidator(name -> name.length() >= 3, msg) .withValidationStatusHandler(status -> { nameStatus.setValue(status.getMessage().orElse("")); // Only show the label when validation has failed boolean error = status.getStatus() == Status.ERROR; nameStatus.setVisible(error); statusCapture.set(status); }).bind(BookPerson::getLastName, BookPerson::setLastName); field.setValue("aa"); binder.validate(); Assert.assertTrue(nameStatus.isVisible()); Assert.assertEquals(msg, nameStatus.getValue()); Assert.assertNotNull(statusCapture.get()); BindingValidationStatus<?> status = statusCapture.get(); Assert.assertEquals(Status.ERROR, status.getStatus()); Assert.assertEquals(msg, status.getMessage().get()); Assert.assertEquals(field, status.getField()); field.setValue("foo"); binder.validate(); Assert.assertFalse(nameStatus.isVisible()); Assert.assertEquals("", nameStatus.getValue()); Assert.assertNotNull(statusCapture.get()); status = statusCapture.get(); Assert.assertEquals(Status.OK, status.getStatus()); Assert.assertFalse(status.getMessage().isPresent()); Assert.assertEquals(field, status.getField()); } @Test public void binder_saveIfValid() { Binder<BookPerson> binder = new Binder<>(BookPerson.class); // Phone or email has to be specified for the bean Validator<BookPerson> phoneOrEmail = Validator.from( personBean -> !"".equals(personBean.getPhone()) || !"".equals(personBean.getEmail()), "A person must have either a phone number or an email address"); binder.withValidator(phoneOrEmail); binder.forField(emailField).bind("email"); binder.forField(phoneField).bind("phone"); // Person person = // e.g. JPA entity or bean from Grid BookPerson person = new BookPerson(1900, 5); person.setEmail("Old Email"); // Load person data to a form binder.readBean(person); Button saveButton = new Button("Save", event -> { // Using saveIfValid to avoid the try-catch block that is // needed if using the regular save method if (binder.writeBeanIfValid(person)) { // Person is valid and updated // TODO Store in the database } }); emailField.setValue("foo@bar.com"); Assert.assertTrue(binder.writeBeanIfValid(person)); // Person updated Assert.assertEquals("foo@bar.com", person.getEmail()); emailField.setValue(""); Assert.assertFalse(binder.writeBeanIfValid(person)); // Person updated because phone and email are both empty Assert.assertEquals("foo@bar.com", person.getEmail()); } @Test public void manyConvertersAndValidators() throws ValidationException { TextField yearOfBirthField = new TextField(); binder.forField(yearOfBirthField) // Validator will be run with the String value of the field .withValidator(text -> text.length() == 4, "Doesn't look like a year") // Converter will only be run for strings with 4 characters .withConverter( new StringToIntegerConverter("Must enter a number")) // Validator will be run with the converted value .withValidator(year -> year >= 1900 && year <= 2000, "Person must be born in the 20th century") .bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth); yearOfBirthField.setValue("abc"); Assert.assertEquals("Doesn't look like a year", binder.validate() .getFieldValidationErrors().get(0).getMessage().get()); yearOfBirthField.setValue("abcd"); Assert.assertEquals("Must enter a number", binder.validate() .getFieldValidationErrors().get(0).getMessage().get()); yearOfBirthField.setValue("1200"); Assert.assertEquals("Person must be born in the 20th century", binder.validate().getFieldValidationErrors().get(0).getMessage() .get()); yearOfBirthField.setValue("1950"); Assert.assertFalse(binder.validate().hasErrors()); BookPerson person = new BookPerson(1500, 12); binder.writeBean(person); Assert.assertEquals(1950, person.getYearOfBirth()); } class MyConverter implements Converter<String, Integer> { @Override public Result<Integer> convertToModel(String fieldValue, ValueContext context) { // Produces a converted value or an error try { // ok is a static helper method that creates a Result return Result.ok(Integer.valueOf(fieldValue)); } catch (NumberFormatException e) { // error is a static helper method that creates a Result return Result.error("Please enter a number"); } } @Override public String convertToPresentation(Integer integer, ValueContext context) { // Converting to the field type should always succeed, // so there is no support for returning an error Result. return String.valueOf(integer); } } @Test public void bindUsingCustomConverter() { Binder<BookPerson> binder = new Binder<>(); TextField yearOfBirthField = new TextField(); // Using the converter binder.forField(yearOfBirthField).withConverter(new MyConverter()) .bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth); BookPerson p = new BookPerson(1500, 12); binder.setBean(p); yearOfBirthField.setValue("abc"); Assert.assertTrue(binder.validate().hasErrors()); Assert.assertEquals("Please enter a number", binder.validate() .getFieldValidationErrors().get(0).getMessage().get()); yearOfBirthField.setValue("123"); Assert.assertTrue(binder.validate().isOk()); p.setYearOfBirth(12500); binder.readBean(p); Assert.assertEquals("12500", yearOfBirthField.getValue()); Assert.assertTrue(binder.validate().isOk()); } @Test public void withBinderStatusLabelExample() { Label formStatusLabel = new Label(); Binder<BookPerson> binder = new Binder<>(BookPerson.class); binder.setStatusLabel(formStatusLabel); final String message = "Too young, son"; final String message2 = "Y2K error"; TextField yearOfBirth = new TextField(); BookPerson p = new BookPerson(1500, 12); binder.forField(yearOfBirth) .withConverter(new StringToIntegerConverter("err")) .bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth); binder.withValidator(bean -> bean.yearOfBirth < 2000, message) .withValidator(bean -> bean.yearOfBirth != 2000, message2); binder.setBean(p); // first bean validator fails and passes error message to status label yearOfBirth.setValue("2001"); BinderValidationStatus<?> status = binder.validate(); Assert.assertEquals(0, status.getFieldValidationErrors().size()); Assert.assertEquals(1, status.getBeanValidationErrors().size()); Assert.assertEquals(message, status.getBeanValidationErrors().get(0).getErrorMessage()); Assert.assertEquals(message, formStatusLabel.getValue()); // value is correct, status label is cleared yearOfBirth.setValue("1999"); status = binder.validate(); Assert.assertFalse(status.hasErrors()); Assert.assertEquals("", formStatusLabel.getValue()); // both bean validators fail, should be two error messages chained yearOfBirth.setValue("2000"); status = binder.validate(); Assert.assertEquals(2, status.getBeanValidationResults().size()); Assert.assertEquals(0, status.getFieldValidationErrors().size()); Assert.assertEquals(2, status.getBeanValidationErrors().size()); // only first error is shown Assert.assertEquals(message, formStatusLabel.getValue()); } @Test public void withBinderStatusHandlerExample() { Label formStatusLabel = new Label(); BinderValidationStatusHandler<BookPerson> defaultHandler = binder .getValidationStatusHandler(); binder.setValidationStatusHandler(status -> { // create an error message on failed bean level validations List<ValidationResult> errors = status.getBeanValidationErrors(); String errorMessage = errors.stream() .map(ValidationResult::getErrorMessage) .collect(Collectors.joining("\n")); // show error in a label formStatusLabel.setValue(errorMessage); formStatusLabel.setVisible(!errorMessage.isEmpty()); // Let the default handler show messages for each field defaultHandler.statusChange(status); }); final String bindingMessage = "uneven"; final String message = "Too young, son"; final String message2 = "Y2K error"; TextField yearOfBirth = new TextField(); BookPerson p = new BookPerson(1500, 12); binder.forField(yearOfBirth) .withConverter(new StringToIntegerConverter("err")) .withValidator(value -> value % 2 == 0, bindingMessage) .bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth); binder.withValidator(bean -> bean.yearOfBirth < 2000, message) .withValidator(bean -> bean.yearOfBirth != 2000, message2); binder.setBean(p); // first binding validation fails, no bean level validation is done yearOfBirth.setValue("2001"); BinderValidationStatus<?> status = binder.validate(); Assert.assertEquals(1, status.getFieldValidationErrors().size()); Assert.assertEquals(bindingMessage, status.getFieldValidationErrors().get(0).getMessage().get()); Assert.assertEquals("", formStatusLabel.getValue()); // first bean validator fails and passes error message to status label yearOfBirth.setValue("2002"); status = binder.validate(); Assert.assertEquals(0, status.getFieldValidationErrors().size()); Assert.assertEquals(1, status.getBeanValidationErrors().size()); Assert.assertEquals(message, status.getBeanValidationErrors().get(0).getErrorMessage()); Assert.assertEquals(message, formStatusLabel.getValue()); // value is correct, status label is cleared yearOfBirth.setValue("1998"); status = binder.validate(); Assert.assertTrue(status.isOk()); Assert.assertFalse(status.hasErrors()); Assert.assertEquals(0, status.getFieldValidationErrors().size()); Assert.assertEquals(0, status.getBeanValidationErrors().size()); Assert.assertEquals("", formStatusLabel.getValue()); // both bean validators fail, should be two error messages chained yearOfBirth.setValue("2000"); status = binder.validate(); Assert.assertEquals(0, status.getFieldValidationErrors().size()); Assert.assertEquals(2, status.getBeanValidationErrors().size()); Assert.assertEquals(message + "\n" + message2, formStatusLabel.getValue()); } @Test public void statusChangeListener_binderIsNotBound() { Button saveButton = new Button(); Button resetButton = new Button(); AtomicBoolean eventIsFired = new AtomicBoolean(false); binder.addStatusChangeListener(event -> { boolean isValid = event.getBinder().isValid(); boolean hasChanges = event.getBinder().hasChanges(); eventIsFired.set(true); saveButton.setEnabled(hasChanges && isValid); resetButton.setEnabled(hasChanges); }); binder.forField(field) .withValidator(new StringLengthValidator("", 1, 3)) .bind(BookPerson::getLastName, BookPerson::setLastName); // no changes Assert.assertFalse(saveButton.isEnabled()); Assert.assertFalse(resetButton.isEnabled()); verifyEventIsFired(eventIsFired); BookPerson person = new BookPerson(2000, 1); binder.readBean(person); // no changes Assert.assertFalse(saveButton.isEnabled()); Assert.assertFalse(resetButton.isEnabled()); verifyEventIsFired(eventIsFired); field.setValue("a"); // binder is not bound, no event fired // no changes: see #375. There should be a change and enabled state Assert.assertTrue(saveButton.isEnabled()); Assert.assertTrue(resetButton.isEnabled()); Assert.assertTrue(eventIsFired.get()); binder.writeBeanIfValid(person); // no changes Assert.assertFalse(saveButton.isEnabled()); Assert.assertFalse(resetButton.isEnabled()); verifyEventIsFired(eventIsFired); binder.validate(); // no changes Assert.assertFalse(saveButton.isEnabled()); Assert.assertFalse(resetButton.isEnabled()); verifyEventIsFired(eventIsFired); field.setValue(""); // binder is not bound, no event fired // no changes: see #375. There should be a change and disabled state for // save button because of failed validation Assert.assertFalse(saveButton.isEnabled()); Assert.assertTrue(resetButton.isEnabled()); Assert.assertTrue(eventIsFired.get()); } @Test public void statusChangeListener_binderIsBound() { Button saveButton = new Button(); Button resetButton = new Button(); AtomicBoolean eventIsFired = new AtomicBoolean(false); binder.addStatusChangeListener(event -> { boolean isValid = event.getBinder().isValid(); boolean hasChanges = event.getBinder().hasChanges(); eventIsFired.set(true); saveButton.setEnabled(hasChanges && isValid); resetButton.setEnabled(hasChanges); }); binder.forField(field) .withValidator(new StringLengthValidator("", 1, 3)) .bind(BookPerson::getLastName, BookPerson::setLastName); // no changes Assert.assertFalse(saveButton.isEnabled()); Assert.assertFalse(resetButton.isEnabled()); verifyEventIsFired(eventIsFired); BookPerson person = new BookPerson(2000, 1); binder.setBean(person); // no changes Assert.assertFalse(saveButton.isEnabled()); Assert.assertFalse(resetButton.isEnabled()); verifyEventIsFired(eventIsFired); field.setValue("a"); // there are valid changes verifyEventIsFired(eventIsFired); field.setValue(""); // there are invalid changes Assert.assertFalse(saveButton.isEnabled()); Assert.assertTrue(resetButton.isEnabled()); verifyEventIsFired(eventIsFired); // set valid value field.setValue("a"); verifyEventIsFired(eventIsFired); binder.writeBeanIfValid(person); // there are no changes. Assert.assertFalse(saveButton.isEnabled()); Assert.assertFalse(resetButton.isEnabled()); verifyEventIsFired(eventIsFired); } @Test public void statusChangeListener_multipleRequiredFields() { Button saveButton = new Button(); binder.addStatusChangeListener(event -> { boolean isValid = event.getBinder().isValid(); boolean hasChanges = event.getBinder().hasChanges(); saveButton.setEnabled(hasChanges && isValid); }); binder.forField(field).asRequired("").bind(BookPerson::getLastName, BookPerson::setLastName); binder.forField(emailField).asRequired("").bind(BookPerson::getEmail, BookPerson::setEmail); Assert.assertFalse(saveButton.isEnabled()); field.setValue("not empty"); Assert.assertFalse(saveButton.isEnabled()); emailField.setValue("not empty"); Assert.assertTrue(saveButton.isEnabled()); field.clear(); Assert.assertFalse(saveButton.isEnabled()); } @Test public void writeBean_throwsValidationException_bookExampleShouldCompile() { // The person to edit // Would be loaded from the backend in a real application BookPerson person = new BookPerson("John Doe", 1957); // Updates the value in each bound field component binder.readBean(person); Button saveButton = new Button("Save", event -> { try { binder.writeBean(person); // A real application would also save the updated person // using the application's backend } catch (ValidationException e) { Notification.show("Person could not be saved, " + "please check error messages for each field."); } }); // Updates the fields again with the previously saved values Button resetButton = new Button("Reset", event -> binder.readBean(person)); } private void verifyEventIsFired(AtomicBoolean flag) { Assert.assertTrue(flag.get()); flag.set(false); } }