diff --git a/README.md b/README.md index 4ba3a6c10..e21394500 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ __MVVM__ is the enhanced version of the [Presentation Model](http://martinfowler de.saxsys mvvmfx - 1.2.1 + 1.3.1 ``` @@ -24,7 +24,7 @@ __MVVM__ is the enhanced version of the [Presentation Model](http://martinfowler de.saxsys mvvmfx - 1.3.0-SNAPSHOT + 1.4.0-SNAPSHOT ``` @@ -36,9 +36,9 @@ If you need help you can use the forums on [Google Groups](https://groups.google ### Links - [Project Page](http://sialcasa.github.io/mvvmFX/) -- [javadoc mvvmfx core](http://sialcasa.github.io/mvvmFX/javadoc/1.2.0/mvvmfx/) -- [javadoc mvvmfx-cdi](http://sialcasa.github.io/mvvmFX/javadoc/1.2.0/mvvmfx-cdi/) -- [javadoc mvvmfx-guice](http://sialcasa.github.io/mvvmFX/javadoc/1.2.0/mvvmfx-guice/) -- [javadoc mvvmfx-utils](http://sialcasa.github.io/mvvmFX/javadoc/1.2.0/mvvmfx-utils/) -- [javadoc mvvmfx-testing-utils](http://sialcasa.github.io/mvvmFX/javadoc/1.2.0/mvvmfx-testing-utils/) +- [javadoc mvvmfx core](http://sialcasa.github.io/mvvmFX/javadoc/1.3.0/mvvmfx/) +- [javadoc mvvmfx-cdi](http://sialcasa.github.io/mvvmFX/javadoc/1.3.0/mvvmfx-cdi/) +- [javadoc mvvmfx-guice](http://sialcasa.github.io/mvvmFX/javadoc/1.3.0/mvvmfx-guice/) +- [javadoc mvvmfx-utils](http://sialcasa.github.io/mvvmFX/javadoc/1.3.0/mvvmfx-utils/) +- [javadoc mvvmfx-testing-utils](http://sialcasa.github.io/mvvmFX/javadoc/1.3.0/mvvmfx-testing-utils/) diff --git a/examples/mvvmfx-books-example/pom.xml b/examples/mvvmfx-books-example/pom.xml index e6ea1c156..3bbbb5d1b 100644 --- a/examples/mvvmfx-books-example/pom.xml +++ b/examples/mvvmfx-books-example/pom.xml @@ -5,7 +5,7 @@ mvvmfx-examples de.saxsys - 1.3.0 + 1.3.1 4.0.0 @@ -21,7 +21,6 @@ de.saxsys mvvmfx - ${project.parent.version} eu.lestard diff --git a/examples/mvvmfx-cdi-starter/pom.xml b/examples/mvvmfx-cdi-starter/pom.xml index 516461afd..28c6c7edc 100644 --- a/examples/mvvmfx-cdi-starter/pom.xml +++ b/examples/mvvmfx-cdi-starter/pom.xml @@ -12,7 +12,7 @@ de.saxsys mvvmfx-examples - 1.3.0 + 1.3.1 @@ -31,17 +31,14 @@ de.saxsys mvvmfx - ${project.parent.version} de.saxsys mvvmfx-cdi - ${project.parent.version} de.saxsys mvvmfx-complex - ${project.parent.version} org.jboss diff --git a/examples/mvvmfx-complex-example/pom.xml b/examples/mvvmfx-complex-example/pom.xml index 6b4f1b082..d1141c813 100644 --- a/examples/mvvmfx-complex-example/pom.xml +++ b/examples/mvvmfx-complex-example/pom.xml @@ -7,7 +7,7 @@ de.saxsys mvvmfx-examples - 1.3.0 + 1.3.1 UTF-8 @@ -45,7 +45,6 @@ de.saxsys mvvmfx - ${project.parent.version} com.google.guava diff --git a/examples/mvvmfx-contacts/pom.xml b/examples/mvvmfx-contacts/pom.xml index 27eb9fd7c..2026c7c78 100644 --- a/examples/mvvmfx-contacts/pom.xml +++ b/examples/mvvmfx-contacts/pom.xml @@ -6,7 +6,7 @@ mvvmfx-examples de.saxsys - 1.3.0 + 1.3.1 mvvmfx-contacts @@ -33,12 +33,10 @@ de.saxsys mvvmfx - ${project.parent.version} de.saxsys mvvmfx-cdi - ${project.parent.version} @@ -117,7 +115,6 @@ de.saxsys mvvmfx-testing-utils - ${project.parent.version} test diff --git a/examples/mvvmfx-fx-root-example/pom.xml b/examples/mvvmfx-fx-root-example/pom.xml index 3db18415e..85e84e10d 100644 --- a/examples/mvvmfx-fx-root-example/pom.xml +++ b/examples/mvvmfx-fx-root-example/pom.xml @@ -7,7 +7,7 @@ de.saxsys mvvmfx-examples - 1.3.0 + 1.3.1 @@ -21,7 +21,6 @@ de.saxsys mvvmfx - ${project.parent.version} diff --git a/examples/mvvmfx-guice-starter/pom.xml b/examples/mvvmfx-guice-starter/pom.xml index c0b520f6c..e6a22de70 100644 --- a/examples/mvvmfx-guice-starter/pom.xml +++ b/examples/mvvmfx-guice-starter/pom.xml @@ -12,7 +12,7 @@ de.saxsys mvvmfx-examples - 1.3.0 + 1.3.1 UTF-8 @@ -45,17 +45,14 @@ de.saxsys mvvmfx - ${project.parent.version} de.saxsys mvvmfx-guice - ${project.parent.version} de.saxsys mvvmfx-complex - ${project.parent.version} org.slf4j diff --git a/examples/mvvmfx-helloworld-without-fxml/pom.xml b/examples/mvvmfx-helloworld-without-fxml/pom.xml index 43876d299..13b48b13c 100644 --- a/examples/mvvmfx-helloworld-without-fxml/pom.xml +++ b/examples/mvvmfx-helloworld-without-fxml/pom.xml @@ -7,7 +7,7 @@ de.saxsys mvvmfx-examples - 1.3.0 + 1.3.1 @@ -21,7 +21,6 @@ de.saxsys mvvmfx - ${project.parent.version} diff --git a/examples/mvvmfx-helloworld/pom.xml b/examples/mvvmfx-helloworld/pom.xml index 3f92c72e0..5db2635b7 100644 --- a/examples/mvvmfx-helloworld/pom.xml +++ b/examples/mvvmfx-helloworld/pom.xml @@ -6,7 +6,7 @@ de.saxsys mvvmfx-examples - 1.3.0 + 1.3.1 UTF-8 @@ -19,7 +19,6 @@ de.saxsys mvvmfx - ${project.parent.version} diff --git a/examples/mvvmfx-synchronizefx/pom.xml b/examples/mvvmfx-synchronizefx/pom.xml index df20339ec..6ac0fe19e 100644 --- a/examples/mvvmfx-synchronizefx/pom.xml +++ b/examples/mvvmfx-synchronizefx/pom.xml @@ -6,7 +6,7 @@ de.saxsys mvvmfx-examples - 1.3.0 + 1.3.1 UTF-8 @@ -19,7 +19,6 @@ de.saxsys mvvmfx - ${project.parent.version} de.saxsys.synchronizefx diff --git a/examples/mvvmfx-todomvc/pom.xml b/examples/mvvmfx-todomvc/pom.xml index f9496e218..d0d09324f 100644 --- a/examples/mvvmfx-todomvc/pom.xml +++ b/examples/mvvmfx-todomvc/pom.xml @@ -5,7 +5,7 @@ mvvmfx-examples de.saxsys - 1.3.0 + 1.3.1 4.0.0 @@ -15,7 +15,6 @@ de.saxsys mvvmfx - ${project.parent.version} org.fxmisc.easybind diff --git a/examples/pom.xml b/examples/pom.xml index 6dab532de..7eba761f9 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -5,7 +5,7 @@ de.saxsys mvvmfx-parent - 1.3.0 + 1.3.1 mvvmfx-examples @@ -25,4 +25,60 @@ mvvmfx-todomvc + + + + + de.saxsys + mvvmfx-complex + ${project.version} + + + de.saxsys + mvvmfx-guice-example + ${project.version} + + + de.saxsys + mvvmfx-cdi-example + ${project.version} + + + de.saxsys + mvvmfx-fx-root-example + ${project.version} + + + de.saxsys + mvvmfx-helloworld + ${project.version} + + + de.saxsys + mvvmfx-helloworld-without-fxml + ${project.version} + + + de.saxsys + mvvmfx-synchronizefx + ${project.version} + + + de.saxsys + mvvmfx-contacts + ${project.version} + + + de.saxsys + mvvmfx-library-example + ${project.version} + + + de.saxsys + mvvmfx-todomvc + ${project.version} + + + + diff --git a/mvvmfx-archetype/pom.xml b/mvvmfx-archetype/pom.xml index 565217525..5a8b7e967 100644 --- a/mvvmfx-archetype/pom.xml +++ b/mvvmfx-archetype/pom.xml @@ -5,7 +5,7 @@ de.saxsys mvvmfx-parent - 1.3.0 + 1.3.1 @@ -17,10 +17,6 @@ An maven archetype to create an example application with mvvmFX - - ${project.parent.version} - - diff --git a/mvvmfx-archetype/src/main/resources/archetype-resources/pom.xml b/mvvmfx-archetype/src/main/resources/archetype-resources/pom.xml index 750718ff5..660dc2d08 100644 --- a/mvvmfx-archetype/src/main/resources/archetype-resources/pom.xml +++ b/mvvmfx-archetype/src/main/resources/archetype-resources/pom.xml @@ -11,12 +11,22 @@ 1.8 + + + + de.saxsys + mvvmfx-parent + 1.3.1 + pom + import + + + de.saxsys mvvmfx - ${version} diff --git a/mvvmfx-cdi/pom.xml b/mvvmfx-cdi/pom.xml index 655c9a117..18c124410 100644 --- a/mvvmfx-cdi/pom.xml +++ b/mvvmfx-cdi/pom.xml @@ -20,7 +20,7 @@ de.saxsys mvvmfx-parent - 1.3.0 + 1.3.1 mvvmfx-cdi @@ -49,7 +49,6 @@ de.saxsys mvvmfx - ${project.parent.version} provided diff --git a/mvvmfx-guice/pom.xml b/mvvmfx-guice/pom.xml index d9e2edd65..9412d8100 100644 --- a/mvvmfx-guice/pom.xml +++ b/mvvmfx-guice/pom.xml @@ -20,7 +20,7 @@ de.saxsys mvvmfx-parent - 1.3.0 + 1.3.1 mvvmfx-guice @@ -46,7 +46,6 @@ de.saxsys mvvmfx - ${project.parent.version} provided diff --git a/mvvmfx-testing-utils/pom.xml b/mvvmfx-testing-utils/pom.xml index 33a1c611a..e9e2b8596 100644 --- a/mvvmfx-testing-utils/pom.xml +++ b/mvvmfx-testing-utils/pom.xml @@ -5,7 +5,7 @@ mvvmfx-parent de.saxsys - 1.3.0 + 1.3.1 4.0.0 diff --git a/mvvmfx-utils/pom.xml b/mvvmfx-utils/pom.xml index 1bef96ee5..a26609834 100644 --- a/mvvmfx-utils/pom.xml +++ b/mvvmfx-utils/pom.xml @@ -5,7 +5,7 @@ mvvmfx-parent de.saxsys - 1.3.0 + 1.3.1 4.0.0 @@ -19,7 +19,6 @@ de.saxsys mvvmfx-testing-utils - ${project.parent.version} diff --git a/mvvmfx/pom.xml b/mvvmfx/pom.xml index 644e15399..295833e94 100644 --- a/mvvmfx/pom.xml +++ b/mvvmfx/pom.xml @@ -20,7 +20,7 @@ de.saxsys mvvmfx-parent - 1.3.0 + 1.3.1 mvvmfx @@ -56,7 +56,6 @@ de.saxsys mvvmfx-testing-utils - ${project.parent.version} test diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java index cca35ce63..97071d741 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java @@ -15,6 +15,7 @@ ******************************************************************************/ package de.saxsys.mvvmfx; +import de.saxsys.mvvmfx.utils.notifications.NotificationTestHelper; import javafx.application.Platform; import de.saxsys.mvvmfx.internal.viewloader.View; import de.saxsys.mvvmfx.utils.notifications.NotificationCenter; @@ -45,8 +46,17 @@ public interface ViewModel { /** - * Publishes a notification to the subscribers of the notificationId. This notification will be send to the - * UI-Thread. + * Publishes a notification to the subscribers of the messageName. This notification will be send to the + * UI-Thread (if the UI-toolkit was bootstrapped). If no UI-Toolkit is available the notification will be directly + * published. This is typically the case in unit tests. + *

+ * + * This notification mechanism uses the {@link NotificationCenter} internally with the difference that messages send + * by this method aren't globally available. Instead they can only be received by this viewModels {@link #subscribe(String, NotificationObserver)} + * method or when using this viewModel instance as argument to the {@link NotificationCenter#subscribe(ViewModel, String, NotificationObserver)} method. + *

+ * + * See {@link NotificationTestHelper} for a utility that's purpose is to simplify unit tests with notifications. * * @param messageName * of the notification @@ -57,7 +67,18 @@ default void publish(String messageName, Object... payload) { if (Platform.isFxApplicationThread()) { MvvmFX.getNotificationCenter().publish(this, messageName, payload); } else { - Platform.runLater(() -> MvvmFX.getNotificationCenter().publish(this, messageName, payload)); + try { + Platform.runLater(() -> MvvmFX.getNotificationCenter().publish(this, messageName, payload)); + } catch(IllegalStateException e) { + + // If the toolkit isn't initialized yet we will publish the notification directly. + // In most cases this means that we are in a unit test and not JavaFX application is running. + if(e.getMessage().equals("Toolkit not initialized")) { + MvvmFX.getNotificationCenter().publish(this, messageName, payload); + } else { + throw e; + } + } } } diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationCenter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationCenter.java index 4724860b3..3d50d0509 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationCenter.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationCenter.java @@ -24,6 +24,11 @@ * * {@code Object[]} with a notification. * + *

+ * + * There is a util {@link NotificationTestHelper} that's purpose is to simplify testing of notifications in Unit-Tests. + * + * * @author sialcasa * */ @@ -99,7 +104,7 @@ void unsubscribe(String messageName, * @param observer * which should execute when the notification occurs */ - void subscribe(ViewModel view, String messageName, + void subscribe(ViewModel viewModel, String messageName, NotificationObserver observer); /** @@ -118,7 +123,7 @@ void unsubscribe(ViewModel viewModel, String messageName, * @param viewModel * @param observer */ - void unsubscribe(ViewModel view, + void unsubscribe(ViewModel viewModel, NotificationObserver observer); } diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationObserver.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationObserver.java index 6aa4963ac..a800c1e9e 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationObserver.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationObserver.java @@ -31,5 +31,5 @@ public interface NotificationObserver { * @param payload * which are passed */ - public void receivedNotification(String key, Object... payload); + void receivedNotification(String key, Object... payload); } diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelper.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelper.java new file mode 100644 index 000000000..513274ebd --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelper.java @@ -0,0 +1,138 @@ +package de.saxsys.mvvmfx.utils.notifications; + +import de.saxsys.mvvmfx.ViewModel; +import javafx.application.Platform; +import javafx.embed.swing.JFXPanel; +import javafx.util.Pair; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * The {@link NotificationTestHelper} is used to simplify the testing of + * notifications. It is especially useful when notifications are send from + * different thread and when testing the direct notification between a viewModel and the View + * (via {@link ViewModel#publish(String, Object...)} and {@link ViewModel#subscribe(String, NotificationObserver)}) + *

+ * This class implements {@link NotificationObserver} and therefore can be added as subscriber. It will record + * every received notification and can be tested afterwards. + *

+ * + * The {@link ViewModel#publish(String, Object...)} method will send all notifications on the JavaFX UI thread. + * Therefore when testing the publishing of notifications JavaFX has to be running which isn't the case + * with plain JUnit tests. The {@link NotificationTestHelper} will take care for thread handling. + * + *

+ * Example: + *

+ * + *

+ *     
+ *     public class MyViewModel implements ViewModel {
+ *         public static final String ACTION_KEY = "my-action";
+ *     
+ *         public void someAction() {
+ *             ...
+ *             publish(ACTION_KEY);
+ *         }
+ *     }
+ *     
+ *     // unit test
+ *     {@code @Test}
+ *     public void testSomething() {
+ *         MyViewModel viewModel = new MyViewModel();
+ *         
+ *         NotificationTestHelper helper = new NotificationTestHelper();
+ *         viewModel.subscribe(MyViewModel.ACTION_KEY, helper);
+ *         
+ *         
+ *         viewModel.someAction();
+ *         
+ *         assertEquals(1, helper.numberOfReceivedNotifications());
+ *     }
+ * 
+ * + * + * + * You can provide a timeout as constructor parameter. + * This is useful in case of asynchronous code (f.e. when notifications are send from another Thread). + * + * By default the timeout is set to {@value #DEFAULT_TIMEOUT}. When you have a long running thread + * you should use a higher timeout. + * + * @author manuel.mauky + */ +public class NotificationTestHelper implements NotificationObserver { + + public static final long DEFAULT_TIMEOUT = 0l; + + private List> notifications = new ArrayList<>(); + + private long timeout = DEFAULT_TIMEOUT; + + /** + * Create a test helper with a default timeout of {@value #DEFAULT_TIMEOUT} millis. + */ + public NotificationTestHelper() { + new JFXPanel(); + } + + /** + * Create a test helper with the given timeout in millis. + * + * @param timeoutInMillis the timeout. + */ + public NotificationTestHelper(long timeoutInMillis) { + this(); + this.timeout = timeoutInMillis; + } + + @Override + public void receivedNotification(String key, Object... payload) { + notifications.add(new Pair<>(key, payload)); + } + + /** + * @return the number of received notifications. + */ + public int numberOfReceivedNotifications() { + waitForUiThread(); + return notifications.size(); + } + + /** + * @param key the key of the notification. + * @return the number of received notifications for the given key. + */ + public int numberOfReceivedNotifications(String key) { + waitForUiThread(); + return (int) notifications.stream() + .filter(pair -> pair.getKey().equals(key)) + .count(); + } + + private void waitForUiThread() { + CompletableFuture future = new CompletableFuture<>(); + + Platform.runLater(() -> { + if(timeout > 0) { + try { + Thread.sleep(timeout); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + future.complete(null); + }); + + try { + future.get(timeout+50, TimeUnit.MILLISECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + e.printStackTrace(); + } + } +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/validation/CompositeValidator.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/validation/CompositeValidator.java index 19ac80c15..d472ed967 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/validation/CompositeValidator.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/validation/CompositeValidator.java @@ -13,7 +13,7 @@ */ public class CompositeValidator implements Validator { - private CompositeValidationResult result = new CompositeValidationResult(); + private CompositeValidationResult validationStatus = new CompositeValidationResult(); public CompositeValidator() { } @@ -24,19 +24,19 @@ public CompositeValidator(Validator... validators) { public void addValidators(Validator... validators) { - result.addResults(Stream.of(validators) + validationStatus.addResults(Stream.of(validators) .map(Validator::getValidationStatus) .collect(Collectors.toList())); } public void removeValidators(Validator... validators) { - result.removeResults(Stream.of(validators) + validationStatus.removeResults(Stream.of(validators) .map(Validator::getValidationStatus) .collect(Collectors.toList())); } @Override public ValidationStatus getValidationStatus() { - return result; + return validationStatus; } } diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/validation/FunctionBasedValidator.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/validation/FunctionBasedValidator.java index a0f640124..c1430adbf 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/validation/FunctionBasedValidator.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/validation/FunctionBasedValidator.java @@ -33,7 +33,7 @@ */ public class FunctionBasedValidator implements Validator { - private ValidationStatus result = new ValidationStatus(); + private ValidationStatus validationStatus = new ValidationStatus(); private Function> validateFunction; @@ -83,13 +83,13 @@ public FunctionBasedValidator(ObservableValue source, Predicate predicate, } private void validate(T newValue) { - result.clearMessages(); + validationStatus.clearMessages(); Optional message = validateFunction.apply(newValue); - message.ifPresent(result::addMessage); + message.ifPresent(validationStatus::addMessage); } @Override public ValidationStatus getValidationStatus() { - return result; + return validationStatus; } } diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/validation/ObservableRuleBasedValidator.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/validation/ObservableRuleBasedValidator.java index 291ba6ea2..20c087274 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/validation/ObservableRuleBasedValidator.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/validation/ObservableRuleBasedValidator.java @@ -30,7 +30,7 @@ public class ObservableRuleBasedValidator implements Validator { private List> rules = new ArrayList<>(); - private ValidationStatus result = new ValidationStatus(); + private ValidationStatus validationStatus = new ValidationStatus(); /** * Add a rule for this validator. @@ -58,15 +58,15 @@ public void addRule(ObservableValue rule, ValidationMessage message) { private void validateRule(boolean isValid, ValidationMessage message) { if (isValid) { - result.removeMessage(message); + validationStatus.removeMessage(message); } else { - result.addMessage(message); + validationStatus.addMessage(message); } } @Override public ValidationStatus getValidationStatus() { - return result; + return validationStatus; } } diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/validation/ValidationStatus.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/validation/ValidationStatus.java index e5161c836..016a196a3 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/validation/ValidationStatus.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/validation/ValidationStatus.java @@ -82,12 +82,18 @@ public boolean isValid() { * @return an Optional containing the ValidationMessage or an empty Optional. */ public Optional getHighestMessage() { - if (!errorMessages.isEmpty()) { - return Optional.of(errorMessages.get(0)); - } else if (!warningMessages.isEmpty()) { - return Optional.of(warningMessages.get(0)); + final Optional error = messages.stream() + .filter(message -> message.getSeverity().equals(Severity.ERROR)) + .findFirst(); + + if (error.isPresent()) { + return error; } else { - return Optional.empty(); + final Optional warning = messages.stream() + .filter(message -> message.getSeverity().equals(Severity.WARNING)) + .findFirst(); + + return warning; } } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/CompositeCommandTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/CompositeCommandTest.java index 56e4ba9a0..ad9d7b103 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/CompositeCommandTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/CompositeCommandTest.java @@ -189,57 +189,43 @@ protected void action() throws Exception { CompositeCommand compositeCommand = new CompositeCommand(delegateCommand1, delegateCommand2, delegateCommand3); - compositeCommand.progressProperty().addListener(new ChangeListener() { - - @Override - public void changed(ObservableValue observable, Number oldValue, Number newValue) { - } - }); - GCVerifier.forceGC(); - compositeCommand.runningProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) { - if (newValue && !oldValue) { - Platform.runLater(new Runnable() { - @Override - public void run() { - assertTrue(compositeCommand.runningProperty().get()); - assertTrue(delegateCommand1.runningProperty().get()); - assertTrue(delegateCommand2.runningProperty().get()); - assertTrue(delegateCommand3.runningProperty().get()); - assertFalse(compositeCommand.notRunningProperty().get()); - assertFalse(delegateCommand1.notRunningProperty().get()); - assertFalse(delegateCommand2.notRunningProperty().get()); - assertFalse(delegateCommand3.notRunningProperty().get()); - commandCompleted.complete(null); - } - }); - } - if (oldValue && !newValue) { - Platform.runLater(new Runnable() { - @Override - public void run() { - assertFalse(compositeCommand.runningProperty().get()); - assertFalse(delegateCommand1.runningProperty().get()); - assertFalse(delegateCommand2.runningProperty().get()); - assertFalse(delegateCommand3.runningProperty().get()); - assertTrue(compositeCommand.notRunningProperty().get()); - assertTrue(delegateCommand1.notRunningProperty().get()); - assertTrue(delegateCommand2.notRunningProperty().get()); - assertTrue(delegateCommand3.notRunningProperty().get()); - commandStarted.complete(null); - } - }); - } + compositeCommand.runningProperty().addListener((observable, oldValue, newValue) -> { + if (newValue && !oldValue) { // command is now running, wasn't running before + Platform.runLater(() -> { + assertTrue(compositeCommand.runningProperty().get()); + assertTrue(delegateCommand1.runningProperty().get()); + assertTrue(delegateCommand2.runningProperty().get()); + assertTrue(delegateCommand3.runningProperty().get()); + assertFalse(compositeCommand.notRunningProperty().get()); + assertFalse(delegateCommand1.notRunningProperty().get()); + assertFalse(delegateCommand2.notRunningProperty().get()); + assertFalse(delegateCommand3.notRunningProperty().get()); + + commandStarted.complete(null); + }); + } + if (oldValue && !newValue) { // command was running, isn't running now + Platform.runLater(() -> { + assertFalse(compositeCommand.runningProperty().get()); + assertFalse(delegateCommand1.runningProperty().get()); + assertFalse(delegateCommand2.runningProperty().get()); + assertFalse(delegateCommand3.runningProperty().get()); + assertTrue(compositeCommand.notRunningProperty().get()); + assertTrue(delegateCommand1.notRunningProperty().get()); + assertTrue(delegateCommand2.notRunningProperty().get()); + assertTrue(delegateCommand3.notRunningProperty().get()); + + commandCompleted.complete(null); + }); } }); compositeCommand.execute(); - commandStarted.get(3, TimeUnit.SECONDS); - future.get(3, TimeUnit.SECONDS); - commandCompleted.get(4, TimeUnit.SECONDS); + commandStarted.get(5, TimeUnit.SECONDS); + future.get(5, TimeUnit.SECONDS); + commandCompleted.get(5, TimeUnit.SECONDS); } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/DefaultNotificationCenterTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/DefaultNotificationCenterTest.java index c0c72cc63..1b1afe823 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/DefaultNotificationCenterTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/DefaultNotificationCenterTest.java @@ -87,22 +87,22 @@ public void addAndRemoveObserverForNameToDefaultNotificationCenterAndPostNotific defaultCenter.publish(TEST_NOTIFICATION); Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION); } - + @Test public void subscribeSameObserverMultipleTimes() { defaultCenter.subscribe(TEST_NOTIFICATION, observer1); defaultCenter.subscribe(TEST_NOTIFICATION, observer1); - + defaultCenter.publish(TEST_NOTIFICATION); Mockito.verify(observer1, Mockito.times(2)).receivedNotification(TEST_NOTIFICATION); } - + @Test public void unsubscribeObserverThatWasSubscribedMultipleTimes() { defaultCenter.subscribe(TEST_NOTIFICATION, observer1); defaultCenter.subscribe(TEST_NOTIFICATION, observer1); defaultCenter.subscribe(TEST_NOTIFICATION, observer1); - + defaultCenter.unsubscribe(observer1); defaultCenter.publish(TEST_NOTIFICATION); diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelperTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelperTest.java new file mode 100644 index 000000000..a2f65dc5e --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelperTest.java @@ -0,0 +1,102 @@ +package de.saxsys.mvvmfx.utils.notifications; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Before; +import org.junit.Test; + +import de.saxsys.mvvmfx.ViewModel; + +/** + * @author manuel.mauky + */ +public class NotificationTestHelperTest { + + public class MyViewModel implements ViewModel { + } + + private MyViewModel viewModel; + + @Before + public void setUp() throws Exception { + viewModel = new MyViewModel(); + } + + @Test + public void singlePublish() { + NotificationTestHelper helper = new NotificationTestHelper(); + viewModel.subscribe("test", helper); + + viewModel.publish("test"); + + assertThat(helper.numberOfReceivedNotifications()).isEqualTo(1); + } + + @Test + public void multiplePublish() { + NotificationTestHelper helper = new NotificationTestHelper(); + viewModel.subscribe("test", helper); + + int n = 10; + + for (int i = 0; i < n; i++) { + viewModel.publish("test"); + } + + assertThat(helper.numberOfReceivedNotifications()).isEqualTo(n); + } + + @Test + public void globalNotificationCenter() { + NotificationTestHelper helper = new NotificationTestHelper(); + + NotificationCenter notificationCenter = new DefaultNotificationCenter(); + + notificationCenter.subscribe("OK", helper); + + + notificationCenter.publish("OK"); + + + assertThat(helper.numberOfReceivedNotifications()).isEqualTo(1); + } + + @Test + public void publishOnOtherThread() { + NotificationTestHelper helper = new NotificationTestHelper(50l); + + NotificationCenter notificationCenter = new DefaultNotificationCenter(); + + notificationCenter.subscribe("OK", helper); + + + Runnable r = () -> notificationCenter.publish("OK"); + + new Thread(r).start(); + + assertThat(helper.numberOfReceivedNotifications()).isEqualTo(1); + } + + @Test + public void timeout() { + NotificationTestHelper helper = new NotificationTestHelper(300l); + + NotificationCenter notificationCenter = new DefaultNotificationCenter(); + + notificationCenter.subscribe("OK", helper); + + + Runnable r = () -> { + try { + Thread.sleep(100l); + } catch (InterruptedException e) { + e.printStackTrace(); + } + notificationCenter.publish("OK"); + }; + + new Thread(r).start(); + + assertThat(helper.numberOfReceivedNotifications()).isEqualTo(1); + } +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ViewModelTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ViewModelTest.java index 117a143ab..b6ea747a9 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ViewModelTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ViewModelTest.java @@ -41,7 +41,15 @@ public void init() { @Test public void observerIsCalledFromUiThread() throws InterruptedException, ExecutionException, TimeoutException { - + // Check that there is a UI-Thread available. This JUnit-Test isn't running on the UI-Thread but there needs to + // be a UI-Thread available in the background. + CompletableFuture uiThreadIsAvailable = new CompletableFuture<>(); + Platform.runLater(() -> uiThreadIsAvailable.complete(null)); // This would throw an IllegalStateException if no + // UI-Thread is available. + uiThreadIsAvailable.get(1l, TimeUnit.SECONDS); + + + CompletableFuture future = new CompletableFuture<>(); // The test doesn't run on the FX thread. @@ -49,9 +57,9 @@ public void observerIsCalledFromUiThread() throws InterruptedException, Executio viewModel.subscribe(TEST_NOTIFICATION, (key, payload) -> { // the notification is executed on the FX thread. - future.complete(Platform.isFxApplicationThread()); - }); - + future.complete(Platform.isFxApplicationThread()); + }); + viewModel.publish(TEST_NOTIFICATION); @@ -59,22 +67,22 @@ public void observerIsCalledFromUiThread() throws InterruptedException, Executio assertThat(wasCalledOnUiThread).isTrue(); } - - + + @Test public void observerFromOutsideDoesNotReceiveNotifications() { MvvmFX.getNotificationCenter().subscribe(TEST_NOTIFICATION, observer1); viewModel.publish(TEST_NOTIFICATION); - + waitForUiThread(); - Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION); + Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION); } @Test public void addObserverAndPublish() throws Exception { viewModel.subscribe(TEST_NOTIFICATION, observer1); viewModel.publish(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); - + waitForUiThread(); Mockito.verify(observer1).receivedNotification(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); } @@ -84,14 +92,14 @@ public void addAndRemoveObserverAndPublish() throws Exception { viewModel.subscribe(TEST_NOTIFICATION, observer1); viewModel.unsubscribe(observer1); viewModel.publish(TEST_NOTIFICATION); - + waitForUiThread(); Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION); viewModel.subscribe(TEST_NOTIFICATION, observer1); viewModel.unsubscribe(TEST_NOTIFICATION, observer1); viewModel.publish(TEST_NOTIFICATION); - + waitForUiThread(); Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION); } @@ -102,7 +110,7 @@ public void addMultipleObserverAndPublish() throws Exception { viewModel.subscribe(TEST_NOTIFICATION, observer2); viewModel.subscribe(TEST_NOTIFICATION, observer3); viewModel.publish(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); - + waitForUiThread(); Mockito.verify(observer1).receivedNotification(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); @@ -118,7 +126,7 @@ public void addMultipleObserverAndRemoveOneAndPublish() throws Exception { viewModel.subscribe(TEST_NOTIFICATION, observer3); viewModel.unsubscribe(observer1); viewModel.publish(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); - + waitForUiThread(); Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION, @@ -128,9 +136,10 @@ public void addMultipleObserverAndRemoveOneAndPublish() throws Exception { Mockito.verify(observer3).receivedNotification(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); } - + /** - * This method is used to wait until the UI thread has done all work that was queued via {@link Platform#runLater(Runnable)}. + * This method is used to wait until the UI thread has done all work that was queued via + * {@link Platform#runLater(Runnable)}. */ private void waitForUiThread() { CompletableFuture future = new CompletableFuture<>(); @@ -141,7 +150,7 @@ private void waitForUiThread() { throw new IllegalStateException(e); } } - + private class DummyNotificationObserver implements NotificationObserver { @Override public void receivedNotification(String key, Object... payload) { diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ViewModelWithoutUiThreadTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ViewModelWithoutUiThreadTest.java new file mode 100644 index 000000000..adbdf4a18 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ViewModelWithoutUiThreadTest.java @@ -0,0 +1,39 @@ +package de.saxsys.mvvmfx.utils.notifications; + +import de.saxsys.mvvmfx.ViewModel; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * This test verifies the behaviour of the publish/subscribe mechanism of ViewModels when no JavaFX thread is running. + * In this case the publish/subscribe should still be working. + * + * @author manuel.mauky + */ +public class ViewModelWithoutUiThreadTest { + + public static class MyViewModel implements ViewModel { + } + + + @Test + public void test() throws InterruptedException, ExecutionException, TimeoutException { + + MyViewModel viewModel = new MyViewModel(); + + CompletableFuture future = new CompletableFuture<>(); + + viewModel.subscribe("test", (key, payload) -> { + future.complete(null); + }); + + + viewModel.publish("test"); + + future.get(1l, TimeUnit.SECONDS); + } +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/validation/HighestMessageBugTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/validation/HighestMessageBugTest.java new file mode 100644 index 000000000..61bfa184e --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/validation/HighestMessageBugTest.java @@ -0,0 +1,73 @@ +package de.saxsys.mvvmfx.utils.validation; + +import static org.assertj.core.api.Assertions.*; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.ListChangeListener; + +import org.junit.Before; +import org.junit.Test; + +/** + * This test case reproduces the bug #264 + * + * @author manuel.mauky + */ +public class HighestMessageBugTest { + + + private StringProperty value; + private Validator validator; + private ValidationStatus validationStatus; + + + @Before + public void setUp() throws Exception { + value = new SimpleStringProperty(""); + validator = new FunctionBasedValidator<>(value, v -> v != null, ValidationMessage.error("error")); + + + validationStatus = validator.getValidationStatus(); + } + + @Test + public void testValidProperty() throws Exception { + assertThat(validationStatus.getHighestMessage().isPresent()).isFalse(); + + CompletableFuture future = new CompletableFuture<>(); + + validationStatus.validProperty().addListener((observable, oldValue, newValue) -> { + assertThat(validationStatus.getHighestMessage().isPresent()).isTrue(); + + future.complete(null); + }); + + + value.set(null); + + future.get(1l, TimeUnit.SECONDS); + } + + @Test + public void testMessagesList() throws Exception { + assertThat(validationStatus.getHighestMessage().isPresent()).isFalse(); + + CompletableFuture future = new CompletableFuture<>(); + + validationStatus.getMessages().addListener((ListChangeListener) c -> { + assertThat(validationStatus.getHighestMessage().isPresent()).isTrue(); + + future.complete(null); + }); + + + value.set(null); + + future.get(1l, TimeUnit.SECONDS); + } + +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/validation/crossfieldexample/RegisterFormView.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/validation/crossfieldexample/RegisterFormView.java index 3dac00b55..f1896fac6 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/validation/crossfieldexample/RegisterFormView.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/validation/crossfieldexample/RegisterFormView.java @@ -44,9 +44,9 @@ public void initialize() { } private void updateMessage() { - Platform.runLater(() -> message.setText( + message.setText( viewModel.getValidation().getHighestMessage().map(ValidationMessage::getMessage) - .orElse("everythink ok"))); + .orElse("everythink ok")); } public void ok() { diff --git a/pom.xml b/pom.xml index f68de8a9e..a87170cdd 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ de.saxsys mvvmfx-parent pom - 1.3.0 + 1.3.1 mvvmFX parent Application Framework for MVVM with JavaFX. http://www.saxsys.de @@ -87,6 +87,38 @@ + + de.saxsys + mvvmfx + ${project.version} + + + de.saxsys + mvvmfx-cdi + ${project.version} + + + de.saxsys + mvvmfx-guice + ${project.version} + + + de.saxsys + mvvmfx-archetype + ${project.version} + + + de.saxsys + mvvmfx-utils + ${project.version} + + + de.saxsys + mvvmfx-testing-utils + ${project.version} + test + + org.slf4j slf4j-api