From 7e331e1732760547af33514f336b34b2c003b0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Thu, 26 Mar 2026 21:22:35 +0100 Subject: [PATCH 1/4] refactor: extract common set of signal tests --- .../datepicker/DatePickerSignalTest.java | 140 +++------------ .../com/vaadin/tests/AbstractSignalsTest.java | 164 ++++++++++++++++++ 2 files changed, 186 insertions(+), 118 deletions(-) diff --git a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/DatePickerSignalTest.java b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/DatePickerSignalTest.java index 09d18645fe6..d4930e874f0 100644 --- a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/DatePickerSignalTest.java +++ b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/DatePickerSignalTest.java @@ -16,13 +16,12 @@ package com.vaadin.flow.component.datepicker; import java.time.LocalDate; +import java.util.stream.Stream; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; -import com.vaadin.flow.component.UI; -import com.vaadin.flow.signals.BindingActiveException; import com.vaadin.flow.signals.local.ValueSignal; import com.vaadin.tests.AbstractSignalsTest; @@ -37,123 +36,28 @@ void setup() { signal = new ValueSignal<>(LocalDate.of(2023, 1, 1)); } - @Test - void bindMin_synchronizedWhenAttached() { - UI.getCurrent().add(datePicker); - datePicker.bindMin(signal); - Assertions.assertEquals(signal.peek(), datePicker.getMin()); - - signal.set(LocalDate.of(2023, 2, 1)); - Assertions.assertEquals(signal.peek(), datePicker.getMin()); - } - - @Test - void bindMin_noEffectWhenDetached() { - UI.getCurrent().add(datePicker); - datePicker.bindMin(signal); - datePicker.removeFromParent(); - - signal.set(LocalDate.of(2023, 2, 1)); - Assertions.assertEquals(LocalDate.of(2023, 1, 1), datePicker.getMin()); - - UI.getCurrent().add(datePicker); - Assertions.assertEquals(LocalDate.of(2023, 2, 1), datePicker.getMin()); - } - - @Test - void bindMin_manualSetThrows() { - UI.getCurrent().add(datePicker); - datePicker.bindMin(signal); - Assertions.assertThrows(BindingActiveException.class, - () -> datePicker.setMin(LocalDate.of(2023, 2, 1))); - } - - @Test - void bindMin_rebindingThrows() { - UI.getCurrent().add(datePicker); - datePicker.bindMin(signal); - Assertions.assertThrows(BindingActiveException.class, () -> datePicker - .bindMin(new ValueSignal<>(LocalDate.of(2023, 2, 1)))); - } - - @Test - void bindMax_synchronizedWhenAttached() { - UI.getCurrent().add(datePicker); - datePicker.bindMax(signal); - Assertions.assertEquals(signal.peek(), datePicker.getMax()); - - signal.set(LocalDate.of(2023, 2, 1)); - Assertions.assertEquals(signal.peek(), datePicker.getMax()); - } - - @Test - void bindMax_noEffectWhenDetached() { - UI.getCurrent().add(datePicker); - datePicker.bindMax(signal); - datePicker.removeFromParent(); - - signal.set(LocalDate.of(2023, 2, 1)); - Assertions.assertEquals(LocalDate.of(2023, 1, 1), datePicker.getMax()); - - UI.getCurrent().add(datePicker); - Assertions.assertEquals(LocalDate.of(2023, 2, 1), datePicker.getMax()); - } - - @Test - void bindMax_manualSetThrows() { - UI.getCurrent().add(datePicker); - datePicker.bindMax(signal); - Assertions.assertThrows(BindingActiveException.class, - () -> datePicker.setMax(LocalDate.of(2023, 2, 1))); - } - - @Test - void bindMax_rebindingThrows() { - UI.getCurrent().add(datePicker); - datePicker.bindMax(signal); - Assertions.assertThrows(BindingActiveException.class, () -> datePicker - .bindMax(new ValueSignal<>(LocalDate.of(2023, 2, 1)))); - } - - @Test - void bindInitialPosition_synchronizedWhenAttached() { - UI.getCurrent().add(datePicker); - datePicker.bindInitialPosition(signal); - Assertions.assertEquals(signal.peek(), datePicker.getInitialPosition()); - - signal.set(LocalDate.of(2023, 2, 1)); - Assertions.assertEquals(signal.peek(), datePicker.getInitialPosition()); - } - - @Test - void bindInitialPosition_noEffectWhenDetached() { - UI.getCurrent().add(datePicker); - datePicker.bindInitialPosition(signal); - datePicker.removeFromParent(); - - signal.set(LocalDate.of(2023, 2, 1)); - Assertions.assertEquals(LocalDate.of(2023, 1, 1), - datePicker.getInitialPosition()); - - UI.getCurrent().add(datePicker); - Assertions.assertEquals(LocalDate.of(2023, 2, 1), - datePicker.getInitialPosition()); + @TestFactory + Stream bindMin() { + return generateBindingTests(DatePicker::new, DatePicker::bindMin, + DatePicker::getMin, DatePicker::setMin, + () -> new ValueSignal<>(LocalDate.of(2023, 1, 1)), + LocalDate.of(2023, 1, 2)); } - @Test - void bindInitialPosition_manualSetThrows() { - UI.getCurrent().add(datePicker); - datePicker.bindInitialPosition(signal); - Assertions.assertThrows(BindingActiveException.class, - () -> datePicker.setInitialPosition(LocalDate.of(2023, 2, 1))); + @TestFactory + Stream bindMax() { + return generateBindingTests(DatePicker::new, DatePicker::bindMax, + DatePicker::getMax, DatePicker::setMax, + () -> new ValueSignal<>(LocalDate.of(2023, 1, 1)), + LocalDate.of(2023, 1, 2)); } - @Test - void bindInitialPosition_rebindingThrows() { - UI.getCurrent().add(datePicker); - datePicker.bindInitialPosition(signal); - Assertions.assertThrows(BindingActiveException.class, - () -> datePicker.bindInitialPosition( - new ValueSignal<>(LocalDate.of(2023, 2, 1)))); + @TestFactory + Stream bindInitialPosition() { + return generateBindingTests(DatePicker::new, + DatePicker::bindInitialPosition, DatePicker::getInitialPosition, + DatePicker::setInitialPosition, + () -> new ValueSignal<>(LocalDate.of(2023, 1, 1)), + LocalDate.of(2023, 1, 2)); } } diff --git a/vaadin-flow-components-shared-parent/vaadin-flow-components-test-util/src/main/java/com/vaadin/tests/AbstractSignalsTest.java b/vaadin-flow-components-shared-parent/vaadin-flow-components-test-util/src/main/java/com/vaadin/tests/AbstractSignalsTest.java index f9a1656f529..41e4874a69d 100644 --- a/vaadin-flow-components-shared-parent/vaadin-flow-components-test-util/src/main/java/com/vaadin/tests/AbstractSignalsTest.java +++ b/vaadin-flow-components-shared-parent/vaadin-flow-components-test-util/src/main/java/com/vaadin/tests/AbstractSignalsTest.java @@ -15,7 +15,23 @@ */ package com.vaadin.tests; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.function.Executable; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.signals.BindingActiveException; +import com.vaadin.flow.signals.local.ValueSignal; /** * Base class for testing components with full-stack signals. Since signal @@ -25,4 +41,152 @@ public class AbstractSignalsTest { @RegisterExtension protected MockUIExtension ui = new MockUIExtension(); + + /** + * Generates a suite of JUnit 5 dynamic tests that verify the standard + * behavior of a signal binding on a component property. The returned stream + * is intended to be used with {@link org.junit.jupiter.api.TestFactory}. + *

+ * The following test cases are generated: + *

    + *
  • synchronizesWhileAttached – verifies that the component + * property reflects the signal's initial value after binding and updates + * when the signal value changes, while the component is attached to a + * UI.
  • + *
  • appliesInitialValueWhileDetached – verifies that the signal's + * initial value is applied to the component property immediately upon + * binding, even when the component is not attached to a UI.
  • + *
  • doesNotSynchronizeWhileDetached – verifies that subsequent + * signal value changes are not propagated to the component property while + * the component is detached.
  • + *
  • resynchronizesAfterAttach – verifies that the component + * property catches up with the latest signal value when the component is + * attached to a UI after the signal was updated while detached.
  • + *
  • manualSetWhileBoundThrows – verifies that imperative updates + * to a bound property via its setter throws a + * {@link com.vaadin.flow.signals.BindingActiveException}.
  • + *
  • rebindWhileBoundThrows – verifies that calling the bind method + * again while a binding is already active throws a + * {@link com.vaadin.flow.signals.BindingActiveException}.
  • + *
+ *

+ * NOTE: The tests are not necessarily exhaustive for all aspects of + * a specific signal binding, but only cover common expected behaviors. + * Additional test cases may be needed for edge cases or specific + * implementation details of a particular binding. + * + * @param + * the component type + * @param + * the property value type + * @param componentFactory + * supplier that creates a new component instance for each test + * @param bind + * the bind method under test (e.g. {@code DatePicker::bindMin}) + * @param getter + * the getter for the bound property (e.g. + * {@code DatePicker::getMin}) + * @param setter + * the setter for the bound property (e.g. + * {@code DatePicker::setMin}), used to verify that imperative + * updates are rejected while a binding is active + * @param signalFactory + * supplier that creates a new signal with an initial value for + * each test + * @param updatedValue + * a value different from the signal's initial value, used to + * test synchronization behavior + * @return a stream of dynamic tests to be returned from a + * {@link org.junit.jupiter.api.TestFactory} method + */ + protected Stream generateBindingTests( + Supplier componentFactory, BiConsumer> bind, + Function getter, BiConsumer setter, + Supplier> signalFactory, T updatedValue) { + return Stream + .of(dynamicTest("synchronizesWhileAttached", withMockUI(() -> { + var component = componentFactory.get(); + var signal = signalFactory.get(); + var initialValue = signal.peek(); + + UI.getCurrent().add(component); + + bind.accept(component, signal); + assertEquals(initialValue, getter.apply(component)); + + signal.set(updatedValue); + assertEquals(updatedValue, getter.apply(component)); + })), dynamicTest("appliesInitialValueWhileDetached", + withMockUI(() -> { + var component = componentFactory.get(); + var signal = signalFactory.get(); + var initialValue = signal.peek(); + + bind.accept(component, signal); + + assertEquals(initialValue, getter.apply(component)); + })), dynamicTest("doesNotSynchronizeWhileDetached", + withMockUI(() -> { + var component = componentFactory.get(); + var signal = signalFactory.get(); + T initialValue = signal.peek(); + + bind.accept(component, signal); + signal.set(updatedValue); + + assertEquals(initialValue, + getter.apply(component)); + })), + dynamicTest("resynchronizesAfterAttach", + withMockUI(() -> { + var component = componentFactory.get(); + var signal = signalFactory.get(); + T initialValue = signal.peek(); + + bind.accept(component, signal); + signal.set(updatedValue); + + UI.getCurrent().add(component); + + assertEquals(updatedValue, + getter.apply(component)); + })), + dynamicTest("manualSetWhileBoundThrows", + withMockUI(() -> { + var component = componentFactory.get(); + var signal = signalFactory.get(); + + bind.accept(component, signal); + + assertThrows(BindingActiveException.class, + () -> setter.accept(component, + updatedValue)); + })), + dynamicTest("rebindWhileBoundThrows", withMockUI(() -> { + var component = componentFactory.get(); + var signal = signalFactory.get(); + + bind.accept(component, signal); + + assertThrows(BindingActiveException.class, + () -> bind.accept(component, + signalFactory.get())); + }))); + } + + /** + * Wraps a test executable with {@link MockUIExtension} beforeEach and + * afterEach, since JUnit 5 dynamic tests do not support lifecycle + * callbacks. + */ + private Executable withMockUI(Executable test) { + return () -> { + ui.beforeEach(null); + try { + test.execute(); + } finally { + ui.afterEach(null); + } + }; + } } From dd263d2ac4b652a6ed9dac4c5d831c809e5f6e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Fri, 27 Mar 2026 11:02:15 +0100 Subject: [PATCH 2/4] add test for binding null signal --- .../main/java/com/vaadin/tests/AbstractSignalsTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vaadin-flow-components-shared-parent/vaadin-flow-components-test-util/src/main/java/com/vaadin/tests/AbstractSignalsTest.java b/vaadin-flow-components-shared-parent/vaadin-flow-components-test-util/src/main/java/com/vaadin/tests/AbstractSignalsTest.java index 41e4874a69d..a017a135524 100644 --- a/vaadin-flow-components-shared-parent/vaadin-flow-components-test-util/src/main/java/com/vaadin/tests/AbstractSignalsTest.java +++ b/vaadin-flow-components-shared-parent/vaadin-flow-components-test-util/src/main/java/com/vaadin/tests/AbstractSignalsTest.java @@ -68,6 +68,8 @@ public class AbstractSignalsTest { *

  • rebindWhileBoundThrows – verifies that calling the bind method * again while a binding is already active throws a * {@link com.vaadin.flow.signals.BindingActiveException}.
  • + *
  • bindNullSignalThrows – verifies that passing a {@code null} + * signal to the bind method throws a {@link NullPointerException}.
  • * *

    * NOTE: The tests are not necessarily exhaustive for all aspects of @@ -171,6 +173,12 @@ protected Stream generateBindingTests( assertThrows(BindingActiveException.class, () -> bind.accept(component, signalFactory.get())); + })), + dynamicTest("bindNullSignalThrows", withMockUI(() -> { + var component = componentFactory.get(); + + assertThrows(NullPointerException.class, + () -> bind.accept(component, null)); }))); } From 70a4b4d8ed57786e5a39fa1c7db8b97112ff7958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Fri, 27 Mar 2026 11:09:50 +0100 Subject: [PATCH 3/4] improve readability --- .../com/vaadin/tests/AbstractSignalsTest.java | 149 ++++++++++-------- 1 file changed, 80 insertions(+), 69 deletions(-) diff --git a/vaadin-flow-components-shared-parent/vaadin-flow-components-test-util/src/main/java/com/vaadin/tests/AbstractSignalsTest.java b/vaadin-flow-components-shared-parent/vaadin-flow-components-test-util/src/main/java/com/vaadin/tests/AbstractSignalsTest.java index a017a135524..5d935a29cca 100644 --- a/vaadin-flow-components-shared-parent/vaadin-flow-components-test-util/src/main/java/com/vaadin/tests/AbstractSignalsTest.java +++ b/vaadin-flow-components-shared-parent/vaadin-flow-components-test-util/src/main/java/com/vaadin/tests/AbstractSignalsTest.java @@ -105,8 +105,9 @@ protected Stream generateBindingTests( Supplier componentFactory, BiConsumer> bind, Function getter, BiConsumer setter, Supplier> signalFactory, T updatedValue) { - return Stream - .of(dynamicTest("synchronizesWhileAttached", withMockUI(() -> { + + var synchronizesWhileAttached = createTest("synchronizesWhileAttached", + () -> { var component = componentFactory.get(); var signal = signalFactory.get(); var initialValue = signal.peek(); @@ -118,83 +119,93 @@ protected Stream generateBindingTests( signal.set(updatedValue); assertEquals(updatedValue, getter.apply(component)); - })), dynamicTest("appliesInitialValueWhileDetached", - withMockUI(() -> { - var component = componentFactory.get(); - var signal = signalFactory.get(); - var initialValue = signal.peek(); - - bind.accept(component, signal); - - assertEquals(initialValue, getter.apply(component)); - })), dynamicTest("doesNotSynchronizeWhileDetached", - withMockUI(() -> { - var component = componentFactory.get(); - var signal = signalFactory.get(); - T initialValue = signal.peek(); - - bind.accept(component, signal); - signal.set(updatedValue); - - assertEquals(initialValue, - getter.apply(component)); - })), - dynamicTest("resynchronizesAfterAttach", - withMockUI(() -> { - var component = componentFactory.get(); - var signal = signalFactory.get(); - T initialValue = signal.peek(); - - bind.accept(component, signal); - signal.set(updatedValue); - - UI.getCurrent().add(component); - - assertEquals(updatedValue, - getter.apply(component)); - })), - dynamicTest("manualSetWhileBoundThrows", - withMockUI(() -> { - var component = componentFactory.get(); - var signal = signalFactory.get(); - - bind.accept(component, signal); - - assertThrows(BindingActiveException.class, - () -> setter.accept(component, - updatedValue)); - })), - dynamicTest("rebindWhileBoundThrows", withMockUI(() -> { - var component = componentFactory.get(); - var signal = signalFactory.get(); - - bind.accept(component, signal); - - assertThrows(BindingActiveException.class, - () -> bind.accept(component, - signalFactory.get())); - })), - dynamicTest("bindNullSignalThrows", withMockUI(() -> { - var component = componentFactory.get(); - - assertThrows(NullPointerException.class, - () -> bind.accept(component, null)); - }))); + }); + + var appliesInitialValueWhileDetached = createTest( + "appliesInitialValueWhileDetached", () -> { + var component = componentFactory.get(); + var signal = signalFactory.get(); + var initialValue = signal.peek(); + + bind.accept(component, signal); + + assertEquals(initialValue, getter.apply(component)); + }); + + var doesNotSynchronizeWhileDetached = createTest( + "doesNotSynchronizeWhileDetached", () -> { + var component = componentFactory.get(); + var signal = signalFactory.get(); + T initialValue = signal.peek(); + + bind.accept(component, signal); + signal.set(updatedValue); + + assertEquals(initialValue, getter.apply(component)); + }); + + var resynchronizesAfterAttach = createTest("resynchronizesAfterAttach", + () -> { + var component = componentFactory.get(); + var signal = signalFactory.get(); + + bind.accept(component, signal); + signal.set(updatedValue); + + UI.getCurrent().add(component); + + assertEquals(updatedValue, getter.apply(component)); + }); + + var manualSetWhileBoundThrows = createTest("manualSetWhileBoundThrows", + () -> { + var component = componentFactory.get(); + var signal = signalFactory.get(); + + bind.accept(component, signal); + + assertThrows(BindingActiveException.class, + () -> setter.accept(component, updatedValue)); + }); + + var rebindWhileBoundThrows = createTest("rebindWhileBoundThrows", + () -> { + var component = componentFactory.get(); + var signal = signalFactory.get(); + + bind.accept(component, signal); + + assertThrows(BindingActiveException.class, + () -> bind.accept(component, signalFactory.get())); + }); + + var bindNullSignalThrows = createTest("bindNullSignalThrows", () -> { + var component = componentFactory.get(); + + assertThrows(NullPointerException.class, + () -> bind.accept(component, null)); + }); + + return Stream.of(synchronizesWhileAttached, + appliesInitialValueWhileDetached, + doesNotSynchronizeWhileDetached, resynchronizesAfterAttach, + manualSetWhileBoundThrows, rebindWhileBoundThrows, + bindNullSignalThrows); } /** - * Wraps a test executable with {@link MockUIExtension} beforeEach and - * afterEach, since JUnit 5 dynamic tests do not support lifecycle + * Creates a dynamic test that sets up and tears down a mock UI around the + * test executable, since JUnit 5 dynamic tests do not support lifecycle * callbacks. */ - private Executable withMockUI(Executable test) { - return () -> { + private DynamicTest createTest(String name, Executable test) { + return dynamicTest(name, () -> { ui.beforeEach(null); try { test.execute(); } finally { ui.afterEach(null); } - }; + }); } } From cd33bec6c40a557f4658f2ee79842c6df009d820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Fri, 27 Mar 2026 11:24:05 +0100 Subject: [PATCH 4/4] cleanup test --- .../component/datepicker/DatePickerSignalTest.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/DatePickerSignalTest.java b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/DatePickerSignalTest.java index d4930e874f0..e7e5e51547f 100644 --- a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/DatePickerSignalTest.java +++ b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/DatePickerSignalTest.java @@ -18,7 +18,6 @@ import java.time.LocalDate; import java.util.stream.Stream; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; @@ -27,15 +26,6 @@ class DatePickerSignalTest extends AbstractSignalsTest { - private DatePicker datePicker; - private ValueSignal signal; - - @BeforeEach - void setup() { - datePicker = new DatePicker(); - signal = new ValueSignal<>(LocalDate.of(2023, 1, 1)); - } - @TestFactory Stream bindMin() { return generateBindingTests(DatePicker::new, DatePicker::bindMin,