Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<LocalDate> 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<DynamicTest> bindMin() {

Check failure on line 30 in vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/DatePickerSignalTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add at least one assertion to this test case.

See more on https://sonarcloud.io/project/issues?id=vaadin_flow-components&issues=AZ0uyNf4JsiWTBVgvrjt&open=AZ0uyNf4JsiWTBVgvrjt&pullRequest=9033
return generateBindingTests(DatePicker::new, DatePicker::bindMin,
DatePicker::getMin, DatePicker::setMin,
() -> new ValueSignal<>(LocalDate.of(2023, 1, 1)),
LocalDate.of(2023, 1, 2));
}

@TestFactory
Stream<DynamicTest> bindMax() {

Check failure on line 38 in vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/DatePickerSignalTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add at least one assertion to this test case.

See more on https://sonarcloud.io/project/issues?id=vaadin_flow-components&issues=AZ0uyNf4JsiWTBVgvrju&open=AZ0uyNf4JsiWTBVgvrju&pullRequest=9033
return generateBindingTests(DatePicker::new, DatePicker::bindMax,
DatePicker::getMax, DatePicker::setMax,
() -> new ValueSignal<>(LocalDate.of(2023, 1, 1)),
LocalDate.of(2023, 1, 2));
}

@TestFactory
Stream<DynamicTest> bindInitialPosition() {

Check failure on line 46 in vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/DatePickerSignalTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add at least one assertion to this test case.

See more on https://sonarcloud.io/project/issues?id=vaadin_flow-components&issues=AZ0uyNf4JsiWTBVgvrjv&open=AZ0uyNf4JsiWTBVgvrjv&pullRequest=9033
return generateBindingTests(DatePicker::new,
DatePicker::bindInitialPosition, DatePicker::getInitialPosition,
DatePicker::setInitialPosition,
() -> new ValueSignal<>(LocalDate.of(2023, 1, 1)),
LocalDate.of(2023, 1, 2));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}.
* <p>
* The following test cases are generated:
* <ul>
* <li><b>synchronizesWhileAttached</b> – 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.</li>
* <li><b>appliesInitialValueWhileDetached</b> – 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.</li>
* <li><b>doesNotSynchronizeWhileDetached</b> – verifies that subsequent
* signal value changes are not propagated to the component property while
* the component is detached.</li>
* <li><b>resynchronizesAfterAttach</b> – 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.</li>
* <li><b>manualSetWhileBoundThrows</b> – verifies that imperative updates
* to a bound property via its setter throws a
* {@link com.vaadin.flow.signals.BindingActiveException}.</li>
* <li><b>rebindWhileBoundThrows</b> – verifies that calling the bind method
* again while a binding is already active throws a
* {@link com.vaadin.flow.signals.BindingActiveException}.</li>
* <li><b>bindNullSignalThrows</b> – verifies that passing a {@code null}
* signal to the bind method throws a {@link NullPointerException}.</li>
* </ul>
* <p>
* <b>NOTE:</b> 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 <C>
* the component type
* @param <T>
* 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 <C extends Component, T> Stream<DynamicTest> generateBindingTests(
Supplier<C> componentFactory, BiConsumer<C, ValueSignal<T>> bind,
Function<C, T> getter, BiConsumer<C, T> setter,
Supplier<ValueSignal<T>> 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);
}
});
}
}