Kaynağa Gözat

Add test case for ObservableController, fix things up to handle various permutations.

Sam Jaffe 5 yıl önce
ebeveyn
işleme
d1dc62a810

+ 1 - 0
lombok.config

@@ -0,0 +1 @@
+lombok.addLombokGeneratedAnnotation = true

+ 29 - 33
src/main/lombok/org/leumasjaffe/observer/ObservableController.java

@@ -26,14 +26,14 @@ import lombok.NonNull;
 public class ObservableController {
 	
 	private static class TextFieldController<S extends JTextComponent, T extends Observable> extends ObservableListener<S, T> {
-		BiFunction<String, T, Boolean> func;
+		BiFunction<String, T, Boolean> updateModel;
 		
-		public TextFieldController(@NonNull final S comp,
-				@NonNull final BiFunction<String, T, Boolean> func,
-				@NonNull final BiConsumer<? super S, ? super T> update,
+		public TextFieldController(final S comp,
+				final BiFunction<String, T, Boolean> updateModel,
+				final BiConsumer<? super S, ? super T> updateComp,
 				final Consumer<T> onEmpty) {
-			super(comp, update);
-			this.func = func;
+			super(comp, updateComp);
+			this.updateModel = updateModel;
 			if (onEmpty == null) {
 				AnyActionDocumentListener.skipEmpty(comp, evt -> accept( ) );
 			} else {
@@ -43,10 +43,8 @@ public class ObservableController {
 			}
 		}
 		
-		public TextFieldController(@NonNull final S comp,
-				@NonNull final Function<T, String> get,
-				@NonNull final BiConsumer<T, String> set,
-				final Consumer<T> onEmpty) {
+		public TextFieldController(final S comp, final Function<T, String> get,
+				final BiConsumer<T, String> set, final Consumer<T> onEmpty) {
 			this(comp, (t, u) -> {
 				if (t.equals(get.apply(u))) return false;
 				set.accept(u, t);
@@ -58,7 +56,7 @@ public class ObservableController {
 		}
 
 		private boolean update() {
-			return func.apply( impl.getComponent().getText( ), impl.getModel() );
+			return updateModel.apply( impl.getComponent().getText( ), impl.getModel() );
 		}
 
 		private void accept() {
@@ -70,17 +68,16 @@ public class ObservableController {
 	}
 	
 	private static class FormattedTextFieldController<S extends JFormattedTextField, T extends Observable, V> extends ObservableListener<S, T> {
-		BiFunction<V, T, Boolean> func;
+		BiFunction<V, T, Boolean> updateModel;
 		
-		public FormattedTextFieldController(@NonNull final S comp,
-				@NonNull final Function<T, V> get,
-				@NonNull final BiConsumer<T, V> set,
-				V onEmpty) {
+		public FormattedTextFieldController(final S comp,
+				final Function<T, V> get, final BiConsumer<T, V> set,
+				Consumer<T> onEmpty) {
 			super(comp, (c, u) -> {
 				if (Objects.equals(c.getValue(), get.apply(u))) return;
 				c.setValue(get.apply(u));
 			});
-			this.func = (t, u) -> {
+			this.updateModel = (t, u) -> {
 				if (t.equals(get.apply(u))) return false;
 				set.accept(u, t);
 				return true;
@@ -90,14 +87,14 @@ public class ObservableController {
 				AnyActionDocumentListener.skipEmpty(comp, evt -> accept( ) );
 			} else {
 				AnyActionDocumentListener.emptyOrText( comp, 
-						e -> set.accept( impl.getModel(), onEmpty ), 
+						e -> onEmpty.accept( impl.getModel() ), 
 						evt -> accept( ) );
 			}
 		}
 
 		@SuppressWarnings("unchecked")
 		private boolean update() {
-			return func.apply( (V) impl.getComponent().getValue( ), impl.getModel() );
+			return updateModel.apply( (V) impl.getComponent().getValue( ), impl.getModel() );
 		}
 
 		private void accept() {
@@ -125,9 +122,9 @@ public class ObservableController {
 	 * Construct an ObservableController that modifies its model whenever the linked
 	 * JTextComponent is updated, including deleting all of the text.
 	 * @param comp A GUI object that is to be coupled with the model
-	 * @param func An update function to modify the model from text content
+	 * @param updateModel An update function to modify the model from text content
 	 * @param update The standard callback to set the component when the model changes elsewhere
-	 * @param onEmpty A special handler to set model value when we delete the content of the
+	 * @param onEmpty A special handler to set model str when we delete the content of the
 	 * component.
 	 */
 	public <S extends JTextComponent, T extends Observable> ObservableListener<S, T> from(
@@ -145,7 +142,7 @@ public class ObservableController {
 	 * @param get A model getter function to be called on the observed object
 	 * @param set A model setter function to be called on the observed object
 	 */
-	public <S extends JTextComponent, T extends Observable, V> ObservableListener<S, T> from(
+	public <S extends JTextComponent, T extends Observable> ObservableListener<S, T> from(
 			@NonNull final S comp, @NonNull final Function<T, String> get,
 			@NonNull final BiConsumer<T, String> set) {
 		return from(comp, get, set, null);
@@ -159,22 +156,21 @@ public class ObservableController {
 	 * @param get A model getter function to be called on the observed object
 	 * @param set A model setter function to be called on the observed object
 	 */
+	@SuppressWarnings("unchecked")
 	public <S extends JTextComponent, T extends Observable> ObservableListener<S, T> from(
 			@NonNull final S comp, @NonNull final Function<T, String> get,
 			@NonNull final BiConsumer<T, String> set, Consumer<T> onEmpty) {
-		return new TextFieldController<S, T>(comp, get, set, onEmpty);
+		if (comp instanceof JFormattedTextField) {
+			final JFormattedTextField fmt = (JFormattedTextField) comp;
+			return (ObservableListener<S, T>) new FormattedTextFieldController<>(fmt, get, set, onEmpty);
+		} else {
+			return new TextFieldController<S, T>(comp, get, set, onEmpty);
+		}
 	}
 	
-	/**
-	 * Construct an ObservableController that ties together a JFormattedTextField with
-	 * the member of an observable object such that updates flow between the model
-	 * and the JFormattedTextField. Ignores changes when the text document is empty.
-	 * @param comp The component being watched
-	 * @param get A model getter function to be called on the observed object
-	 * @param set A model setter function to be called on the observed object
-	 */
 	public <S extends JFormattedTextField, T extends Observable, V> ObservableListener<S, T> from(
-			@NonNull final S comp, @NonNull final Function<T, V> get, @NonNull final BiConsumer<T, V> set) {
-		return new FormattedTextFieldController<>(comp, get, set, null);
+			@NonNull final S comp, @NonNull final Function<T, V> get, @NonNull final BiConsumer<T, V> set,
+			final V onEmpty) {
+		return new FormattedTextFieldController<>(comp, get, set, model -> set.accept(model, onEmpty));
 	}
 }

+ 2 - 0
src/test/java/org/leumasjaffe/observer/IndirectObservableListenerTest.java

@@ -1,6 +1,8 @@
 package org.leumasjaffe.observer;
 
 import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.Mockito.*;
 
 import java.util.Arrays;

+ 287 - 0
src/test/java/org/leumasjaffe/observer/ObservableControllerTest.java

@@ -0,0 +1,287 @@
+package org.leumasjaffe.observer;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+
+import javax.swing.JFormattedTextField;
+import javax.swing.JTextField;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class ObservableControllerTest extends SwingTestCase {
+	private static class Value extends Observable.Instance {
+		String str = "<INIT>";
+		String get() { return str; }
+		void set(String value) { this.str = value; }
+	}
+	
+	@Spy MockObserverListener signal;
+	@Mock Consumer<Value> onEmpty;
+	
+	@Spy Value value;
+	
+	@Spy JTextField component;
+	@Spy JFormattedTextField formatted;
+	
+	@BeforeEach
+	void setUp() {
+		signal.setObserved(value);
+		clearInvocations(signal);
+	}
+
+	@Test
+	void testThrowsOnNullForAnythingButEmptyHandler() {
+		assertThrows(NullPointerException.class,
+				() -> ObservableController.from(null, Value::get, Value::set));
+		assertThrows(NullPointerException.class,
+				() -> ObservableController.from(component, null, Value::set));
+		assertThrows(NullPointerException.class,
+				() -> ObservableController.from(component, Value::get, null));
+
+		assertThrows(NullPointerException.class,
+				() -> ObservableController.from(null, (str, model) -> false, (comp, model) -> {}));
+		assertThrows(NullPointerException.class,
+				() -> ObservableController.from(component, (BiFunction<String, Value, Boolean>) null, (comp, model) -> {}));
+		assertThrows(NullPointerException.class,
+				() -> ObservableController.from(component, (str, model) -> false, null));
+
+		assertThrows(NullPointerException.class,
+				() -> ObservableController.from((JTextField) null, Value::get, Value::set, null));
+		assertThrows(NullPointerException.class,
+				() -> ObservableController.from((JFormattedTextField) null, Value::get, Value::set, (String) null));
+		assertThrows(NullPointerException.class,
+				() -> ObservableController.from(component, null, Value::set, null));
+		assertThrows(NullPointerException.class,
+				() -> ObservableController.from(component, Value::get, null, null));
+
+		assertThrows(NullPointerException.class,
+				() -> ObservableController.from(null, (str, model) -> false, (comp, model) -> {}, null));
+		assertThrows(NullPointerException.class,
+				() -> ObservableController.from(component, (BiFunction<String, Value, Boolean>) null, (comp, model) -> {}, null));
+		assertThrows(NullPointerException.class,
+				() -> ObservableController.from(component, (str, model) -> false, null, null));
+		
+		assertDoesNotThrow(() -> ObservableController.from(component, Value::get, Value::set));
+		assertDoesNotThrow(() -> ObservableController.from(component, Value::get, Value::set, null));
+		assertDoesNotThrow(() -> ObservableController.from(component, (str, model) -> false, (comp, model) -> {}));
+		assertDoesNotThrow(() -> ObservableController.from(component, (str, model) -> false, (comp, model) -> {}, null));
+
+		assertDoesNotThrow(() -> ObservableController.from(formatted, Value::get, Value::set));
+		assertDoesNotThrow(() -> ObservableController.from(formatted, Value::get, Value::set, (String) null));
+	}
+
+	@Test
+	void testCanLinkComponentToObjectBeanlike() {
+		final ObservableListener<JTextField, Value> listener =
+				ObservableController.from(component, Value::get, Value::set);
+		
+		listener.setObserved(value);
+		
+		verify(component).setText("<INIT>");
+		
+		component.setText("Hello");
+		waitForSwing();
+		
+		verify(value).set(eq("Hello"));
+		verify(signal).updateWasSignalled();
+	}
+	
+	@Test
+	void testWillNotSignalOnNonChangingOperation() {
+		final ObservableListener<JTextField, Value> listener =
+				ObservableController.from(component, Value::get, Value::set);
+		
+		listener.setObserved(value);
+		
+		component.setText("<INIT>");
+		waitForSwing();
+	
+		verify(signal, never()).updateWasSignalled();
+		verify(value, never()).set(eq("<INIT>"));
+	}
+	
+	@Test
+	void testWillNotFireUpdateIfModelHasNotChanged() {
+		final ObservableListener<JTextField, Value> listener =
+				ObservableController.from(component, Value::get, Value::set);
+		
+		listener.setObserved(value);
+		
+		ObserverDispatch.notifySubscribers(value);
+		
+		verify(component, times(1)).setText("<INIT>");
+	}
+	
+	@Test
+	void testByDefaultWeIgnoreEmptyString() {
+		final ObservableListener<JTextField, Value> listener =
+				ObservableController.from(component, Value::get, Value::set);
+		
+		listener.setObserved(value);
+		
+		component.setText("");
+		waitForSwing();
+
+		verify(value, never()).set(eq(""));
+		verify(signal, never()).updateWasSignalled();
+	}
+	
+	@Test
+	void testCanProvideEmptyStringHandler() {
+		final ObservableListener<JTextField, Value> listener =
+				ObservableController.from(component, Value::get, Value::set,
+						onEmpty);
+		
+		listener.setObserved(value);
+		
+		component.setText("");
+		waitForSwing();
+
+		verify(onEmpty).accept(same(value));
+		verify(signal, never()).updateWasSignalled();
+	}
+	
+	@Test
+	void testIfUpdateModelReturnsFalseThenWeDontSignal() {
+		final ObservableListener<JTextField, Value> listener =
+				ObservableController.from(component, (str, model) -> false,
+						(comp, model) -> {});
+		
+		listener.setObserved(value);
+
+		component.setText("Something");
+		waitForSwing();
+		
+		verify(signal, never()).updateWasSignalled();
+	}
+	
+	@Test
+	void testIfUpdateModelReturnsTrueThenWeSignal() {
+		final ObservableListener<JTextField, Value> listener =
+				ObservableController.from(component, (str, model) -> true,
+						(comp, model) -> {});
+		
+		listener.setObserved(value);
+		
+		component.setText("Something");
+		waitForSwing();
+		
+		verify(signal).updateWasSignalled();
+	}
+	
+	@Test
+	void testIfSignalledThenConsumerIsInvoked() {
+		@SuppressWarnings("unchecked")
+		BiConsumer<JTextField, Value> updateComp = mock(BiConsumer.class);
+		final ObservableListener<JTextField, Value> listener =
+				ObservableController.from(component, (str, model) -> false,
+						updateComp);
+		
+		listener.setObserved(value);
+		ObserverDispatch.notifySubscribers(value);
+
+		verify(updateComp, times(2)).accept(same(component), same(value));
+	}
+
+	@Test
+	void testFormattedAffectValueOverText() {
+		final ObservableListener<JFormattedTextField, Value> listener =
+				ObservableController.from(formatted, Value::get, Value::set);
+
+		listener.setObserved(value);
+		value.set("1");
+		ObserverDispatch.notifySubscribers(value);
+
+		verify(formatted).setValue(eq("1"));
+	}
+
+	@Test
+	void testFormattedManipulatingTextDoesntPropogate() {
+		final ObservableListener<JFormattedTextField, Value> listener =
+				ObservableController.from(formatted, Value::get, Value::set);
+
+		listener.setObserved(value);
+		formatted.setText("1");
+		waitForSwing();
+
+		verify(value, never()).set(eq("1"));
+	}
+
+	@Test
+	void testFormattedManipulatingValuePropogates() {
+		final ObservableListener<JFormattedTextField, Value> listener =
+				ObservableController.from(formatted, Value::get, Value::set);
+
+		listener.setObserved(value);
+		formatted.setValue("1");
+		waitForSwing();
+
+		verify(value).set(eq("1"));
+	}
+
+	@Test
+	void testFormattedManipulatingValueDoesNothingWhenEqual() {
+		final ObservableListener<JFormattedTextField, Value> listener =
+				ObservableController.from(formatted, Value::get, Value::set);
+
+		listener.setObserved(value);
+		formatted.setValue("<INIT>");
+		waitForSwing();
+
+		verify(value, never()).set(any());
+	}
+
+	@Test
+	void testFormattedManipulatingModelDoesNothingWhenEqual() {
+		final ObservableListener<JFormattedTextField, Value> listener =
+				ObservableController.from(formatted, Value::get, Value::set);
+
+		listener.setObserved(value);
+		clearInvocations(formatted);
+
+		ObserverDispatch.notifySubscribers(value);
+
+		verify(formatted, never()).setValue(any());
+	}
+	
+	@Test
+	void testFormattedCanProvideEmptyStringHandler() {
+		final ObservableListener<JFormattedTextField, Value> listener =
+				ObservableController.from(formatted, Value::get, Value::set,
+						onEmpty);
+		
+		listener.setObserved(value);
+		
+		formatted.setValue("");
+		waitForSwing();
+
+		verify(onEmpty).accept(same(value));
+		verify(signal, never()).updateWasSignalled();
+	}
+	
+	@Test
+	void testFormattedCanProvideDefaultValueOnEmpty() {
+		final ObservableListener<JFormattedTextField, Value> listener =
+				ObservableController.from(formatted, Value::get, Value::set,
+						"<DEFAULT>");
+		
+		listener.setObserved(value);
+		
+		formatted.setValue("");
+		waitForSwing();
+
+		verify(signal, never()).updateWasSignalled();
+		verify(value).set(eq("<DEFAULT>"));
+		verify(value, never()).set(eq(""));
+	}
+}

+ 40 - 0
src/test/java/org/leumasjaffe/observer/SwingTestCase.java

@@ -0,0 +1,40 @@
+package org.leumasjaffe.observer;
+
+import java.lang.reflect.InvocationTargetException;
+
+import javax.swing.JFrame;
+import javax.swing.SwingUtilities;
+
+import org.junit.jupiter.api.AfterEach;
+
+public class SwingTestCase {
+    private JFrame testFrame;
+
+    @AfterEach
+    protected void tearDown() {
+        if (this.testFrame != null) {
+            this.testFrame.dispose();
+            this.testFrame = null;
+        }
+    }
+
+    public JFrame getTestFrame() {
+        if (this.testFrame == null) {
+            this.testFrame = new JFrame("Test");
+        }
+        return this.testFrame;
+    }
+    
+    public void waitForSwing() {
+        if (!SwingUtilities.isEventDispatchThread(  )) {
+            try {
+                SwingUtilities.invokeAndWait(new Runnable(  ) {
+                    public void run(  ) {
+                    }
+                });
+            } catch (InterruptedException e) {
+            } catch (InvocationTargetException e) {
+            }
+        }
+    }
+}