浏览代码

Merge branch 'feat/more-autogrows'

* feat/more-autogrows: (24 commits)
  Fix auto-grow panel not updating indices.
  Fix NPE in ReplaceChildrenControllerTest
  Fix components not disappearing properly.
  Add hookups to update the Element Name
  Switch RecipeCardPanel to use AutoGrow.
  Clean up StepPanelIT
  Fix up a bunch of objects to properly use removeNotify().
  Add more tests.
  Add state test to ElementPanel ahead of making it into an AutoGrowPanel.ChildComponent.
  Clean up some test case legacy stuff.
  Make StepPanel test use fewer getters.
  Change StepPanel to the construct -> setModel pattern which allows better mocking in tests.
  BetterAutoGrowPanel becomes AutoGrowPanel
  Move PhasePanel to use BetterAutoGrowPanel
  Move StepPanel to use BetterAutoGrowPanel.
  Fix layout in BetterAutoGrowPanel
  Fix a bug where we weren't adding the model components above the empty one.
  Remove debugging statement
  Create a better version of AutoGrowPanel, to be swapped in next commit.
  Fix up StepPanel to properly link the Instruction pane
  ...
Sam Jaffe 5 年之前
父节点
当前提交
cbef269bad
共有 27 个文件被更改,包括 739 次插入319 次删除
  1. 4 0
      src/main/lombok/org/leumasjaffe/recipe/controller/ReplaceChildrenController.java
  2. 3 3
      src/main/lombok/org/leumasjaffe/recipe/model/RecipeCard.java
  3. 120 74
      src/main/lombok/org/leumasjaffe/recipe/view/AutoGrowPanel.java
  4. 32 14
      src/main/lombok/org/leumasjaffe/recipe/view/ElementPanel.java
  5. 11 9
      src/main/lombok/org/leumasjaffe/recipe/view/IngredientPanel.java
  6. 25 21
      src/main/lombok/org/leumasjaffe/recipe/view/PhasePanel.java
  7. 19 9
      src/main/lombok/org/leumasjaffe/recipe/view/PreparationPanel.java
  8. 20 7
      src/main/lombok/org/leumasjaffe/recipe/view/RecipeCardPanel.java
  9. 10 3
      src/main/lombok/org/leumasjaffe/recipe/view/RestPanel.java
  10. 44 35
      src/main/lombok/org/leumasjaffe/recipe/view/StepPanel.java
  11. 10 5
      src/main/lombok/org/leumasjaffe/recipe/view/summary/ElementPanel.java
  12. 3 3
      src/test/java/org/leumasjaffe/recipe/controller/ReplaceChildrenControllerTest.java
  13. 8 8
      src/test/java/org/leumasjaffe/recipe/model/DurationTest.java
  14. 111 31
      src/test/java/org/leumasjaffe/recipe/view/AutoGrowPanelTest.java
  15. 52 13
      src/test/java/org/leumasjaffe/recipe/view/ElementPanelTest.java
  16. 0 3
      src/test/java/org/leumasjaffe/recipe/view/FileMenuTest.java
  17. 0 3
      src/test/java/org/leumasjaffe/recipe/view/IngredientPanelTest.java
  18. 86 0
      src/test/java/org/leumasjaffe/recipe/view/PhasePanelIT.java
  19. 0 3
      src/test/java/org/leumasjaffe/recipe/view/PhasePanelTest.java
  20. 9 4
      src/test/java/org/leumasjaffe/recipe/view/PreparationPanelTest.java
  21. 93 0
      src/test/java/org/leumasjaffe/recipe/view/RecipeCardPanelIT.java
  22. 23 28
      src/test/java/org/leumasjaffe/recipe/view/RecipeCardPanelTest.java
  23. 2 5
      src/test/java/org/leumasjaffe/recipe/view/RestPanelTest.java
  24. 16 15
      src/test/java/org/leumasjaffe/recipe/view/StepPanelIT.java
  25. 38 17
      src/test/java/org/leumasjaffe/recipe/view/StepPanelTest.java
  26. 0 3
      src/test/java/org/leumasjaffe/recipe/view/summary/ElementPanelTest.java
  27. 0 3
      src/test/java/org/leumasjaffe/recipe/view/summary/SummaryPanelTest.java

+ 4 - 0
src/main/lombok/org/leumasjaffe/recipe/controller/ReplaceChildrenController.java

@@ -22,6 +22,10 @@ public class ReplaceChildrenController<T, V> implements BiConsumer<Container, T>
 		if (parent.getComponents().length == children.size()) {
 			return;
 		}
+		// Make sure that our components disappear correctly
+		for (final Component comp : parent.getComponents()) {
+			comp.setVisible(false);
+		}
 		parent.removeAll();
 		children.stream().map(makeView).forEach(parent::add);
 	}

+ 3 - 3
src/main/lombok/org/leumasjaffe/recipe/model/RecipeCard.java

