package org.codefx.libfx.nesting.property; import static org.codefx.libfx.nesting.testhelper.NestingAccess.getNestingObservable; import static org.codefx.libfx.nesting.testhelper.NestingAccess.setNestingObservable; import static org.codefx.libfx.nesting.testhelper.NestingAccess.setNestingValue; import static org.codefx.libfx.nesting.testhelper.NestingAccess.EditableNesting.createWithInnerObservable; import static org.codefx.tarkastus.AssertFX.assertDefault; import static org.codefx.tarkastus.AssertFX.assertSameOrEqual; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.Optional; import java.util.function.Supplier; import javafx.beans.property.Property; import org.codefx.libfx.nesting.Nesting; import org.codefx.libfx.nesting.property.InnerObservableMissingBehavior.WhenInnerObservableGoesMissing; import org.codefx.libfx.nesting.property.InnerObservableMissingBehavior.WhenInnerObservableMissingOnUpdate; import org.codefx.libfx.nesting.testhelper.NestingAccess.EditableNesting; import org.junit.Before; import org.junit.Test; /** * Abstract superclass to tests of nested properties. By implementing the few abstract methods subclasses can run all * tests which apply to all nested property implementations. * * @param <S> * the type of the instances contained in the nested property; e.g. {@link Integer} for * {@link NestedIntegerProperty} * @param <T> * the type wrapped by the nested property; e.g. {@link Number} for {@link NestedIntegerProperty} * @param <P> * the type of property wrapped by the nesting */ @SuppressWarnings("javadoc") public abstract class AbstractNestedPropertyTest<S extends T, T, P extends Property<T>> { // #begin INSTANCES USED FOR TESTING /** * The nesting on which the tested property is based. */ private EditableNesting<P> nesting; /** * The tested property. */ private NestedProperty<T> property; //#end INSTANCES USED FOR TESTING /** * Creates a new instance of {@link #nesting} and {@link #property}. */ @Before public void setUp() { P innerObservable = createNewObservableWithSomeValue(); nesting = createWithInnerObservable(innerObservable); property = createNestedPropertyFromNesting(nesting, MissingBehavior.defaults()); } // #begin TESTS @Test public void innerObservablePresentProperty_getBean_returnsNestedProperty() { assertSame(property, property.innerObservablePresentProperty().getBean()); } @Test public void getValue_afterConstruction_returnsInnerObservablesValue() { // create a nesting with a non-default value for this T initialValue = createNewValue(); nesting = createWithInnerObservable(createNewObservableWithValue(initialValue)); property = createNestedPropertyFromNesting(nesting, MissingBehavior.defaults()); assertSameOrEqual(initialValue, nesting.getInnerObservable().get().getValue(), wrapsPrimitive()); assertSameOrEqual(initialValue, property.getValue(), wrapsPrimitive()); assertTrue(property.isInnerObservablePresent()); } @Test public void setNestingValue_nestedPropertyHoldsSameValue() { T newValue = createNewValue(); setNestingValue(nesting, newValue); assertSameOrEqual(newValue, property.getValue(), wrapsPrimitive()); } @Test public void setNestingValueToNull_nestedPropertyHoldsNull() { setNestingValue(nesting, null); assertDefault(property.getValue()); } @Test public void setInnerObservable_nestedPropertyHoldsNewObservablesValue() { T newValue = createNewValue(); P newObservable = createNewObservableWithValue(newValue); setNestingObservable(nesting, newObservable); assertSameOrEqual(newValue, property.getValue(), wrapsPrimitive()); assertTrue(property.isInnerObservablePresent()); } // inner observable goes missing @Test public void setInnerObservableToNull_defaultBehavior_propertyKeepsOldValue() { T oldValue = property.getValue(); setNestingObservable(nesting, null); assertSameOrEqual(oldValue, property.getValue(), wrapsPrimitive()); assertFalse(property.isInnerObservablePresent()); } @Test public void setInnerObservableToNull_keepValue_propertyKeepsOldValue() { MissingBehavior<S> missingBehavior = MissingBehavior .<S> defaults() .whenGoesMissing(WhenInnerObservableGoesMissing.KEEP_VALUE); property = createNestedPropertyFromNesting(nesting, missingBehavior); T oldValue = property.getValue(); setNestingObservable(nesting, null); assertSameOrEqual(oldValue, property.getValue(), wrapsPrimitive()); assertFalse(property.isInnerObservablePresent()); } @Test public void setInnerObservableToNull_setDefaultValue_propertyHoldsDefaultValue() { MissingBehavior<S> missingBehavior = MissingBehavior .<S> defaults() .whenGoesMissing(WhenInnerObservableGoesMissing.SET_DEFAULT_VALUE); property = createNestedPropertyFromNesting(nesting, missingBehavior); setNestingObservable(nesting, null); assertDefault(property.getValue()); } @Test public void setInnerObservableToNull_setValueFromSupplier_propertyHoldsThatValue() { S newValue = createNewValue(); MissingBehavior<S> missingBehavior = MissingBehavior .<S> defaults() .whenGoesMissing(WhenInnerObservableGoesMissing.SET_VALUE_FROM_SUPPLIER) .valueForMissing(() -> newValue); property = createNestedPropertyFromNesting(nesting, missingBehavior); setNestingObservable(nesting, null); assertSameOrEqual(newValue, property.getValue(), wrapsPrimitive()); } @Test public void setInnerObservableToNull_setNullFromSupplier_propertyHoldsDefaultValue() { MissingBehavior<S> missingBehavior = MissingBehavior .<S> defaults() .whenGoesMissing(WhenInnerObservableGoesMissing.SET_VALUE_FROM_SUPPLIER) .valueForMissing(() -> null); property = createNestedPropertyFromNesting(nesting, missingBehavior); setNestingObservable(nesting, null); assertDefault(property.getValue()); } // update when inner observable missing @Test(expected = IllegalStateException.class) public void setValueOnInnerObservableMissing_defaulBehavior_throwException() { setNestingObservable(nesting, null); property.setValue(createNewValue()); } @Test(expected = IllegalStateException.class) public void setValueOnInnerObservableMissing_throw_throwException() { MissingBehavior<S> missingBehavior = MissingBehavior .<S> defaults() .onUpdate(WhenInnerObservableMissingOnUpdate.THROW_EXCEPTION); property = createNestedPropertyFromNesting(nesting, missingBehavior); setNestingObservable(nesting, null); property.setValue(createNewValue()); } @Test public void setValueOnInnerObservableMissing_acceptUntilNext_nestedPropertyHoldsNewValue() { MissingBehavior<S> missingBehavior = MissingBehavior .<S> defaults() .onUpdate(WhenInnerObservableMissingOnUpdate.ACCEPT_VALUE_UNTIL_NEXT_INNER_OBSERVABLE); property = createNestedPropertyFromNesting(nesting, missingBehavior); setNestingObservable(nesting, null); // set a new value (which can not be written to the nesting's observable as none is present) T newValue = createNewValue(); property.setValue(newValue); assertSameOrEqual(newValue, property.getValue(), wrapsPrimitive()); } @Test public void newInnerObservableAfterSetValueOnMissingInnerObservable_acceptUntilNext_newInnerObservableKeepsValue() { MissingBehavior<S> missingBehavior = MissingBehavior .<S> defaults() .onUpdate(WhenInnerObservableMissingOnUpdate.ACCEPT_VALUE_UNTIL_NEXT_INNER_OBSERVABLE); property = createNestedPropertyFromNesting(nesting, missingBehavior); setNestingObservable(nesting, null); T newInnerObservablesValue = createNewValue(); P newObservable = createNewObservableWithValue(newInnerObservablesValue); // change the nested property's value (which can not be written to the nesting's observable as none is present); property.setValue(createNewValue()); // due to the contract of 'createNewValue' the nested property has currently another value than the new observable assertNotEquals(newObservable.getValue(), property.getValue()); // set the new observable and assert that it kept its value and the nested property was updated setNestingObservable(nesting, newObservable); assertSameOrEqual(newInnerObservablesValue, newObservable.getValue(), wrapsPrimitive()); assertSameOrEqual(newInnerObservablesValue, property.getValue(), wrapsPrimitive()); } // binding to new inner observable @Test public void setValueOnNewInnerObservable_nestedPropertyHoldsThatValue() { P newObservable = createNewObservableWithSomeValue(); setNestingObservable(nesting, newObservable); // change the new observable's value T newValue = createNewValue(); newObservable.setValue(newValue); assertSameOrEqual(newValue, property.getValue(), wrapsPrimitive()); } @Test public void setValueOnNestedProperty_newInnerObservableHoldsThatValue() { P newObservable = createNewObservableWithSomeValue(); setNestingObservable(nesting, newObservable); // change the nested property's value T newValue = createNewValue(); property.setValue(newValue); assertSameOrEqual(newValue, newObservable.getValue(), wrapsPrimitive()); } // unbinding from replaced inner observable @Test public void setValueOnOldInnerObservable_nestedPropertyDoesNotChange() { Property<T> oldObservable = getNestingObservable(nesting); setNestingObservable(nesting, createNewObservableWithValue(createNewValue())); // let the test fail when the nested property changes property.addListener((obs, oldValue, newValue) -> fail()); // change the old observable's value oldObservable.setValue(createNewValue()); } @Test public void setValueOnNestedProperty_oldInnerObservableDoesNotChange() { Property<T> oldObservable = getNestingObservable(nesting); setNestingObservable(nesting, createNewObservableWithValue(createNewValue())); // let the test fail when the old observable changes oldObservable.addListener((obs, oldValue, newValue) -> fail()); // change the nested property's value property.setValue(createNewValue()); } // #end TESTS // #begin ABSTRACT METHODS /** * Indicates whether the tested nested property wraps primitive values (e.g. ints). * * @return true if the nested properties wraps primitive values */ protected abstract boolean wrapsPrimitive(); /** * Creates the property which will be tested from the specified nesting. * * @param nesting * the nesting from which the nested property is created * @param missingBehavior * the behavior for the case that the inner observable is missing * @return a new {@link NestedProperty} instance */ protected abstract NestedProperty<T> createNestedPropertyFromNesting( Nesting<P> nesting, InnerObservableMissingBehavior<S> missingBehavior); /** * Creates a new value. * <p> * Each call must return an instance which is not equal to any of those returned before and to that contained in the * observable returned by {@link #createNewObservableWithSomeValue()}. * * @return a new instance of type {@code S} */ protected abstract S createNewValue(); /** * Creates a new observable which holds the specified value. * <p> * Each call must return a new instance. * * @param value * the new observable's value * @return a new {@link Property} instance with the specified value */ protected abstract P createNewObservableWithValue(T value); /** * Creates a new observable which holds some arbitrary value. * <p> * Each call must return a new instance. * * @return a new {@link Property} instance with the specified value */ protected abstract P createNewObservableWithSomeValue(); // #end ABSTRACT METHODS // #begin HELPERS /** * @return the nesting on which the tested property is based */ protected final EditableNesting<P> getNesting() { return nesting; } /** * @return the tested property */ protected final NestedProperty<T> getProperty() { return property; } /** * @return the {@link #getProperty tested property}'s current value */ protected final T getPropertyValue() { return property.getValue(); } /** * Sets the specified behavior for missing inner observables on the specified builder. * * @param behavior * the behavior to set on the builder * @param builder * the mutated builder */ protected final void setBehaviorOnBuilder( InnerObservableMissingBehavior<S> behavior, AbstractNestedPropertyBuilder<S, ?, ?, ?> builder) { // on goes missing switch (behavior.whenGoesMissing()) { case KEEP_VALUE: builder.onInnerObservableMissingKeepValue(); break; case SET_DEFAULT_VALUE: builder.onInnerObservableMissingSetDefaultValue(); break; case SET_VALUE_FROM_SUPPLIER: builder.onInnerObservableMissingComputeValue(behavior.valueForMissing().get()); break; default: throw new IllegalArgumentException(); } // on update switch (behavior.onUpdate()) { case ACCEPT_VALUE_UNTIL_NEXT_INNER_OBSERVABLE: builder.onUpdateWhenInnerObservableMissingAcceptValues(); break; case THROW_EXCEPTION: builder.onUpdateWhenInnerObservableMissingThrowException(); break; default: throw new IllegalArgumentException(); } } // #end HELPERS // #begin NESTED CLASSES /** * Mutable implementation of {@link InnerObservableMissingBehavior}. * * @param <T> * the type contained in the nested property, e.g. {@link Integer} for {@link NestedIntegerProperty} */ protected static class MissingBehavior<T> implements InnerObservableMissingBehavior<T> { private WhenInnerObservableGoesMissing whenGoesMissing; private Optional<? extends Supplier<T>> valueForMissing; private WhenInnerObservableMissingOnUpdate onUpdate; private MissingBehavior() {} /** * Creates the default specification for the behavior when the inner observable is missing. * <p> * The "production code" defines default behavior as well and it could be referenced here. Instead the defaults * are explicitly specified (again) to ensure that changing them in some other place does not happen without * breaking some tests. * * @param <T> * the type contained in the nested property * @return the default behavior */ public static <T> MissingBehavior<T> defaults() { MissingBehavior<T> behavior = new MissingBehavior<>(); behavior.whenGoesMissing = WhenInnerObservableGoesMissing.KEEP_VALUE; behavior.valueForMissing = Optional.empty(); behavior.onUpdate = WhenInnerObservableMissingOnUpdate.THROW_EXCEPTION; return behavior; } @Override public WhenInnerObservableGoesMissing whenGoesMissing() { return whenGoesMissing; } /** * Determines what happens the inner observable goes missing. * * @param whenGoesMissing * the desired behavior * @return this behavior */ public MissingBehavior<T> whenGoesMissing(WhenInnerObservableGoesMissing whenGoesMissing) { this.whenGoesMissing = whenGoesMissing; return this; } @Override public Optional<? extends Supplier<T>> valueForMissing() { return valueForMissing; } /** * Sets a supplier which is called when the inner observable goes missing and a new value should be set. * * @param valueForMissing * the supplier for the new value * @return this behavior */ public MissingBehavior<T> valueForMissing(Supplier<T> valueForMissing) { this.valueForMissing = Optional.of(valueForMissing); return this; } @Override public WhenInnerObservableMissingOnUpdate onUpdate() { return onUpdate; } /** * Determines what happens when the property is updated while the inner observable is missing. * * @param onUpdate * the desired behavior * @return this behavior */ public MissingBehavior<T> onUpdate(WhenInnerObservableMissingOnUpdate onUpdate) { this.onUpdate = onUpdate; return this; } } // #end NESTED CLASSES }