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..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 @@ -16,144 +16,38 @@ 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; class DatePickerSignalTest extends AbstractSignalsTest { - private DatePicker datePicker; - private ValueSignal signal; - - @BeforeEach - void setup() { - datePicker = new DatePicker(); - 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()); - } - - @Test - void bindInitialPosition_manualSetThrows() { - UI.getCurrent().add(datePicker); - datePicker.bindInitialPosition(signal); - Assertions.assertThrows(BindingActiveException.class, - () -> datePicker.setInitialPosition(LocalDate.of(2023, 2, 1))); - } - - @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 bindMin() { + return generateBindingTests(DatePicker::new, DatePicker::bindMin, + DatePicker::getMin, DatePicker::setMin, + () -> new ValueSignal<>(LocalDate.of(2023, 1, 1)), + LocalDate.of(2023, 1, 2)); + } + + @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)); + } + + @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..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 @@ -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,171 @@ 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}.
  • + *
  • 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 + * 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) { + + var synchronizesWhileAttached = createTest("synchronizesWhileAttached", + () -> { + 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)); + }); + + 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); + } + + /** + * 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 DynamicTest createTest(String name, Executable test) { + return dynamicTest(name, () -> { + ui.beforeEach(null); + try { + test.execute(); + } finally { + ui.afterEach(null); + } + }); + } }