@@ -14,9 +14,9 @@ import lombok.EqualsAndHashCode;
 
 @Data @EqualsAndHashCode(callSuper=false)
 public class RecipeCard extends Observable.Instance implements CompoundRecipeComponent {
-	String title;
-	String description;
-	int servings;
+	String title = "";
+	String description = "";
+	int servings = 1;
 	// TODO: Nutrition information
 	Optional<ImageIcon> photo = Optional.empty(); // TODO JSONIZATION	
 	List<Element> elements = new ArrayList<>();

+ 120 - 74
src/main/lombok/org/leumasjaffe/recipe/view/AutoGrowPanel.java

@@ -5,8 +5,6 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Consumer;
 import java.util.function.Function;
-import java.util.function.IntConsumer;
-import java.util.function.IntFunction;
 import java.util.function.Supplier;
 
 import javax.swing.JPanel;
@@ -14,101 +12,149 @@ import javax.swing.event.DocumentEvent;
 import javax.swing.event.DocumentListener;
 
 import org.jdesktop.swingx.VerticalLayout;
-import org.leumasjaffe.event.AnyActionDocumentListener;
 
+import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.Delegate;
+import lombok.experimental.FieldDefaults;
+import lombok.experimental.NonFinal;
 
-@SuppressWarnings("serial")
-public class AutoGrowPanel extends JPanel {
-	public static interface DocumentListenable {
-		void addDocumentListener(DocumentListener dl);
-		void removeDocumentListener(DocumentListener dl);
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class AutoGrowPanel<C extends Component & AutoGrowPanel.ChildComponent, T> extends JPanel {
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 3815045801030954255L;
+
+	private static interface SetGap { void setGap(int gap); }
+
+	public static interface ChildComponent {
+		void addGrowShrinkListener(DocumentListener dl);
+		void removeGrowShrinkListener(DocumentListener dl);
 		default void setListPosition(int zeroIndex) {}
 	}
 	
-	@AllArgsConstructor
-	private class DeleteOnEmpty implements AnyActionDocumentListener {
-		DocumentListenable content;
-		@Override public void update(DocumentEvent e) {
-			if (e.getDocument().getLength() == 0) {
-				content.removeDocumentListener(this);
-				int index = members.indexOf(content);
-				members.remove(index);
-				onDelete.accept(index);
-				for (final int size = members.size(); index < size; ++index) {
-					members.get(index).setListPosition(index);
-				}
-				remove((Component) content);
-				validateNthParent(4);
+	@RequiredArgsConstructor
+	@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+	private class GrowOnData implements DocumentListener {
+		Supplier<T> makeEmptyModel;
+		Function<T, C> makeComponent;
+		@NonFinal T model = null;
+		
+		@Override public void changedUpdate(DocumentEvent e) {}
+		@Override public void removeUpdate(DocumentEvent e) {}
+
+		@Override
+		public void insertUpdate(DocumentEvent e) {
+			if (model != null) {
+				models.add(model);
+				last().removeGrowShrinkListener(this);
+				last().addGrowShrinkListener(new ShrinkOnEmpty(last()));
 			}
+			
+			model = makeEmptyModel.get();
+			final C comp = makeComponent.apply(model);
+
+			members.add(comp);
+			add(comp);
+			comp.addGrowShrinkListener(this);
+			comp.setListPosition(lastIndex());
+			callback.accept(true);
 		}		
 	}
 	
 	@AllArgsConstructor
-	private class ExtendAction<T, C extends Component & DocumentListenable> implements AnyActionDocumentListener {
-		final Function<T, C> factory;
-		final Consumer<? super T> previous;
-		final Supplier<? extends T> next;
-		T current = null;
+	private class ShrinkOnEmpty implements DocumentListener {
+		ChildComponent component;
+		
+		@Override public void insertUpdate(DocumentEvent e) {}
+		@Override public void changedUpdate(DocumentEvent e) {}
 
 		@Override
-		public void update(DocumentEvent e) {
-			previous.accept(current);
-			
-			final C object = factory.apply(current = next.get());
-			final DocumentListenable back = getBack();
-			
-			back.removeDocumentListener(this);
-			back.addDocumentListener(new DeleteOnEmpty(back));			
-			object.addDocumentListener(this);
+		public void removeUpdate(DocumentEvent e) {
+			if (e.getDocument().getLength() > 0) {
+				return;
+			}
 			
-			members.add(object);
-			add(object);
-
-			validateNthParent(4);
+			component.removeGrowShrinkListener(this);
+			remove(members.indexOf(component));
+			callback.accept(false);
 		}
-		
 	}
 	
-	IntFunction<DocumentListenable> prod;
-	AnyActionDocumentListener grow;
-	IntConsumer onDelete;
-	List<DocumentListenable> members = new ArrayList<>();
+	@Delegate(types={SetGap.class})
+	VerticalLayout layout = new VerticalLayout();
+	List<ChildComponent> members = new ArrayList<>();
+	GrowOnData grow;
 	
-	@SafeVarargs
-	public <T, C extends Component & DocumentListenable> AutoGrowPanel(Function<T, C> function,
-			Supplier<T> newItem, Consumer<? super T> onData, IntConsumer onDelete, T...ts) {
-		setLayout(new VerticalLayout());
-		
-		T next = newItem.get();
-		this.onDelete = onDelete;
-		this.grow = new ExtendAction<T, C>(function, onData, newItem, next);
+	@NonFinal List<T> models = null;
+	@NonFinal Consumer<Boolean> callback = (b) -> {};
+	
+	/**
+	 * 
+	 * @param makeEmptyModel A function to produce a blank model object for display.
+	 * If the model is updated in such a way as to be non-empty, it will be inserted
+	 * into the list of models.
+	 * @param makeComponent A function to generate a UI object given a model. The
+	 * object must meet the ChildComponent interface to install the AutoGrowPanel's
+	 * growing/shrinking listener objects.
+	 */
+	public AutoGrowPanel(final @NonNull Supplier<T> makeEmptyModel,
+			final @NonNull Function<T, C> makeComponent) {
+		setLayout(layout);
+		this.grow = new GrowOnData(makeEmptyModel, makeComponent);
+		this.grow.insertUpdate(null);
+	}
+	
+	/**
+	 * Activate this component against the target list of children
+	 * @param models A mutable list object containing the "child models" to be
+	 * rendered in this component
+	 */
+	public void setModel(final @NonNull List<T> models) {
+		setModel(models, (b) -> {});
+	}
+	
+	/**
+	 * 
+	 * @param models A mutable list object containing the "child models" to be
+	 * rendered in this component
+	 * @param callback A callback that will be invoked each time a child is
+	 * added or removed. This allows us to provide some custom interactions
+	 * with the parent's context in case other actions need to occur.
+	 */
+	public void setModel(final @NonNull List<T> models,
+			final Consumer<Boolean> callback) {
+		this.models = models;
+		this.callback = callback;
 		
-		for (T value : ts) {
-			C listen = function.apply(value);
-			members.add(listen);
-			add(listen);
-			listen.addDocumentListener(new DeleteOnEmpty(listen));
-		}
+		this.members.subList(0, lastIndex()).clear();
+		models.forEach(model -> {
+			final C comp = this.grow.makeComponent.apply(model);
+			comp.addGrowShrinkListener(new ShrinkOnEmpty(comp));
+			add(comp, lastIndex());
+			comp.setListPosition(lastIndex());
+			this.members.add(lastIndex(), comp);
+		});
 		
-		C empty = function.apply(next);
-		members.add(empty);
-		add(empty);
-		empty.addDocumentListener(this.grow);
-	}
-
-	private DocumentListenable getBack() {
-		return members.get(members.size() - 1);
+		last().setListPosition(lastIndex());
 	}
 	
-	private void validateNthParent(int i) {
-		Component current = getParent();
-		Component next = current;
-		while (i --> 0 && next != null) {
-			current = next;
-			next = current.getParent();
+	@Override
+	public void remove(final int index) {
+		super.remove(index);
+		members.remove(index);
+		for (int size = lastIndex(); size >= index; --size) {
+			members.get(index).setListPosition(index);
 		}
-		current.validate();
+		models.remove(index);
 	}
 	
+	private int lastIndex() { return members.size() - 1; }
+	
+	private ChildComponent last() {
+		return members.get(lastIndex());
+	}
 }

+ 32 - 14
src/main/lombok/org/leumasjaffe/recipe/view/ElementPanel.java

@@ -4,21 +4,24 @@ import javax.swing.JPanel;
 import javax.swing.JScrollPane;
 
 import org.leumasjaffe.observer.ForwardingObservableListener;
+import org.leumasjaffe.observer.ObservableController;
 import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Phase;
 import org.leumasjaffe.recipe.model.Element;
 
 import lombok.AccessLevel;
+import lombok.Getter;
 import lombok.experimental.FieldDefaults;
 
 import org.jdesktop.swingx.VerticalLayout;
 
 import javax.swing.JSeparator;
+import javax.swing.JTextField;
 import javax.swing.ScrollPaneConstants;
+import javax.swing.event.DocumentListener;
 
 import java.awt.GridBagLayout;
-import javax.swing.JLabel;
 import java.awt.GridBagConstraints;
 import java.awt.Insets;
 import java.awt.Component;
@@ -27,12 +30,13 @@ import java.awt.Dimension;
 import javax.swing.Box;
 
 @SuppressWarnings("serial")
-@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class ElementPanel extends JScrollPane {
-	ObservableListener<CollatedDurationPanel, Element> durationListener;
+@FieldDefaults(level=AccessLevel.PRIVATE)
+public class ElementPanel extends JScrollPane implements AutoGrowPanel.ChildComponent {
 	ForwardingObservableListener<Element> listener = new ForwardingObservableListener<>();
+	ObservableListener<JTextField, Element> nameController;
+	ObservableListener<CollatedDurationPanel, Element> durationListener;
 
-	JLabel lblName;
+	@Getter(AccessLevel.PACKAGE) JTextField txtName;
 	JPanel panelViewPort;
 
 	public ElementPanel() {
@@ -50,12 +54,12 @@ public class ElementPanel extends JScrollPane {
 		gbl_panelColumnHeader.rowWeights = new double[]{0.0, Double.MIN_VALUE};
 		panelColumnHeader.setLayout(gbl_panelColumnHeader);
 		
-		lblName = new JLabel();
-		GridBagConstraints gbc_lblName = new GridBagConstraints();
-		gbc_lblName.insets = new Insets(0, 0, 0, 5);
-		gbc_lblName.gridx = 0;
-		gbc_lblName.gridy = 0;
-		panelColumnHeader.add(lblName, gbc_lblName);
+		txtName = new JTextField();
+		GridBagConstraints gbc_txtName = new GridBagConstraints();
+		gbc_txtName.insets = new Insets(0, 0, 0, 5);
+		gbc_txtName.gridx = 0;
+		gbc_txtName.gridy = 0;
+		panelColumnHeader.add(txtName, gbc_txtName);
 		
 		Component horizontalGlue = Box.createHorizontalGlue();
 		GridBagConstraints gbc_horizontalGlue = new GridBagConstraints();
@@ -73,7 +77,9 @@ public class ElementPanel extends JScrollPane {
 		panelViewPort = new JPanel();
 		setViewportView(panelViewPort);
 		panelViewPort.setLayout(new VerticalLayout(5));
-				
+		
+		nameController = ObservableController.from(txtName,
+				Element::getName, Element::setName);
 		durationListener = new ObservableListener<>(panelDuration,
 				(c, v) -> c.setModel(v.getCollatedDuration()));
 	}
@@ -84,8 +90,6 @@ public class ElementPanel extends JScrollPane {
 	}
 	
 	public void setModel(final Element element) {
-		lblName.setText(element.getName());
-		
 		panelViewPort.removeAll();
 		for (final Phase phase : element.getPhases()) {
 			panelViewPort.add(new PhasePanel(phase));
@@ -93,6 +97,7 @@ public class ElementPanel extends JScrollPane {
 		}
 		
 		listener.setObserved(element, element.getPhases());
+		nameController.setObserved(element);
 		durationListener.setObserved(element);
 	}
 	
@@ -100,5 +105,18 @@ public class ElementPanel extends JScrollPane {
 	public void removeNotify() {
 		super.removeNotify();
 		ObserverDispatch.unsubscribeAll(listener);
+		ObserverDispatch.unsubscribeAll(nameController);
+		ObserverDispatch.unsubscribeAll(durationListener);
 	}
+
+	@Override
+	public void addGrowShrinkListener(DocumentListener dl) {
+		this.txtName.getDocument().addDocumentListener(dl);
+	}
+
+	@Override
+	public void removeGrowShrinkListener(DocumentListener dl) {
+		this.txtName.getDocument().removeDocumentListener(dl);
+	}
+	
 }

+ 11 - 9
src/main/lombok/org/leumasjaffe/recipe/view/IngredientPanel.java

@@ -23,7 +23,7 @@ import javax.swing.JLabel;
 
 @SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentListenable {
+public class IngredientPanel extends JPanel implements AutoGrowPanel.ChildComponent {
 	ObservableListener<JTextField, Ingredient> nameController;
 	ObservableListener<JFormattedTextField, Ingredient> amountController;
 	ObservableListener<JTextField, Ingredient> preparationController;
@@ -96,19 +96,21 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentLis
 	}
 	
 	@Override
-	public void addDocumentListener(DocumentListener dl) {
-		this.txtName.getDocument().addDocumentListener(dl);
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(nameController);
+		ObserverDispatch.unsubscribeAll(amountController);
+		ObserverDispatch.unsubscribeAll(preparationController);
 	}
 
 	@Override
-	public void removeDocumentListener(DocumentListener dl) {
-		this.txtName.getDocument().removeDocumentListener(dl);
+	public void addGrowShrinkListener(DocumentListener dl) {
+		this.txtName.getDocument().addDocumentListener(dl);
 	}
-	
+
 	@Override
-	public void removeNotify() {
-		super.removeNotify();
-		ObserverDispatch.unsubscribeAll(nameController);
+	public void removeGrowShrinkListener(DocumentListener dl) {
+		this.txtName.getDocument().removeDocumentListener(dl);
 	}
 
 }

+ 25 - 21
src/main/lombok/org/leumasjaffe/recipe/view/PhasePanel.java

@@ -9,45 +9,49 @@ import org.leumasjaffe.observer.ForwardingObservableListener;
 import org.leumasjaffe.observer.Observable;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Phase;
-import org.leumasjaffe.recipe.model.Preparation;
-import org.leumasjaffe.recipe.model.Rest;
 import org.leumasjaffe.recipe.model.Step;
 
 import lombok.AccessLevel;
+import lombok.Getter;
 import lombok.experimental.FieldDefaults;
-import lombok.experimental.NonFinal;
 
 import org.jdesktop.swingx.VerticalLayout;
 
 @SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class PhasePanel extends JPanel {
-	@NonFinal int steps = 0;
 	ForwardingObservableListener<Phase> listener = new ForwardingObservableListener<>();
 
+	@Getter(AccessLevel.PACKAGE) AutoGrowPanel<StepPanel, Step> panelSteps;
+	
+	// TODO Re-configure to support this(); setModel(phase); pattern.
 	public PhasePanel(final Phase phase) {		
 		setLayout(new VerticalLayout(5));
 		
-		phase.getPreparation().ifPresent(this::addPrep);
-		phase.getCooking().forEach(this::addStep);
-		phase.getRest().ifPresent(this::addRest);
-
+		if (phase.getPreparation().isPresent()) {
+			add(new PreparationPanel(phase));
+		}
+		
+		panelSteps = new AutoGrowPanel<>(Step::new, StepPanel::new);
+		panelSteps.setGap(5);
+		add(panelSteps);
+		
+		phase.getRest().ifPresent(rest -> add(new RestPanel(rest)));
+
+		panelSteps.setModel(phase.getCooking(), added -> {
+			listener.setObserved(phase, allChildren(phase));
+			if (!added) {
+				ObserverDispatch.notifySubscribers(phase);
+			}
+		});
+		listener.setObserved(phase, allChildren(phase));
+	}
+	
+	private static List<Observable> allChildren(Phase phase) {
 		List<Observable> children = new ArrayList<>(phase.getCooking());
 		phase.getPreparation().ifPresent(children::add);
 		phase.getRest().ifPresent(children::add);
-		listener.setObserved(phase, children);
-	}
-	
-	void addPrep(final Preparation step) {
-		add(new PreparationPanel(step));
-	}
-	
-	void addStep(final Step step) {
-		add(new StepPanel(steps++, step));
-	}
-	
-	void addRest(final Rest rest) {
-		add(new RestPanel(rest));
+		return children;
 	}
 	
 	@Override

+ 19 - 9
src/main/lombok/org/leumasjaffe/recipe/view/PreparationPanel.java

@@ -3,11 +3,14 @@ package org.leumasjaffe.recipe.view;
 import javax.swing.JPanel;
 
 import org.jdesktop.swingx.VerticalLayout;
+import org.leumasjaffe.observer.IndirectObservableListener;
 import org.leumasjaffe.observer.ObservableController;
 import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.controller.ReplaceChildrenController;
 import org.leumasjaffe.recipe.model.Duration;
 import org.leumasjaffe.recipe.model.Ingredient;
+import org.leumasjaffe.recipe.model.Phase;
 import org.leumasjaffe.recipe.model.Preparation;
 
 import lombok.AccessLevel;
@@ -27,8 +30,8 @@ import javax.swing.JFormattedTextField;
 @FieldDefaults(level=AccessLevel.PRIVATE)
 public class PreparationPanel extends JPanel {
 	ReplaceChildrenController<Preparation, Ingredient> controller;
-	ObservableListener<JPanel, Preparation> childListener;
-	ObservableListener<JFormattedTextField, Preparation> durationListener;
+	IndirectObservableListener<JPanel, Preparation> childListener;
+	ObservableListener<JFormattedTextField, Preparation> durationController;
 	
 	public PreparationPanel() {
 		controller = new ReplaceChildrenController<>(Preparation::getIngredients,
@@ -87,20 +90,27 @@ public class PreparationPanel extends JPanel {
 		panelLeft.add(panelIngredients, gbc_panelIngredients);
 		
 		// This indirection allows for testing of controller
-		childListener = new ObservableListener<>(panelIngredients,
+		childListener = new IndirectObservableListener<>(panelIngredients,
 				(c, v) -> controller.accept(c, v));
-		durationListener = ObservableController.from(panelDuration.txtTime,
+		durationController = ObservableController.from(panelDuration.txtTime,
 				Preparation::getDuration, Preparation::setDuration);
 	}
 	
-	public PreparationPanel(final Preparation preparation) {
+	public PreparationPanel(final Phase phase) {
 		this();
-		setModel(preparation);
+		setModel(phase);
 	}
 	
-	public void setModel(final Preparation preparation) {
-		durationListener.setObserved(preparation);
-		childListener.setObserved(preparation);
+	public void setModel(final Phase phase) {
+		childListener.setObserved(phase.getPreparation().get(), phase);
+		durationController.setObserved(phase.getPreparation().get());
+	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(childListener);
+		ObserverDispatch.unsubscribeAll(durationController);
 	}
 
 }

+ 20 - 7
src/main/lombok/org/leumasjaffe/recipe/view/RecipeCardPanel.java

@@ -2,18 +2,19 @@ package org.leumasjaffe.recipe.view;
 
 import javax.swing.JSplitPane;
 
-import org.jdesktop.swingx.VerticalLayout;
 import org.leumasjaffe.observer.ForwardingObservableListener;
 import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
+import org.leumasjaffe.recipe.model.Element;
 import org.leumasjaffe.recipe.model.RecipeCard;
 import org.leumasjaffe.recipe.view.summary.SummaryPanel;
 
 import lombok.AccessLevel;
+import lombok.Getter;
 import lombok.experimental.FieldDefaults;
 
 import java.awt.Dimension;
 
-import javax.swing.JPanel;
 import javax.swing.JScrollPane;
 import javax.swing.ScrollPaneConstants;
 
@@ -24,16 +25,16 @@ public class RecipeCardPanel extends JSplitPane {
 	ForwardingObservableListener<RecipeCard> listener;
 	
 	SummaryPanel summaryPanel;
-	JPanel rightPanel;
+	@Getter(AccessLevel.PACKAGE) AutoGrowPanel<ElementPanel, Element> panelElements;
 	
 	public RecipeCardPanel() {
 		setPreferredSize(new Dimension(1050, 600));
 
 		summaryPanel = new SummaryPanel();
-		rightPanel = new JPanel();
-		rightPanel.setLayout(new VerticalLayout(5));
+		panelElements = new AutoGrowPanel<>(Element::new, ElementPanel::new);
+		panelElements.setGap(5);
 
-		final JScrollPane scrollPane = new JScrollPane(rightPanel);
+		final JScrollPane scrollPane = new JScrollPane(panelElements);
 		scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
 		scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
 		setRightComponent(scrollPane);
@@ -50,10 +51,22 @@ public class RecipeCardPanel extends JSplitPane {
 	
 	public void setModel(final RecipeCard card) {
 		summaryPanel.setModel(card);
-		card.getComponents().map(ElementPanel::new).forEach(rightPanel::add);
+		panelElements.setModel(card.getElements(), added -> {
+			listener.setObserved(card, card.getElements());
+			if (!added) {
+				ObserverDispatch.notifySubscribers(card);
+			}
+		});
 
 		listener.setObserved(card, card.getElements());
 		updateUI.setObserved(card);		
 	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(listener);
+		ObserverDispatch.unsubscribeAll(updateUI);
+	}
 
 }

+ 10 - 3
src/main/lombok/org/leumasjaffe/recipe/view/RestPanel.java

@@ -4,6 +4,7 @@ import javax.swing.JPanel;
 
 import org.leumasjaffe.observer.ObservableController;
 import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Rest;
 
 import lombok.AccessLevel;
@@ -19,7 +20,7 @@ import java.awt.Insets;
 @SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE)
 public class RestPanel extends JPanel {
-	ObservableListener<JFormattedTextField, Rest> durationListener;
+	ObservableListener<JFormattedTextField, Rest> durationController;
 
 	JLabel lblLocation;
 
@@ -51,7 +52,7 @@ public class RestPanel extends JPanel {
 		gbc_panelDuration.gridy = 0;
 		add(panelDuration, gbc_panelDuration);
 		
-		durationListener = ObservableController.from(panelDuration.txtTime,
+		durationController = ObservableController.from(panelDuration.txtTime,
 				Rest::getDuration, Rest::setDuration);
 	}
 
@@ -62,7 +63,13 @@ public class RestPanel extends JPanel {
 	
 	public void setModel(final Rest rest) {
 		lblLocation.setText(rest.getWhere().getHumanReadable());
-		durationListener.setObserved(rest);
+		durationController.setObserved(rest);
+	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(durationController);
 	}
 
 }

+ 44 - 35
src/main/lombok/org/leumasjaffe/recipe/view/StepPanel.java

@@ -18,7 +18,6 @@ import java.awt.GridBagLayout;
 
 import java.awt.GridBagConstraints;
 import java.awt.Insets;
-import java.util.List;
 
 import javax.swing.JLabel;
 import javax.swing.JTextPane;
@@ -29,16 +28,17 @@ import javax.swing.JFormattedTextField;
 import java.awt.Dimension;
 
 @SuppressWarnings("serial")
-@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class StepPanel extends JPanel implements AutoGrowPanel.DocumentListenable {
+@FieldDefaults(level=AccessLevel.PRIVATE)
+public class StepPanel extends JPanel implements AutoGrowPanel.ChildComponent {
 	ForwardingObservableListener<Step> listener = new ForwardingObservableListener<>();
-	ObservableListener<JFormattedTextField, Step> durationListener;
+	ObservableListener<JTextPane, Step> instructionListener;
+	ObservableListener<JFormattedTextField, Step> durationController;
 
-	@Getter(AccessLevel.PACKAGE) JLabel lblIndex;
+	JLabel lblIndex;
 	@Getter(AccessLevel.PACKAGE) JTextPane txtpnInstructions;
-	@Getter(AccessLevel.PACKAGE) AutoGrowPanel panelIngredients;
+	@Getter(AccessLevel.PACKAGE) AutoGrowPanel<IngredientPanel, Ingredient> panelIngredients;
 		
-	public StepPanel(int zeroIndex, Step step) {
+	public StepPanel() {
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0};
@@ -75,22 +75,13 @@ public class StepPanel extends JPanel implements AutoGrowPanel.DocumentListenabl
 		gbc_horizontalGlue.gridy = 0;
 		panelLeft.add(horizontalGlue, gbc_horizontalGlue);
 		
-		DurationPanel panelDuration = new DurationPanel("Requires", step.getDuration());
+		DurationPanel panelDuration = new DurationPanel("Requires");
 		GridBagConstraints gbc_panelDuration = new GridBagConstraints();
 		gbc_panelDuration.gridx = 2;
 		gbc_panelDuration.gridy = 0;
 		panelLeft.add(panelDuration, gbc_panelDuration);
 		
-		final List<Ingredient> ingredients = step.getIngredients();
-		panelIngredients = new AutoGrowPanel(IngredientPanel::new,
-				Ingredient::new, ing -> {
-					ingredients.add(ing);
-					listener.setObserved(step, ingredients);
-				}, i -> {
-					ingredients.remove(i);
-					listener.setObserved(step, ingredients);
-					ObserverDispatch.notifySubscribers(step);
-				}, ingredients.toArray(new Ingredient[0]));
+		panelIngredients = new AutoGrowPanel<>(Ingredient::new, IngredientPanel::new);
 		GridBagConstraints gbc_panelIngredients = new GridBagConstraints();
 		gbc_panelIngredients.gridwidth = 3;
 		gbc_panelIngredients.insets = new Insets(0, 0, 0, 5);
@@ -101,40 +92,58 @@ public class StepPanel extends JPanel implements AutoGrowPanel.DocumentListenabl
 		
 		txtpnInstructions = new JTextPane();
 		txtpnInstructions.setPreferredSize(new Dimension(200, 30));
-		txtpnInstructions.setText(step.getInstruction());
 		GridBagConstraints gbc_txtpnInstructions = new GridBagConstraints();
 		gbc_txtpnInstructions.fill = GridBagConstraints.BOTH;
 		gbc_txtpnInstructions.gridx = 1;
 		gbc_txtpnInstructions.gridy = 0;
 		add(txtpnInstructions, gbc_txtpnInstructions);
 		
-		durationListener = ObservableController.from(panelDuration.txtTime,
+		instructionListener = ObservableController.from(txtpnInstructions,
+				Step::getInstruction, Step::setInstruction);
+		durationController = ObservableController.from(panelDuration.txtTime,
 				Step::getDuration, Step::setDuration);
 		
-		setListPosition(zeroIndex);
-		listener.setObserved(step, ingredients);
-		durationListener.setObserved(step);
+		setListPosition(0);
 	}
-
-	@Override
-	public void addDocumentListener(DocumentListener dl) {
-		this.txtpnInstructions.getDocument().addDocumentListener(dl);
+	
+	public StepPanel(final Step step) {
+		this();
+		setModel(step);
 	}
-
-	@Override
-	public void removeDocumentListener(DocumentListener dl) {
-		this.txtpnInstructions.getDocument().removeDocumentListener(dl);		
+	
+	void setModel(final Step step) {
+		panelIngredients.setModel(step.getIngredients(), added -> {
+			listener.setObserved(step, step.getIngredients());
+			if (!added) {
+				ObserverDispatch.notifySubscribers(step);
+			}
+		});
+		listener.setObserved(step, step.getIngredients());
+		instructionListener.setObserved(step);
+		durationController.setObserved(step);
 	}
 	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(listener);
+		ObserverDispatch.unsubscribeAll(instructionListener);
+		ObserverDispatch.unsubscribeAll(durationController);
+	}
+
 	@Override
 	public void setListPosition(int zeroIndex) {
 		this.lblIndex.setText("Step " + Integer.toString(zeroIndex + 1));
 		repaint();
 	}
-	
+
 	@Override
-	public void removeNotify() {
-		super.removeNotify();
-		ObserverDispatch.unsubscribeAll(listener);
+	public void addGrowShrinkListener(DocumentListener dl) {
+		this.txtpnInstructions.getDocument().addDocumentListener(dl);
+	}
+
+	@Override
+	public void removeGrowShrinkListener(DocumentListener dl) {
+		this.txtpnInstructions.getDocument().removeDocumentListener(dl);		
 	}
 }

+ 10 - 5
src/main/lombok/org/leumasjaffe/recipe/view/summary/ElementPanel.java

@@ -20,7 +20,9 @@ import java.awt.GridBagConstraints;
 @SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class ElementPanel extends JPanel {
-	ObservableListener<JPanel, Element> listener;
+	ObservableListener<JLabel, Element> nameListener;
+	ObservableListener<JPanel, Element> childListener;
+	
 	@Getter(AccessLevel.PACKAGE) JLabel lblProductName;
 	@Getter(AccessLevel.PACKAGE) JPanel panelIngredients;
 	
@@ -32,7 +34,7 @@ public class ElementPanel extends JPanel {
 		gridBagLayout.rowWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
 		setLayout(gridBagLayout);
 		
-		lblProductName = new JLabel(element.getName());
+		lblProductName = new JLabel();
 		GridBagConstraints gbc_lblProductName = new GridBagConstraints();
 		gbc_lblProductName.insets = new Insets(0, 0, 0, 5);
 		gbc_lblProductName.gridx = 0;
@@ -49,16 +51,19 @@ public class ElementPanel extends JPanel {
 		gbc_panel.gridy = 1;
 		add(panelIngredients, gbc_panel);
 		
-		listener = new ObservableListener<>(panelIngredients, (c, t) -> {
+		nameListener = new ObservableListener<>(lblProductName, (c, t) -> c.setText(t.getName()));
+		childListener = new ObservableListener<>(panelIngredients, (c, t) -> {
 			c.removeAll();
 			element.getIngredients().stream().map(IngredientPanel::new).forEach(c::add);
 		});
-		listener.setObserved(element);
+		
+		nameListener.setObserved(element);
+		childListener.setObserved(element);
 	}
 	
 	@Override
 	public void removeNotify() {
 		super.removeNotify();
-		ObserverDispatch.unsubscribeAll(listener);
+		ObserverDispatch.unsubscribeAll(childListener);
 	}
 }

+ 3 - 3
src/test/java/org/leumasjaffe/recipe/controller/ReplaceChildrenControllerTest.java

@@ -22,7 +22,7 @@ class ReplaceChildrenControllerTest {
 	
 	@Mock Container parent;
 	Function<Void, Collection<Void>> getChildren = (v) -> Arrays.asList(null, null);
-	Function<Void, Component> makeView = (v) -> null;
+	Function<Void, Component> makeView = (v) -> mock(Component.class);
 	ReplaceChildrenController<Void, Void> controller;
 	
 	@BeforeEach
@@ -32,7 +32,7 @@ class ReplaceChildrenControllerTest {
 
 	@Test
 	void testDoesNotReplaceComponentsWhenNoSizeChange() {
-		doReturn(new Component[2]).when(parent).getComponents();
+		doReturn(new Component[] { mock(Component.class), mock(Component.class) }).when(parent).getComponents();
 		
 		controller.accept(parent, null);
 		
@@ -42,7 +42,7 @@ class ReplaceChildrenControllerTest {
 
 	@Test
 	void testReplacesComponents() {
-		doReturn(new Component[1]).when(parent).getComponents();
+		doReturn(new Component[] { mock(Component.class) }).when(parent).getComponents();
 		
 		controller.accept(parent, null);
 		

+ 8 - 8
src/test/java/org/leumasjaffe/recipe/model/DurationTest.java

@@ -75,20 +75,20 @@ class DurationTest {
 	
 	@Test
 	void testPerformsRoundingOnHigherDisplay() {
-		assertEquals("0 min", new Duration(MINUTES, 0, 29).toString());
-		assertEquals("1 min", new Duration(MINUTES, 0, 30).toString());
+		assertEquals("0 min", new Duration(MINUTES, 29, 29).toString());
+		assertEquals("1 min", new Duration(MINUTES, 30, 30).toString());
 	}
 	
 	@Test
 	void testCanDisplayHalfHours() {
-		assertEquals("1 hr", new Duration(HOURS, 0, 3600).toString());
-		assertEquals("1.5 hr", new Duration(HOURS, 0, 5400).toString());
+		assertEquals("1 hr", new Duration(HOURS, 3600, 3600).toString());
+		assertEquals("1.5 hr", new Duration(HOURS, 5400, 5400).toString());
 	}
 	
 	@ParameterizedTest
 	@ValueSource(ints= {900, 2699})
 	void testHalfHourDisplayIsUsedForRoundNear(int value) {
-		assertEquals("0.5 hr", new Duration(HOURS, 0, value).toString());
+		assertEquals("0.5 hr", new Duration(HOURS, value, value).toString());
 	}
 
 	@ParameterizedTest
@@ -103,9 +103,9 @@ class DurationTest {
 		assertEquals(new Duration(MINUTES, 600, 3600), new Duration("10 - 60 min"));
 		assertEquals(new Duration(SECONDS, 5, 10), new Duration("5 - 10 s"));
 
-		assertEquals(new Duration(HOURS, 0, 3600), new Duration("1 hr"));
-		assertEquals(new Duration(MINUTES, 0, 3600), new Duration("60 min"));
-		assertEquals(new Duration(SECONDS, 0, 10), new Duration("10 s"));
+		assertEquals(new Duration(HOURS, 3600, 3600), new Duration("1 hr"));
+		assertEquals(new Duration(MINUTES, 3600, 3600), new Duration("60 min"));
+		assertEquals(new Duration(SECONDS, 10, 10), new Duration("10 s"));
 	}
 	
 	@Test

+ 111 - 31
src/test/java/org/leumasjaffe/recipe/view/AutoGrowPanelTest.java

@@ -5,9 +5,9 @@ import static org.hamcrest.collection.IsArrayWithSize.arrayWithSize;
 import static org.mockito.Mockito.*;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.function.Consumer;
-import java.util.function.IntConsumer;
 
 import javax.swing.JTextField;
 import javax.swing.event.DocumentListener;
@@ -15,17 +15,15 @@ import javax.swing.text.BadLocationException;
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.platform.runner.JUnitPlatform;
-import org.junit.runner.RunWith;
-import org.leumasjaffe.recipe.view.AutoGrowPanel.DocumentListenable;
+import org.leumasjaffe.recipe.view.AutoGrowPanel.ChildComponent;
 import org.mockito.Mock;
+import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
-@RunWith(JUnitPlatform.class)
 class AutoGrowPanelTest extends SwingTestCase {
 	@SuppressWarnings("serial")
-	private static class MockComponent extends JTextField implements DocumentListenable {
+	private static class MockComponent extends JTextField implements ChildComponent {
 		public MockComponent() {
 		}
 
@@ -34,92 +32,174 @@ class AutoGrowPanelTest extends SwingTestCase {
 		}
 
 		@Override
-		public void addDocumentListener(DocumentListener dl) {
+		public void addGrowShrinkListener(DocumentListener dl) {
 			super.getDocument().addDocumentListener(dl);
 		}
 
 		@Override
-		public void removeDocumentListener(DocumentListener dl) {
+		public void removeGrowShrinkListener(DocumentListener dl) {
 			super.getDocument().removeDocumentListener(dl);
 		}
 	}
 	
-	@Mock Consumer<MockComponent> add;
-	@Mock IntConsumer remove;
-	List<MockComponent> components = new ArrayList<>();
+	@Mock Consumer<Boolean> callback;
+	List<MockComponent> internal = new ArrayList<>();
+	@Spy List<MockComponent> shared = new ArrayList<>();
 	
 	private MockComponent mocked() {
 		final MockComponent mock = spy(new MockComponent());
-		components.add(mock);
+		internal.add(mock);
 		return mock;
 	}
 	
-	private AutoGrowPanel create(MockComponent... mocks) {
-		return new AutoGrowPanel(m -> m, this::mocked, add, remove, mocks);
+	private AutoGrowPanel<MockComponent, MockComponent> create(MockComponent... mocks) {
+		shared.addAll(Arrays.asList(mocks));
+		final AutoGrowPanel<MockComponent, MockComponent> rval =
+				new AutoGrowPanel<>(this::mocked, m -> m);
+		
+		rval.setModel(shared);
+
+		return rval;
 	}
 	
 	@Test
 	void testAlwaysHasAtLeastOneComponent() {		
-		AutoGrowPanel panel = create();
+		AutoGrowPanel<MockComponent, MockComponent> panel =
+				new AutoGrowPanel<>(this::mocked, m -> m);
+		
+		assertThat(panel.getComponents(), arrayWithSize(1));
+	}
+	
+	@Test
+	void testInitialComponentAssignedFirstPosition() {		
+		@SuppressWarnings("unused")
+		AutoGrowPanel<MockComponent, MockComponent> panel =
+				new AutoGrowPanel<>(this::mocked, m -> m);
+		
+		verify(internal.get(0)).setListPosition(0);
+	}
+	
+	@Test
+	void testSetModelCallsListPositionEvenIfNoChange() {		
+		AutoGrowPanel<MockComponent, MockComponent> panel = create();
 		
 		assertThat(panel.getComponents(), arrayWithSize(1));
-		verify(add, never()).accept(any());
+		verify(internal.get(0), times(2)).setListPosition(0);
 	}
 
 	@Test
 	void testCreatesGivenNumberOfChildrenPlusOne() {
-		AutoGrowPanel panel = create(mocked(), mocked());
+		AutoGrowPanel<MockComponent, MockComponent> panel = create(mocked(), mocked());
 
 		assertThat(panel.getComponents(), arrayWithSize(3));
-		verify(add, never()).accept(any());
+		verify(shared, never()).add(any());
+	}
+
+	@Test
+	void testResetsListPositionAfterSettingModel() {
+		@SuppressWarnings("unused")
+		AutoGrowPanel<MockComponent, MockComponent> panel = create(mocked(), mocked());
+		
+		// Last element because my helper function pre-installs into the internal list
+		verify(internal.get(2)).setListPosition(0);
+		verify(internal.get(2)).setListPosition(2);
 	}
 
 	@Test
 	void testEnteringContentTriggersNewRow() {
-		AutoGrowPanel panel = create();
-		getTestFrame().add(panel);
+		AutoGrowPanel<MockComponent, MockComponent> panel = create();
 
-		components.get(0).setText("A");
+		internal.get(0).setText("A");
 		assertThat(panel.getComponents(), arrayWithSize(2));
 		
-		components.get(1).setText("B");
+		internal.get(1).setText("B");
 		assertThat(panel.getComponents(), arrayWithSize(3));
-		verify(add, times(2)).accept(any());
+		
+		verify(shared, times(2)).add(any());
+	}
+
+	@Test
+	void testEnteringContentAssignsCorrectListPosition() {
+		@SuppressWarnings("unused")
+		AutoGrowPanel<MockComponent, MockComponent> panel = create();
+
+		internal.get(0).setText("A");
+		
+		verify(internal.get(1)).setListPosition(1);
 	}
 
 	@Test
 	void testEnteringEmptyContentDoesNotTrigger() {
-		AutoGrowPanel panel = create();
+		AutoGrowPanel<MockComponent, MockComponent> panel = create();
 		getTestFrame().add(panel);
-		components.get(0).setText("");
+		internal.get(0).setText("");
 		
 		assertThat(panel.getComponents(), arrayWithSize(1));
-		verify(add, never()).accept(any());
-		verify(remove, never()).accept(anyInt());
+		verify(shared, never()).add(any());
+		verify(shared, never()).remove(anyInt());
 	}
 
 	@Test
 	void testEmptyingContentClearsPanel() {
 		final MockComponent mock = spy(new MockComponent("A"));
 		
-		AutoGrowPanel panel = create(mock);
+		AutoGrowPanel<MockComponent, MockComponent> panel = create(mock);
 		getTestFrame().add(panel);
 		mock.setText("");
 		
 		assertThat(panel.getComponents(), arrayWithSize(1));
-		verify(remove, times(1)).accept(0);
+		verify(shared, times(1)).remove(0);
 	}
 
 
+	@Test
+	void testEmptyingContentAdjustsAllPositions() {
+		final MockComponent mock = spy(new MockComponent("A"));
+		
+		@SuppressWarnings("unused")
+		AutoGrowPanel<MockComponent, MockComponent> panel = create(mock);
+		
+		mock.setText("");
+		
+		verify(internal.get(0), times(2)).setListPosition(0);
+	}
+
+	@Test
+	void testRemovingSomeContentDoesntClear() throws BadLocationException {
+		final MockComponent mock = spy(new MockComponent("AB"));
+		
+		AutoGrowPanel<MockComponent, MockComponent> panel = create(mock);
+		getTestFrame().add(panel);
+		mock.getDocument().remove(0, 1);
+		
+		assertThat(panel.getComponents(), arrayWithSize(2));
+		verify(shared, never()).remove(anyInt());
+	}
+
 	@Test
 	void testChangingTextDoesNotDeleteRow() throws BadLocationException {
 		final MockComponent mock = spy(new MockComponent("A"));
 		
-		AutoGrowPanel panel = create(mock);
+		AutoGrowPanel<MockComponent, MockComponent> panel = create(mock);
 		getTestFrame().add(panel);
 		mock.getDocument().insertString(0, "B", null);
 		
 		assertThat(panel.getComponents(), arrayWithSize(2));
-		verify(remove, never()).accept(anyInt());
+		verify(shared, never()).remove(anyInt());
 	}
+
+	@Test
+	void testCanInstallNotificationCallbackForAddsAndDeletes() {
+		AutoGrowPanel<MockComponent, MockComponent> panel = create();
+		panel.setModel(shared, callback);
+		getTestFrame().add(panel);
+		verify(callback, never()).accept(anyBoolean());
+
+		internal.get(0).setText("A");
+		verify(callback).accept(true);
+
+		internal.get(0).setText("");
+		verify(callback).accept(false);
+	}
+	
 }

+ 52 - 13
src/test/java/org/leumasjaffe/recipe/view/ElementPanelTest.java

@@ -1,46 +1,85 @@
 package org.leumasjaffe.recipe.view;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.Mockito.*;
 
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
+
+import javax.swing.JPanel;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.platform.runner.JUnitPlatform;
-import org.junit.runner.RunWith;
 import org.leumasjaffe.mock.MockObserverListener;
+import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Phase;
-import org.leumasjaffe.recipe.model.CollatedDuration;
 import org.leumasjaffe.recipe.model.Element;
+import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
-@RunWith(JUnitPlatform.class)
 class ElementPanelTest extends SwingTestCase {
 	
-	@Spy MockObserverListener listener;
-	final Phase stub = new Phase();
-	@Mock Element stuff;
-	ElementPanel panel;
+	List<Phase> phases;
+	Element stuff;
+	
+	@Mock ObservableListener<CollatedDurationPanel, Element> durationListener;
+	@Spy JPanel panelViewPort;
+	@InjectMocks ElementPanel panel = new ElementPanel();
 
 	@BeforeEach
 	void setUp() {
-		doReturn(Arrays.asList(stub)).when(stuff).getPhases();
-		doReturn(CollatedDuration.ZERO).when(stuff).getCollatedDuration();
-		panel = new ElementPanel(stuff);
+		phases = new ArrayList<>(Arrays.asList(new Phase()));
+		stuff = new Element();
+		stuff.setName("Curry");
+		stuff.setPhases(phases);
+		
+		panel.setModel(stuff);
+	}
+	
+	@Test
+	void testFilledOutWithContent() {
+		assertEquals("Curry", panel.getTxtName().getText());
+		verify(panelViewPort).add(any(PhasePanel.class));
+		verify(durationListener).setObserved(same(stuff));
+	}
+	
+	@Test
+	void testIsSubscribedToUpdates() {
+		stuff.setName("Sandwich");
+		
+		ObserverDispatch.notifySubscribers(stuff);
 		
+		assertEquals("Sandwich", panel.getTxtName().getText());
+	}
+	
+	@Test
+	void testViewUpdateToNameAltersModel() {
+		final MockObserverListener listener = spy(MockObserverListener.class);
 		listener.setObserved(stuff);
 		// setObserved() calls update
 		clearInvocations(listener);
+
+		panel.getTxtName().setText("Sandwich");
+		waitForSwing();
+
+		assertEquals("Sandwich", stuff.getName());
+		verify(listener).updateWasSignalled();
 	}
 
 	@Test
-	void testPropogatesSignalFromChildren() {
-		ObserverDispatch.notifySubscribers(stub);
+	void testPropogatesNotifications() {
+		final MockObserverListener listener = spy(MockObserverListener.class);
+		listener.setObserved(stuff);
+		// setObserved() calls update
+		clearInvocations(listener);
+
+		ObserverDispatch.notifySubscribers(phases.get(0));
 		verify(listener).updateWasSignalled();
 	}
 

+ 0 - 3
src/test/java/org/leumasjaffe/recipe/view/FileMenuTest.java

@@ -12,14 +12,11 @@ import javax.swing.JMenuBar;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.platform.runner.JUnitPlatform;
-import org.junit.runner.RunWith;
 import org.leumasjaffe.recipe.controller.FileController;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
-@RunWith(JUnitPlatform.class)
 class FileMenuTest extends SwingTestCase {
 	
 	@Mock FileController controller;

+ 0 - 3
src/test/java/org/leumasjaffe/recipe/view/IngredientPanelTest.java

@@ -6,8 +6,6 @@ import static org.mockito.Mockito.*;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.platform.runner.JUnitPlatform;
-import org.junit.runner.RunWith;
 import org.leumasjaffe.mock.MockObserverListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Amount;
@@ -16,7 +14,6 @@ import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
-@RunWith(JUnitPlatform.class)
 class IngredientPanelTest extends SwingTestCase {
 	@Spy MockObserverListener listener;
 	Ingredient stuff;

+ 86 - 0
src/test/java/org/leumasjaffe/recipe/view/PhasePanelIT.java

@@ -0,0 +1,86 @@
+package org.leumasjaffe.recipe.view;
+
+import static org.mockito.Mockito.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.leumasjaffe.mock.MockObserverListener;
+import org.leumasjaffe.observer.ObserverDispatch;
+import org.leumasjaffe.recipe.model.Phase;
+import org.leumasjaffe.recipe.model.Step;
+import org.mockito.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class PhasePanelIT extends SwingTestCase {
+	@Spy MockObserverListener listener;
+	List<Step> steps;
+	@Spy Phase stuff;
+	PhasePanel panel;
+
+	@BeforeEach
+	void setUp() {
+		Step step = new Step();
+		step.setInstruction("Dance");
+		steps = new ArrayList<>(Arrays.asList(step));
+		doReturn(steps).when(stuff).getCooking();
+				
+		panel = new PhasePanel(stuff);
+		listener.setObserved(stuff);
+		// setObserved invokes our callback.
+		clearInvocations(listener);
+	}
+
+	@Test
+	void testRecievesSignalWhenNewElementAdded() {
+		final StepPanel newIngredient =
+				(StepPanel) panel.getPanelSteps().getComponent(1);
+		
+		newIngredient.getTxtpnInstructions().setText("Bacon");
+		waitForSwing();
+		
+		verify(listener, times(1)).updateWasSignalled();
+	}
+
+	@Test
+	void testNewItemCanProduceUpdate() {
+		final StepPanel newIngredient =
+				(StepPanel) panel.getPanelSteps().getComponent(1);
+		newIngredient.getTxtpnInstructions().setText("Bacon");
+		waitForSwing();
+
+		ObserverDispatch.notifySubscribers(steps.get(1));
+		
+		verify(listener, times(2)).updateWasSignalled();
+	}
+
+	@Test
+	void testReceivesSignalWhenElementRemoved() {
+		final StepPanel oldIngredient =
+				(StepPanel) panel.getPanelSteps().getComponent(0);
+		oldIngredient.getTxtpnInstructions().setText("");
+		waitForSwing();
+
+		verify(listener, times(1)).updateWasSignalled();
+	}
+
+	@Test
+	void testIgnoresOldItemUpdates() {
+		final Step st = steps.get(0);
+		final StepPanel oldIngredient =
+				(StepPanel) panel.getPanelSteps().getComponent(0);
+		oldIngredient.getTxtpnInstructions().setText("");
+
+		waitForSwing();
+		clearInvocations(listener);
+
+		ObserverDispatch.notifySubscribers(st);
+		verify(listener, never()).updateWasSignalled();
+	}
+
+}

+ 0 - 3
src/test/java/org/leumasjaffe/recipe/view/PhasePanelTest.java

@@ -8,8 +8,6 @@ import java.util.Optional;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.platform.runner.JUnitPlatform;
-import org.junit.runner.RunWith;
 import org.leumasjaffe.mock.MockObserverListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Duration;
@@ -22,7 +20,6 @@ import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
-@RunWith(JUnitPlatform.class)
 class PhasePanelTest extends SwingTestCase {
 	
 	@Spy MockObserverListener listener;

+ 9 - 4
src/test/java/org/leumasjaffe/recipe/view/PreparationPanelTest.java

@@ -2,6 +2,8 @@ package org.leumasjaffe.recipe.view;
 
 import static org.mockito.Mockito.*;
 
+import java.util.Optional;
+
 import javax.swing.JFormattedTextField;
 
 import org.junit.jupiter.api.BeforeEach;
@@ -11,6 +13,7 @@ import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.controller.ReplaceChildrenController;
 import org.leumasjaffe.recipe.model.Ingredient;
+import org.leumasjaffe.recipe.model.Phase;
 import org.leumasjaffe.recipe.model.Preparation;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
@@ -19,25 +22,27 @@ import org.mockito.junit.jupiter.MockitoExtension;
 @ExtendWith(MockitoExtension.class)
 class PreparationPanelTest extends SwingTestCase {
 	
-	@Mock ObservableListener<JFormattedTextField, Preparation> durationListener;
+	@Mock ObservableListener<JFormattedTextField, Preparation> durationController;
 	@Mock ReplaceChildrenController<Preparation, Ingredient> controller;
 	Preparation stuff = new Preparation();
+	@Mock Phase parent;
 	@InjectMocks PreparationPanel panel = new PreparationPanel();
 	
 	@BeforeEach
 	void setUp() {
-		panel.setModel(stuff);
+		doReturn(Optional.of(stuff)).when(parent).getPreparation();
+		panel.setModel(parent);
 	}
 
 	@Test
 	void testHasContent() {
-		verify(durationListener, times(1)).setObserved(same(stuff));
+		verify(durationController, times(1)).setObserved(same(stuff));
 		verify(controller, times(1)).accept(any(), same(stuff));
 	}
 
 	@Test
 	void testUpdatesNumberOfChildrenWhenNotified() {
-		ObserverDispatch.notifySubscribers(stuff);		
+		ObserverDispatch.notifySubscribers(parent);		
 
 		verify(controller, times(2)).accept(any(), same(stuff));
 	}

+ 93 - 0
src/test/java/org/leumasjaffe/recipe/view/RecipeCardPanelIT.java

@@ -0,0 +1,93 @@
+package org.leumasjaffe.recipe.view;
+
+import static org.mockito.Mockito.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.leumasjaffe.mock.MockObserverListener;
+import org.leumasjaffe.observer.ObserverDispatch;
+import org.leumasjaffe.recipe.model.Element;
+import org.leumasjaffe.recipe.model.RecipeCard;
+import org.mockito.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class RecipeCardPanelIT extends SwingTestCase {
+	@Spy MockObserverListener listener;
+
+	List<Element> elements;
+	RecipeCard stuff;
+	
+	RecipeCardPanel panel = new RecipeCardPanel();
+
+	@BeforeEach
+	void setUp() {
+		elements = new ArrayList<>(Arrays.asList(new Element()));
+		elements.get(0).setName("Stuff");
+		stuff = new RecipeCard();
+		stuff.setElements(elements);
+				
+		panel.setModel(stuff);
+		
+		listener.setObserved(stuff);
+		// setObserved invokes our callback.
+		clearInvocations(listener);
+	}
+	
+	private AutoGrowPanel<ElementPanel, Element> getPanelIngredients() {
+		return panel.getPanelElements();
+	}
+
+	@Test
+	void testRecievesSignalWhenNewElementAdded() {
+		final ElementPanel newElement =
+				(ElementPanel) getPanelIngredients().getComponent(1);
+		
+		newElement.getTxtName().setText("Bacon");
+		waitForSwing();
+		
+		verify(listener, times(1)).updateWasSignalled();
+	}
+
+	@Test
+	void testNewItemCanProduceUpdate() {
+		final ElementPanel newElement =
+				(ElementPanel) getPanelIngredients().getComponent(1);
+		newElement.getTxtName().setText("Bacon");
+		waitForSwing();
+
+		ObserverDispatch.notifySubscribers(elements.get(1));
+		
+		verify(listener, times(2)).updateWasSignalled();
+	}
+
+	@Test
+	void testReceivesSignalWhenElementRemoved() {
+		final ElementPanel oldElement =
+				(ElementPanel) getPanelIngredients().getComponent(0);
+		oldElement.getTxtName().setText("");
+		waitForSwing();
+
+		verify(listener, times(1)).updateWasSignalled();
+	}
+
+	@Test
+	void testIgnoresOldItemUpdates() {
+		final Element elem = elements.get(0);
+		final ElementPanel oldElement =
+				(ElementPanel) getPanelIngredients().getComponent(0);
+		oldElement.getTxtName().setText("");
+
+		waitForSwing();
+		clearInvocations(listener);
+
+		ObserverDispatch.notifySubscribers(elem);
+		verify(listener, never()).updateWasSignalled();
+	}
+
+}

+ 23 - 28
src/test/java/org/leumasjaffe/recipe/view/RecipeCardPanelTest.java

@@ -2,10 +2,9 @@ package org.leumasjaffe.recipe.view;
 
 import static org.mockito.Mockito.*;
 
-import java.awt.Component;
+import java.util.ArrayList;
 import java.util.Arrays;
-
-import javax.swing.JPanel;
+import java.util.List;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -23,45 +22,41 @@ import org.mockito.junit.jupiter.MockitoExtension;
 @ExtendWith(MockitoExtension.class)
 class RecipeCardPanelTest {
 	
-	@Spy MockObserverListener listener;
-	
+	List<Element> elements;
+	RecipeCard stuff;
+
 	@Mock SummaryPanel summaryPanel;
-	@Mock JPanel rightPanel;
-	@Spy RecipeCard card;
+	@Spy AutoGrowPanel<ElementPanel, Element> panelElements =
+			new AutoGrowPanel<>(Element::new, ElementPanel::new);
+	
 	@InjectMocks RecipeCardPanel panel = new RecipeCardPanel();
 	
 	@BeforeEach
 	void setUp() {
-		listener.setObserved(card);
-		clearInvocations(listener);
-	}
+		elements = new ArrayList<>(Arrays.asList(new Element()));
+		stuff = new RecipeCard();
+		stuff.setTitle("Peanut Curry");
+		stuff.setElements(elements);
+		stuff.setDescription("I am delicious");
 
-	@Test
-	void testModelIsPropogated() {
-		panel.setModel(card);
-		verify(summaryPanel).setModel(same(card));
-		verify(rightPanel, never()).add(any(Component.class));
+		panel.setModel(stuff);
 	}
 
 	@Test
-	void testModelWithChildrenAddsElements() {
-		final Element element = new Element();
-		doReturn(Arrays.asList(element)).when(card).getElements();
-
-		panel.setModel(card);
-		verify(rightPanel, times(1)).add(any(Component.class));
+	void testFilledOutWithContent() {
+		verify(summaryPanel).setModel(same(stuff));
+		verify(panelElements).setModel(any(), any());
 	}
 
 	@Test
-	void testToChildElementsIsPropogated() {
-		final Element element = new Element();
-		doReturn(Arrays.asList(element)).when(card).getElements();
+	void testPropogatesNotifications() {
+		final MockObserverListener listener = spy(MockObserverListener.class);
+		listener.setObserved(stuff);
+		// setObserved invokes our callback.
+		clearInvocations(listener);
 
-		panel.setModel(card);
+		ObserverDispatch.notifySubscribers(elements.get(0));
 		verify(listener).updateWasSignalled();
-		
-		ObserverDispatch.notifySubscribers(element);
-		verify(listener, times(2)).updateWasSignalled();
 	}
 
 }

+ 2 - 5
src/test/java/org/leumasjaffe/recipe/view/RestPanelTest.java

@@ -8,8 +8,6 @@ import javax.swing.JLabel;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.platform.runner.JUnitPlatform;
-import org.junit.runner.RunWith;
 import org.leumasjaffe.mock.MockObserverListener;
 import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.recipe.model.Duration;
@@ -20,7 +18,6 @@ import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
-@RunWith(JUnitPlatform.class)
 class RestPanelTest extends SwingTestCase {
 	
 	@Spy MockObserverListener listener;
@@ -28,7 +25,7 @@ class RestPanelTest extends SwingTestCase {
 	Rest stuff;
 	
 	@Spy JLabel lblLocation;
-	@Mock ObservableListener<JFormattedTextField, Rest> durationListener;
+	@Mock ObservableListener<JFormattedTextField, Rest> durationController;
 	@InjectMocks RestPanel panel = new RestPanel();
 	
 	@BeforeEach
@@ -47,6 +44,6 @@ class RestPanelTest extends SwingTestCase {
 
 	@Test
 	void testDurationIsListeningToModel() {
-		verify(durationListener).setObserved(same(stuff));
+		verify(durationController).setObserved(same(stuff));
 	}
 }

+ 16 - 15
src/test/java/org/leumasjaffe/recipe/view/StepPanelIT.java

@@ -9,43 +9,44 @@ import java.util.List;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.platform.runner.JUnitPlatform;
-import org.junit.runner.RunWith;
 import org.leumasjaffe.mock.MockObserverListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Amount;
-import org.leumasjaffe.recipe.model.Duration;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.model.Step;
 import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
-@RunWith(JUnitPlatform.class)
 class StepPanelIT extends SwingTestCase {
 	@Spy MockObserverListener listener;
+
 	List<Ingredient> ingredients;
-	@Spy Step stuff;
-	StepPanel panel;
+	Step stuff;
+	
+	StepPanel panel = new StepPanel();
 
 	@BeforeEach
 	void setUp() {
-		Duration dur = new Duration(Duration.Display.SECONDS, 0, 30);
 		ingredients = new ArrayList<>(Arrays.asList(new Ingredient("Onion", "Sliced", new Amount("100 g"))));
-		doReturn(dur).when(stuff).getDuration();
-		doReturn("These are test instructions").when(stuff).getInstruction();
-		doReturn(ingredients).when(stuff).getIngredients();
+
+		stuff = new Step();
+		stuff.setIngredients(ingredients);
 				
-		panel = new StepPanel(0, stuff);
+		panel.setModel(stuff);
 		listener.setObserved(stuff);
 		// setObserved invokes our callback.
 		clearInvocations(listener);
 	}
+	
+	private AutoGrowPanel<IngredientPanel, Ingredient> getPanelIngredients() {
+		return panel.getPanelIngredients();
+	}
 
 	@Test
 	void testRecievesSignalWhenNewElementAdded() {
 		final IngredientPanel newIngredient =
-				(IngredientPanel) panel.getPanelIngredients().getComponent(1);
+				(IngredientPanel) getPanelIngredients().getComponent(1);
 		
 		newIngredient.getTxtName().setText("Bacon");
 		waitForSwing();
@@ -56,7 +57,7 @@ class StepPanelIT extends SwingTestCase {
 	@Test
 	void testNewItemCanProduceUpdate() {
 		final IngredientPanel newIngredient =
-				(IngredientPanel) panel.getPanelIngredients().getComponent(1);
+				(IngredientPanel) getPanelIngredients().getComponent(1);
 		newIngredient.getTxtName().setText("Bacon");
 		waitForSwing();
 
@@ -68,7 +69,7 @@ class StepPanelIT extends SwingTestCase {
 	@Test
 	void testReceivesSignalWhenElementRemoved() {
 		final IngredientPanel oldIngredient =
-				(IngredientPanel) panel.getPanelIngredients().getComponent(0);
+				(IngredientPanel) getPanelIngredients().getComponent(0);
 		oldIngredient.getTxtName().setText("");
 		waitForSwing();
 
@@ -79,7 +80,7 @@ class StepPanelIT extends SwingTestCase {
 	void testIgnoresOldItemUpdates() {
 		final Ingredient ing = ingredients.get(0);
 		final IngredientPanel oldIngredient =
-				(IngredientPanel) panel.getPanelIngredients().getComponent(0);
+				(IngredientPanel) getPanelIngredients().getComponent(0);
 		oldIngredient.getTxtName().setText("");
 
 		waitForSwing();

+ 38 - 17
src/test/java/org/leumasjaffe/recipe/view/StepPanelTest.java

@@ -1,53 +1,74 @@
 package org.leumasjaffe.recipe.view;
 
 import static org.junit.jupiter.api.Assertions.*;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.collection.IsArrayWithSize.arrayWithSize;
-import static org.hamcrest.number.OrderingComparison.greaterThanOrEqualTo;
 import static org.mockito.Mockito.*;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
+import javax.swing.JLabel;
+
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.platform.runner.JUnitPlatform;
-import org.junit.runner.RunWith;
 import org.leumasjaffe.mock.MockObserverListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Amount;
 import org.leumasjaffe.recipe.model.Duration;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.model.Step;
-import org.mockito.Mock;
+import org.mockito.InjectMocks;
+import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
-@RunWith(JUnitPlatform.class)
 class StepPanelTest extends SwingTestCase {
 	List<Ingredient> ingredients;
-	@Mock Step stuff;
-	StepPanel panel;
+	Step stuff;
+	
+	@Spy JLabel lblIndex;
+	@Spy AutoGrowPanel<IngredientPanel, Ingredient> panelIngredients =
+			new AutoGrowPanel<>(Ingredient::new, IngredientPanel::new);
+	@InjectMocks StepPanel panel = new StepPanel();
 
 	@BeforeEach
 	void setUp() {
-		Duration dur = new Duration(Duration.Display.SECONDS, 0, 30);
 		ingredients = new ArrayList<>(Arrays.asList(new Ingredient("Onion", "Sliced", new Amount("100 g"))));
-		doReturn(dur).when(stuff).getDuration();
-		doReturn("These are test instructions").when(stuff).getInstruction();
-		doReturn(ingredients).when(stuff).getIngredients();
+		stuff = new Step();
+		stuff.setDuration(new Duration("30 s"));
+		stuff.setIngredients(ingredients);
+		stuff.setInstruction("These are test instructions");
 		
-		panel = new StepPanel(0, stuff);
+		panel.setModel(stuff);
 	}
 
 	@Test
 	void testFilledOutWithContent() {
-		assertEquals("Step 1", panel.getLblIndex().getText());
 		assertEquals("These are test instructions", panel.getTxtpnInstructions().getText());
-		assertThat(panel.getPanelIngredients().getComponents(),
-				arrayWithSize(greaterThanOrEqualTo(1)));
+		verify(panelIngredients).setModel(same(ingredients), any());
+	}
+	
+	@Test
+	void testSetListPositionUpdatesIndex() {
+		panel.setListPosition(1);
+		verify(lblIndex).setText("Step 2");
+	}
+	
+	@Test
+	void testUpdatesContentOnNotification() {
+		stuff.setInstruction("New instructions");
+		ObserverDispatch.notifySubscribers(stuff);
+		
+		assertEquals("New instructions", panel.getTxtpnInstructions().getText());
+	}
+	
+	@Test
+	void testViewUpdatesAltersModel() {
+		panel.getTxtpnInstructions().setText("New instructions");
+		waitForSwing();
+		
+		assertEquals("New instructions", stuff.getInstruction());
 	}
 
 	@Test

+ 0 - 3
src/test/java/org/leumasjaffe/recipe/view/summary/ElementPanelTest.java

@@ -10,8 +10,6 @@ import java.util.Arrays;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.platform.runner.JUnitPlatform;
-import org.junit.runner.RunWith;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.model.Amount;
 import org.leumasjaffe.recipe.model.Ingredient;
@@ -21,7 +19,6 @@ import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
-@RunWith(JUnitPlatform.class)
 class ElementPanelTest extends SwingTestCase {
 	
 	@Spy Element stuff;

+ 0 - 3
src/test/java/org/leumasjaffe/recipe/view/summary/SummaryPanelTest.java

@@ -4,8 +4,6 @@ import static org.mockito.Mockito.*;
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.platform.runner.JUnitPlatform;
-import org.junit.runner.RunWith;
 import org.leumasjaffe.observer.ObserverDispatch;
 import org.leumasjaffe.recipe.controller.ReplaceChildrenController;
 import org.leumasjaffe.recipe.model.Element;
@@ -15,7 +13,6 @@ import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
-@RunWith(JUnitPlatform.class)
 class SummaryPanelTest {
 	
 	RecipeCard card = new RecipeCard();