Bladeren bron

Merge branch 'refactor/controllers'

* refactor/controllers:
  Test RecipeCardPanel.
  Clean up IngredientPanel.
  Fix warning in Preparation
  Alter PreparationPanelTest to properly use mocking/injection for tests.
  Refactor PreparationPanel to use ReplaceChildController.
  Perform some GUI cleanup to match the desired state.
  Fix bug in AutoGrowPanel where children were not pruned properly.
  Add integration test for StepPanel. Fix a few bugs in tests and remove redundant notify().
  Pull out the act of batch-adding children into an object as ReplaceChildrenController. Add test code for ReplaceChildrenController and SummaryPanel.
Sam Jaffe 5 jaren geleden
bovenliggende
commit
8c8747832d
23 gewijzigde bestanden met toevoegingen van 526 en 111 verwijderingen
  1. 28 0
      src/main/lombok/org/leumasjaffe/recipe/controller/ReplaceChildrenController.java
  2. 5 2
      src/main/lombok/org/leumasjaffe/recipe/model/Preparation.java
  3. 1 1
      src/main/lombok/org/leumasjaffe/recipe/model/RecipeCard.java
  4. 1 0
      src/main/lombok/org/leumasjaffe/recipe/view/AutoGrowPanel.java
  5. 5 2
      src/main/lombok/org/leumasjaffe/recipe/view/DurationPanel.java
  6. 9 0
      src/main/lombok/org/leumasjaffe/recipe/view/ElementPanel.java
  7. 2 0
      src/main/lombok/org/leumasjaffe/recipe/view/ImagePanel.java
  8. 10 4
      src/main/lombok/org/leumasjaffe/recipe/view/IngredientPanel.java
  9. 3 1
      src/main/lombok/org/leumasjaffe/recipe/view/IngredientPreparationPanel.java
  10. 34 13
      src/main/lombok/org/leumasjaffe/recipe/view/PreparationPanel.java
  11. 32 10
      src/main/lombok/org/leumasjaffe/recipe/view/RecipeCardPanel.java
  12. 1 2
      src/main/lombok/org/leumasjaffe/recipe/view/StepPanel.java
  13. 13 4
      src/main/lombok/org/leumasjaffe/recipe/view/summary/IngredientPanel.java
  14. 40 21
      src/main/lombok/org/leumasjaffe/recipe/view/summary/SummaryPanel.java
  15. 0 11
      src/test/java/org/leumasjaffe/recipe/TestSuite.java
  16. 52 0
      src/test/java/org/leumasjaffe/recipe/controller/ReplaceChildrenControllerTest.java
  17. 51 5
      src/test/java/org/leumasjaffe/recipe/view/IngredientPanelTest.java
  18. 29 24
      src/test/java/org/leumasjaffe/recipe/view/PreparationPanelTest.java
  19. 67 0
      src/test/java/org/leumasjaffe/recipe/view/RecipeCardPanelTest.java
  20. 92 0
      src/test/java/org/leumasjaffe/recipe/view/StepPanelIT.java
  21. 18 9
      src/test/java/org/leumasjaffe/recipe/view/StepPanelTest.java
  22. 2 2
      src/test/java/org/leumasjaffe/recipe/view/summary/ElementPanelTest.java
  23. 31 0
      src/test/java/org/leumasjaffe/recipe/view/summary/SummaryPanelTest.java

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

@@ -0,0 +1,28 @@
+package org.leumasjaffe.recipe.controller;
+
+import java.awt.Component;
+import java.awt.Container;
+import java.util.Collection;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@RequiredArgsConstructor
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class ReplaceChildrenController<T, V> implements BiConsumer<Container, T> {
+	Function<T, ? extends Collection<V>> getChildren;
+	Function<? super V, ? extends Component> makeView;
+	
+	@Override
+	public void accept(final Container parent, final T model) {
+		final Collection<V> children = getChildren.apply(model);
+		if (parent.getComponents().length == children.size()) {
+			return;
+		}
+		parent.removeAll();
+		children.stream().map(makeView).forEach(parent::add);
+	}
+}

+ 5 - 2
src/main/lombok/org/leumasjaffe/recipe/model/Preparation.java

@@ -6,14 +6,17 @@ import java.util.List;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
+import org.leumasjaffe.observer.Observable;
+
 import com.fasterxml.jackson.annotation.JsonIgnore;
 
 import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 
-@Data @AllArgsConstructor @NoArgsConstructor
-public class Preparation implements RecipeComponent {
+@Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode(callSuper=false)
+public class Preparation extends Observable.Instance implements RecipeComponent {
 	Duration duration;
 	@JsonIgnore Supplier<Collection<Ingredient>> producer = Collections::emptyList;
 	

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

@@ -23,7 +23,7 @@ public class RecipeCard extends Observable.Instance implements CompoundRecipeCom
 	
 	@Override
 	public Stream<Element> getComponents() {
-		return elements.stream();
+		return getElements().stream();
 	}
 	
 	@Override

+ 1 - 0
src/main/lombok/org/leumasjaffe/recipe/view/AutoGrowPanel.java

@@ -33,6 +33,7 @@ public class AutoGrowPanel extends JPanel {
 			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);

+ 5 - 2
src/main/lombok/org/leumasjaffe/recipe/view/DurationPanel.java

@@ -12,7 +12,7 @@ import org.leumasjaffe.recipe.model.Duration;
 @SuppressWarnings("serial")
 public class DurationPanel extends JPanel {
 	private JLabel lblTime;
-	public DurationPanel(String name, Duration duration) {
+	public DurationPanel(String name) {
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0};
@@ -32,7 +32,10 @@ public class DurationPanel extends JPanel {
 		gbc_lblTime.gridx = 1;
 		gbc_lblTime.gridy = 0;
 		add(lblTime, gbc_lblTime);
-		
+	}
+
+	public DurationPanel(String name, Duration duration) {
+		this(name);
 		setModel(duration);
 	}
 

+ 9 - 0
src/main/lombok/org/leumasjaffe/recipe/view/ElementPanel.java

@@ -14,11 +14,15 @@ import lombok.experimental.FieldDefaults;
 import org.jdesktop.swingx.VerticalLayout;
 
 import javax.swing.JSeparator;
+import javax.swing.ScrollPaneConstants;
+
 import java.awt.GridBagLayout;
 import javax.swing.JLabel;
 import java.awt.GridBagConstraints;
 import java.awt.Insets;
 import java.awt.Component;
+import java.awt.Dimension;
+
 import javax.swing.Box;
 
 @SuppressWarnings("serial")
@@ -28,6 +32,11 @@ public class ElementPanel extends JScrollPane {
 	ForwardingObservableListener<Element> listener = new ForwardingObservableListener<>();
 
 	public ElementPanel(Element element) {
+		setPreferredSize(new Dimension(500, 450));
+
+		setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
+		setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+
 		JPanel panelColumnHeader = new JPanel();
 		setColumnHeaderView(panelColumnHeader);
 		GridBagLayout gbl_panelColumnHeader = new GridBagLayout();

+ 2 - 0
src/main/lombok/org/leumasjaffe/recipe/view/ImagePanel.java

@@ -19,6 +19,7 @@ public class ImagePanel extends JPanel {
 
     public ImagePanel() {
     	image = getPlaceholder();
+    	setMinimumSize(new Dimension(200, 200));
     	setPreferredSize(new Dimension(200, 200));
     }
     
@@ -28,6 +29,7 @@ public class ImagePanel extends JPanel {
        } catch (IOException ex) {
           image = getPlaceholder();
        }
+       setMinimumSize(new Dimension(200, 200));
        setPreferredSize(new Dimension(200, 200));
    }
 

+ 10 - 4
src/main/lombok/org/leumasjaffe/recipe/view/IngredientPanel.java

@@ -26,7 +26,8 @@ import javax.swing.JLabel;
 @SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentListenable {
-	ObservableController<JTextField, Ingredient> controller;
+	ObservableController<JTextField, Ingredient> nameController;
+	ObservableController<JTextField, Ingredient> preparationController;
 	@Getter(AccessLevel.PACKAGE) JTextField txtName;
 	@Getter(AccessLevel.PACKAGE) JFormattedTextField txtAmount;
 	@Getter(AccessLevel.PACKAGE) JTextField txtUnit;
@@ -95,10 +96,15 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentLis
 		
 		// I technically don't need to listen here as of this change,
 		// but if I ever restore support for it, it will be convenient.
-		controller = new ObservableController<>(txtName,
+		nameController = new ObservableController<>(txtName,
 				Ingredient::getName, Ingredient::setName,
 				JTextField::setText);
-		controller.setObserved(ingredient);
+		preparationController = new ObservableController<>(txtPreparation,
+				Ingredient::getPreparation, Ingredient::setPreparation,
+				JTextField::setText);
+
+		nameController.setObserved(ingredient);
+		preparationController.setObserved(ingredient);
 	}
 
 	@Override
@@ -114,7 +120,7 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentLis
 	@Override
 	public void removeNotify() {
 		super.removeNotify();
-		ObserverDispatch.unsubscribeAll(controller);
+		ObserverDispatch.unsubscribeAll(nameController);
 	}
 
 }

+ 3 - 1
src/main/lombok/org/leumasjaffe/recipe/view/IngredientPreparationPanel.java

@@ -98,8 +98,10 @@ public class IngredientPreparationPanel extends JPanel {
 		txtPreparation.setColumns(10);
 		
 		listener = new ObservableListener<>(this, (c, t) -> {
-			if (txtName.getText().equals(t.getName())) return;
 			txtName.setText(t.getName());
+			txtAmount.setValue(t.getAmount().getValue());
+			txtUnit.setText(t.getAmount().unitName());
+			txtPreparation.setText(t.getPreparation());
 		});
 		listener.setObserved(ingredient);
 	}

+ 34 - 13
src/main/lombok/org/leumasjaffe/recipe/view/PreparationPanel.java

@@ -3,10 +3,13 @@ package org.leumasjaffe.recipe.view;
 import javax.swing.JPanel;
 
 import org.jdesktop.swingx.VerticalLayout;
+import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.recipe.controller.ReplaceChildrenController;
+import org.leumasjaffe.recipe.model.Duration;
+import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.model.Preparation;
 
 import lombok.AccessLevel;
-import lombok.Getter;
 import lombok.experimental.FieldDefaults;
 
 import java.awt.GridBagLayout;
@@ -19,12 +22,17 @@ import java.awt.Component;
 import javax.swing.Box;
 
 @SuppressWarnings("serial")
-@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+@FieldDefaults(level=AccessLevel.PRIVATE)
 public class PreparationPanel extends JPanel {
-	@Getter(AccessLevel.PACKAGE) JLabel lblDuration;
-	@Getter(AccessLevel.PACKAGE) JPanel panelIngredients;
+	ReplaceChildrenController<Preparation, Ingredient> controller;
+	ObservableListener<JPanel, Preparation> childListener;
+	
+	DurationPanel panelDuration;
+		
+	public PreparationPanel() {
+		controller = new ReplaceChildrenController<>(Preparation::getIngredients,
+				IngredientPreparationPanel::new);
 		
-	public PreparationPanel(Preparation step) {
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0};
@@ -61,14 +69,14 @@ public class PreparationPanel extends JPanel {
 		gbc_horizontalGlue.gridy = 0;
 		panelLeft.add(horizontalGlue, gbc_horizontalGlue);
 		
-		lblDuration = new JLabel("Requires: " + step.getDuration().toString());
-		GridBagConstraints gbc_lblDuration = new GridBagConstraints();
-		gbc_lblDuration.insets = new Insets(0, 0, 5, 0);
-		gbc_lblDuration.gridx = 2;
-		gbc_lblDuration.gridy = 0;
-		panelLeft.add(lblDuration, gbc_lblDuration);
+		panelDuration = new DurationPanel("Requires", Duration.ZERO);
+		GridBagConstraints gbc_panelDuration = new GridBagConstraints();
+		gbc_panelDuration.insets = new Insets(0, 0, 5, 0);
+		gbc_panelDuration.gridx = 2;
+		gbc_panelDuration.gridy = 0;
+		panelLeft.add(panelDuration, gbc_panelDuration);
 		
-		panelIngredients = new JPanel();
+		JPanel panelIngredients = new JPanel();
 		panelIngredients.setLayout(new VerticalLayout(5));
 		GridBagConstraints gbc_panelIngredients = new GridBagConstraints();
 		gbc_panelIngredients.gridwidth = 3;
@@ -77,7 +85,20 @@ public class PreparationPanel extends JPanel {
 		gbc_panelIngredients.gridx = 0;
 		gbc_panelIngredients.gridy = 1;
 		panelLeft.add(panelIngredients, gbc_panelIngredients);
-		step.getIngredientsAsStream().map(IngredientPreparationPanel::new).forEach(panelIngredients::add);
+		
+		// This indirection allows for testing of controller
+		childListener = new ObservableListener<>(panelIngredients,
+				(c, v) -> controller.accept(c, v));
+	}
+	
+	public PreparationPanel(final Preparation preparation) {
+		this();
+		setModel(preparation);
+	}
+	
+	public void setModel(final Preparation preparation) {
+		panelDuration.setModel(preparation.getDuration());
+		childListener.setObserved(preparation);
 	}
 
 }

+ 32 - 10
src/main/lombok/org/leumasjaffe/recipe/view/RecipeCardPanel.java

@@ -12,7 +12,11 @@ import org.leumasjaffe.recipe.view.summary.SummaryPanel;
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
 
+import java.awt.Dimension;
+
 import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.ScrollPaneConstants;
 
 @SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE)
@@ -20,19 +24,37 @@ public class RecipeCardPanel extends JSplitPane {
 	ObservableListener<RecipeCardPanel, RecipeCard> updateUI;
 	ForwardingObservableListener<RecipeCard> listener;
 	
-	public RecipeCardPanel(final RecipeCard card) {
-		final JPanel rightPanel = new JPanel();
+	SummaryPanel summaryPanel;
+	JPanel rightPanel;
+	
+	public RecipeCardPanel() {
+		setPreferredSize(new Dimension(1050, 600));
+
+		summaryPanel = new SummaryPanel();
+		rightPanel = new JPanel();
 		rightPanel.setLayout(new VerticalLayout(5));
-		setRightComponent(rightPanel);
-		setLeftComponent(new SummaryPanel(card));
-		
-		card.getComponents().map(ElementPanel::new).forEach(rightPanel::add);
-		
+
+		final JScrollPane scrollPane = new JScrollPane(rightPanel);
+		scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
+		scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+		setRightComponent(scrollPane);
+		setLeftComponent(summaryPanel);
+						
 		listener = new ForwardingObservableListener<>();
-		listener.setObserved(card, card.getElements().toArray(new Element[0]));
-		
 		updateUI = new ObservableListener<>(this, (c, t) -> validate());
-		updateUI.setObserved(card);
+	}
+	
+	public RecipeCardPanel(final RecipeCard card) {
+		this();
+		setModel(card);
+	}
+	
+	public void setModel(final RecipeCard card) {
+		summaryPanel.setModel(card);
+		card.getComponents().map(ElementPanel::new).forEach(rightPanel::add);
+
+		listener.setObserved(card, card.getElements());
+		updateUI.setObserved(card);		
 	}
 
 }

+ 1 - 2
src/main/lombok/org/leumasjaffe/recipe/view/StepPanel.java

@@ -29,7 +29,7 @@ import java.awt.Dimension;
 public class StepPanel extends JPanel implements AutoGrowPanel.DocumentListenable {
 	@Getter(AccessLevel.PACKAGE) JLabel lblIndex;
 	@Getter(AccessLevel.PACKAGE) JTextPane txtpnInstructions;
-	@Getter(AccessLevel.PACKAGE) JPanel panelIngredients;
+	@Getter(AccessLevel.PACKAGE) AutoGrowPanel panelIngredients;
 	ForwardingObservableListener<Step> listener = new ForwardingObservableListener<>();
 		
 	public StepPanel(int zeroIndex, Step step) {
@@ -81,7 +81,6 @@ public class StepPanel extends JPanel implements AutoGrowPanel.DocumentListenabl
 				Ingredient::new, ing -> {
 					ingredients.add(ing);
 					listener.setObserved(step, ingredients);
-					ObserverDispatch.notifySubscribers(step);
 				}, i -> {
 					ingredients.remove(i);
 					listener.setObserved(step, ingredients);

+ 13 - 4
src/main/lombok/org/leumasjaffe/recipe/view/summary/IngredientPanel.java

@@ -27,7 +27,7 @@ public class IngredientPanel extends JPanel {
 	@Getter(AccessLevel.PACKAGE) JFormattedTextField txtAmount;
 	@Getter(AccessLevel.PACKAGE) JTextField txtUnit;
 	
-	public IngredientPanel(final Ingredient ingredient) {
+	public IngredientPanel() {
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0};
@@ -43,7 +43,7 @@ public class IngredientPanel extends JPanel {
 		gbc_label.gridy = 0;
 		add(label, gbc_label);
 		
-		txtName = new JTextField(ingredient.getName());
+		txtName = new JTextField();
 		txtName.setEditable(false);
 		txtName.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		GridBagConstraints gbc_txtName = new GridBagConstraints();
@@ -59,7 +59,6 @@ public class IngredientPanel extends JPanel {
 		fmtDone.setCommitsOnValidEdit(true);
 		txtAmount = new JFormattedTextField(fmtDone);
 		txtAmount.setEditable(false);
-		txtAmount.setValue(ingredient.getAmount().getValue());
 		txtAmount.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		GridBagConstraints gbc_txtAmount = new GridBagConstraints();
 		gbc_txtAmount.fill = GridBagConstraints.HORIZONTAL;
@@ -69,7 +68,7 @@ public class IngredientPanel extends JPanel {
 		add(txtAmount, gbc_txtAmount);
 		txtAmount.setColumns(4);
 		
-		txtUnit = new JTextField(ingredient.getAmount().unitName());
+		txtUnit = new JTextField();
 		txtUnit.setEditable(false);
 		txtUnit.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
 		GridBagConstraints gbc_txtUnit = new GridBagConstraints();
@@ -81,4 +80,14 @@ public class IngredientPanel extends JPanel {
 		txtUnit.setColumns(6);
 	}
 
+	public IngredientPanel(final Ingredient ingredient) {
+		this();
+		setModel(ingredient);
+	}
+	
+	public void setModel(final Ingredient ingredient) {
+		txtName.setText(ingredient.getName());
+		txtAmount.setValue(ingredient.getAmount().getValue());
+		txtUnit.setText(ingredient.getAmount().unitName());
+	}
 }

+ 40 - 21
src/main/lombok/org/leumasjaffe/recipe/view/summary/SummaryPanel.java

@@ -11,6 +11,8 @@ import javax.swing.JTextField;
 
 import org.jdesktop.swingx.VerticalLayout;
 import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
+import org.leumasjaffe.recipe.controller.ReplaceChildrenController;
 import org.leumasjaffe.recipe.model.CollatedDuration;
 import org.leumasjaffe.recipe.model.Element;
 import org.leumasjaffe.recipe.model.RecipeCard;
@@ -22,12 +24,23 @@ import lombok.experimental.FieldDefaults;
 import java.awt.Font;
 
 @SuppressWarnings("serial")
-@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+@FieldDefaults(level=AccessLevel.PRIVATE)
 public class SummaryPanel extends JPanel {
+	ReplaceChildrenController<RecipeCard, Element> controller;
 	ObservableListener<CollatedDurationPanel, RecipeCard> durationListener;
 	ObservableListener<JPanel, RecipeCard> childListener;
 	
-	public SummaryPanel(final RecipeCard card) {
+	JTextField txtTitle;
+	JTextArea txaDesription;
+	
+	public SummaryPanel() {
+		controller = new ReplaceChildrenController<>(RecipeCard::getElements, element -> {
+			JPanel wrapper = new JPanel(new VerticalLayout());
+			wrapper.add(new ElementPanel(element));
+			wrapper.add(new JSeparator());
+			return wrapper;
+		});
+		
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0, 0, 0};
@@ -50,8 +63,7 @@ public class SummaryPanel extends JPanel {
 		gbl_panelHeader.rowWeights = new double[]{0.0, 0.0, Double.MIN_VALUE};
 		panelHeader.setLayout(gbl_panelHeader);
 		
-		JTextField txtTitle = new JTextField();
-		txtTitle.setText(card.getTitle());
+		txtTitle = new JTextField();
 		GridBagConstraints gbc_txtTitle = new GridBagConstraints();
 		gbc_txtTitle.insets = new Insets(0, 0, 0, 5);
 		gbc_txtTitle.fill = GridBagConstraints.HORIZONTAL;
@@ -84,23 +96,23 @@ public class SummaryPanel extends JPanel {
 		add(panel, gbc_panel);
 		GridBagLayout gbl_panel = new GridBagLayout();
 		gbl_panel.columnWidths = new int[]{0, 0};
-		gbl_panel.rowHeights = new int[]{0, 0, 0, 0};
+		gbl_panel.rowHeights = new int[]{0, 0, 0};
 		gbl_panel.columnWeights = new double[]{1.0, Double.MIN_VALUE};
-		gbl_panel.rowWeights = new double[]{0.0, 1.0, 0.0, Double.MIN_VALUE};
+		gbl_panel.rowWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
 		panel.setLayout(gbl_panel);
 		
 		JPanel panelPhoto = new ImagePanel();
 		GridBagConstraints gbc_panelPhoto = new GridBagConstraints();
+		gbc_panelPhoto.fill = GridBagConstraints.BOTH;
 		gbc_panelPhoto.insets = new Insets(0, 0, 5, 0);
 		gbc_panelPhoto.gridx = 0;
 		gbc_panelPhoto.gridy = 0;
 		panel.add(panelPhoto, gbc_panelPhoto);
 		
-		JTextArea txaDesription = new JTextArea(5, 20);
+		txaDesription = new JTextArea(5, 20);
 		txaDesription.setFont(new Font("Verdana", Font.PLAIN, 10));
 		txaDesription.setWrapStyleWord(true);
 		txaDesription.setLineWrap(true);
-		txaDesription.setText(card.getDescription());
 		GridBagConstraints gbc_txaDesription = new GridBagConstraints();
 		gbc_txaDesription.insets = new Insets(0, 0, 5, 0);
 		gbc_txaDesription.fill = GridBagConstraints.BOTH;
@@ -111,21 +123,28 @@ public class SummaryPanel extends JPanel {
 		durationListener = new ObservableListener<>(panelDuration,
 				(c, v) -> c.setModel(v.getCollatedDuration()));
 		
-		childListener = new ObservableListener<>(panelIngredients, (c, v) -> {
-			if (c.getComponents().length == v.getElements().size()) {
-				return;
-			}
-			c.removeAll();
-			for (final Element element : v.getElements()) {
-				JPanel wrapper = new JPanel(new VerticalLayout());
-				wrapper.add(new ElementPanel(element));
-				wrapper.add(new JSeparator());
-				c.add(wrapper);
-			}
-		});
-		
+		// This indirection allows for testing of controller
+		childListener = new ObservableListener<>(panelIngredients,
+				(c, v) -> controller.accept(c, v));
+	}
+	
+	public SummaryPanel(final RecipeCard card) {
+		this();
+		setModel(card);
+	}
+	
+	public void setModel(final RecipeCard card) {
+		txtTitle.setText(card.getTitle());
+		txaDesription.setText(card.getDescription());
 		durationListener.setObserved(card);
 		childListener.setObserved(card);
 	}
 	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(durationListener);
+		ObserverDispatch.unsubscribeAll(childListener);
+	}
+	
 }

+ 0 - 11
src/test/java/org/leumasjaffe/recipe/TestSuite.java

@@ -1,11 +0,0 @@
-package org.leumasjaffe.recipe;
-
-import org.junit.platform.runner.JUnitPlatform;
-import org.junit.platform.suite.api.SelectPackages;
-import org.junit.runner.RunWith;
-
-@RunWith(JUnitPlatform.class)
-@SelectPackages("org.leumasjaffe.recipe")
-public class TestSuite {
-
-}

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

@@ -0,0 +1,52 @@
+package org.leumasjaffe.recipe.controller;
+
+import static org.mockito.Mockito.*;
+
+import java.awt.Component;
+import java.awt.Container;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.function.Function;
+
+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.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+@RunWith(JUnitPlatform.class)
+class ReplaceChildrenControllerTest {
+	
+	@Mock Container parent;
+	Function<Void, Collection<Void>> getChildren = (v) -> Arrays.asList(null, null);
+	Function<Void, Component> makeView = (v) -> null;
+	ReplaceChildrenController<Void, Void> controller;
+	
+	@BeforeEach
+	void setUp() {
+		controller = new ReplaceChildrenController<>(getChildren, makeView);
+	}
+
+	@Test
+	void testDoesNotReplaceComponentsWhenNoSizeChange() {
+		doReturn(new Component[2]).when(parent).getComponents();
+		
+		controller.accept(parent, null);
+		
+		verify(parent, never()).removeAll();
+		verify(parent, never()).add((Component) any());
+	}
+
+	@Test
+	void testReplacesComponents() {
+		doReturn(new Component[1]).when(parent).getComponents();
+		
+		controller.accept(parent, null);
+		
+		verify(parent).removeAll();
+		verify(parent, times(2)).add((Component) any());
+	}
+}

+ 51 - 5
src/test/java/org/leumasjaffe/recipe/view/IngredientPanelTest.java

@@ -50,26 +50,72 @@ class IngredientPanelTest extends SwingTestCase {
 	@Test
 	void testIsSubscribedToUpdates() {
 		stuff.setName("Bacon");
+		stuff.setPreparation("Cut into Lardons");
+		stuff.setAmount(new Amount("0.25 lb"));
+		
 		assertEquals("Onions", panel.getTxtName().getText());
 		ObserverDispatch.notifySubscribers(stuff);
+		
 		assertEquals("Bacon", panel.getTxtName().getText());
-		// TODO: I need to add hook-ups for the rest of the fields, too
+		assertEquals(100.0, panel.getTxtAmount().getValue());
+		assertEquals("g", panel.getTxtUnit().getText());
+		assertEquals("Cut into Lardons", panel.getTxtPreparation().getText());
 	}
 
-	// TODO: I need to add hook-ups for the rest of the fields, too
 	@Test
-	void testViewUpdateAltersModel() {
+	void testViewUpdateToNameAltersModel() {
 		panel.getTxtName().setText("Bacon");
 		waitForSwing();
 		assertEquals("Bacon", stuff.getName());
 	}
+	
+	@Test
+	void testViewUpdateToPreparationAltersModel() {
+		panel.getTxtPreparation().setText("Cut into Lardons");
+		waitForSwing();
+		assertEquals("Cut into Lardons", stuff.getPreparation());
+	}
+	
+	@Test
+	void testViewUpdateToAmountDoesNotAltersModel() {
+		panel.getTxtAmount().setValue(0.25);
+		waitForSwing();
+		assertEquals(100.0, stuff.getAmount().getValue());
+	}
+	
+	@Test
+	void testViewUpdateToUnitDoesNotAltersModel() {
+		panel.getTxtUnit().setText("lb");
+		waitForSwing();
+		assertEquals("g", stuff.getAmount().unitName());
+	}
 
-	// TODO: I need to add hook-ups for the rest of the fields, too
 	@Test
-	void testViewUpdateSendsNotify() {
+	void testUpdateToNameSendsNotify() {
 		panel.getTxtName().setText("Bacon");
 		waitForSwing();
 		verify(listener).updateWasSignalled();
 	}
+	
+	@Test
+	void testUpdateToPreparationSendsNotify() {
+		panel.getTxtPreparation().setText("Cut into Lardons");
+		waitForSwing();
+		verify(listener).updateWasSignalled();
+	}
+
+	@Test
+	void testUpdateToAmountSendsNotify() {
+		panel.getTxtAmount().setValue(0.25);
+		waitForSwing();
+		verify(listener, never()).updateWasSignalled();
+	}
+	
+	@Test
+	void testUpdateToUnitSendsNotify() {
+		panel.getTxtUnit().setText("lb");
+		waitForSwing();
+		verify(listener, never()).updateWasSignalled();
+	}
 
 }

+ 29 - 24
src/test/java/org/leumasjaffe/recipe/view/PreparationPanelTest.java

@@ -1,52 +1,57 @@
 package org.leumasjaffe.recipe.view;
 
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.collection.IsArrayWithSize.arrayWithSize;
-import static org.hamcrest.core.StringContains.containsString;
 import static org.mockito.Mockito.*;
 
-import java.util.stream.Stream;
-
 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.model.Amount;
+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.Preparation;
+import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
-@RunWith(JUnitPlatform.class)
 class PreparationPanelTest extends SwingTestCase {
 	
-	Duration dur;
-	@Mock Preparation stuff;
-	PreparationPanel panel;
+	@Mock DurationPanel panelDuration;
+	@Mock ReplaceChildrenController<Preparation, Ingredient> controller;
+	Preparation stuff;
+	@InjectMocks PreparationPanel panel = new PreparationPanel();
 	
 	@BeforeEach
 	void setUp() {
-		dur = new Duration(Duration.Display.SECONDS, 0, 30);
+		stuff = mock(Preparation.class);
+		Duration dur = new Duration(Duration.Display.SECONDS, 0, 30);
 		doReturn(dur).when(stuff).getDuration();
-		doReturn(Stream.of(
-				new Ingredient("Butter", "", new Amount("10 g")),
-				new Ingredient("Salt", "", new Amount("0.25 tsp"))
-				)).when(stuff).getIngredientsAsStream();
 		
-		panel = new PreparationPanel(stuff);
+		panel.setModel(stuff);
 	}
 
 	@Test
 	void testHasContent() {
-		assertThat(panel.getLblDuration().getText(),
-				containsString(dur.toString()));
-		assertThat(panel.getPanelIngredients().getComponents(),
-				arrayWithSize(2));
+		verify(panelDuration).setModel(any());
+		verify(controller, times(1)).accept(any(), same(stuff));
+	}
+
+	@Test
+	void testDoesNotUpdateDurationWhenNotified() {
+		clearInvocations(panelDuration);
+
+		ObserverDispatch.notifySubscribers(stuff);		
+
+		verify(panelDuration, never()).setModel(any());
 	}
 
-	// TODO: Hook-ups for editing the preparation time
-	// TODO: Hook-ups for changes to the preparation model
+	@Test
+	void testUpdatesNumberOfChildrenWhenNotified() {
+		clearInvocations((Object) controller);
+		
+		ObserverDispatch.notifySubscribers(stuff);		
+
+		verify(controller, times(1)).accept(any(), same(stuff));
+	}
 }

+ 67 - 0
src/test/java/org/leumasjaffe/recipe/view/RecipeCardPanelTest.java

@@ -0,0 +1,67 @@
+package org.leumasjaffe.recipe.view;
+
+import static org.mockito.Mockito.*;
+
+import java.awt.Component;
+import java.util.Arrays;
+
+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.leumasjaffe.mock.MockObserverListener;
+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 org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class RecipeCardPanelTest {
+	
+	@Spy MockObserverListener listener;
+	
+	@Mock SummaryPanel summaryPanel;
+	@Mock JPanel rightPanel;
+	@Spy RecipeCard card;
+	@InjectMocks RecipeCardPanel panel = new RecipeCardPanel();
+	
+	@BeforeEach
+	void setUp() {
+		listener.setObserved(card);
+		clearInvocations(listener);
+	}
+
+	@Test
+	void testModelIsPropogated() {
+		panel.setModel(card);
+		verify(summaryPanel).setModel(same(card));
+		verify(rightPanel, never()).add(any(Component.class));
+	}
+
+	@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));
+	}
+
+	@Test
+	void testToChildElementsIsPropogated() {
+		final Element element = new Element();
+		doReturn(Arrays.asList(element)).when(card).getElements();
+
+		panel.setModel(card);
+		verify(listener).updateWasSignalled();
+		
+		ObserverDispatch.notifySubscribers(element);
+		verify(listener, times(2)).updateWasSignalled();
+	}
+
+}

+ 92 - 0
src/test/java/org/leumasjaffe/recipe/view/StepPanelIT.java

@@ -0,0 +1,92 @@
+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.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;
+
+	@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();
+				
+		panel = new StepPanel(0, stuff);
+		listener.setObserved(stuff);
+		// setObserved invokes our callback.
+		clearInvocations(listener);
+	}
+
+	@Test
+	void testRecievesSignalWhenNewElementAdded() {
+		final IngredientPanel newIngredient =
+				(IngredientPanel) panel.getPanelIngredients().getComponent(1);
+		
+		newIngredient.getTxtName().setText("Bacon");
+		waitForSwing();
+		
+		verify(listener, times(1)).updateWasSignalled();
+	}
+
+	@Test
+	void testNewItemCanProduceUpdate() {
+		final IngredientPanel newIngredient =
+				(IngredientPanel) panel.getPanelIngredients().getComponent(1);
+		newIngredient.getTxtName().setText("Bacon");
+		waitForSwing();
+
+		ObserverDispatch.notifySubscribers(ingredients.get(1));
+		
+		verify(listener, times(2)).updateWasSignalled();
+	}
+
+	@Test
+	void testReceivesSignalWhenElementRemoved() {
+		final IngredientPanel oldIngredient =
+				(IngredientPanel) panel.getPanelIngredients().getComponent(0);
+		oldIngredient.getTxtName().setText("");
+		waitForSwing();
+
+		verify(listener, times(1)).updateWasSignalled();
+	}
+
+	@Test
+	void testIgnoresOldItemUpdates() {
+		final Ingredient ing = ingredients.get(0);
+		final IngredientPanel oldIngredient =
+				(IngredientPanel) panel.getPanelIngredients().getComponent(0);
+		oldIngredient.getTxtName().setText("");
+
+		waitForSwing();
+		clearInvocations(listener);
+
+		ObserverDispatch.notifySubscribers(ing);
+		verify(listener, never()).updateWasSignalled();
+	}
+
+}

+ 18 - 9
src/test/java/org/leumasjaffe/recipe/view/StepPanelTest.java

@@ -6,7 +6,9 @@ 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 org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -14,34 +16,30 @@ 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.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
 @RunWith(JUnitPlatform.class)
 class StepPanelTest extends SwingTestCase {
-	@Spy MockObserverListener listener;
-	Duration dur;
+	List<Ingredient> ingredients;
 	@Mock Step stuff;
 	StepPanel panel;
 
 	@BeforeEach
 	void setUp() {
-		dur = new Duration(Duration.Display.SECONDS, 0, 30);
+		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(Arrays.asList(new Ingredient("Onion", "Sliced", new Amount("100 g"))))
-			.when(stuff).getIngredients();
+		doReturn(ingredients).when(stuff).getIngredients();
 		
 		panel = new StepPanel(0, stuff);
-		listener.setObserved(stuff);
-		// setObserved invokes our callback.
-		clearInvocations(listener);
 	}
 
 	@Test
@@ -52,4 +50,15 @@ class StepPanelTest extends SwingTestCase {
 				arrayWithSize(greaterThanOrEqualTo(1)));
 	}
 
+	@Test
+	void testPropogatesNotifications() {
+		final MockObserverListener listener = spy(MockObserverListener.class);
+		listener.setObserved(stuff);
+		// setObserved invokes our callback.
+		clearInvocations(listener);
+		
+		ObserverDispatch.notifySubscribers(ingredients.get(0));
+		
+		verify(listener, atLeast(1)).updateWasSignalled();
+	}
 }

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

@@ -17,14 +17,14 @@ import org.leumasjaffe.recipe.model.Amount;
 import org.leumasjaffe.recipe.model.Ingredient;
 import org.leumasjaffe.recipe.view.SwingTestCase;
 import org.leumasjaffe.recipe.model.Element;
-import org.mockito.Mock;
+import org.mockito.Spy;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
 @RunWith(JUnitPlatform.class)
 class ElementPanelTest extends SwingTestCase {
 	
-	@Mock Element stuff;
+	@Spy Element stuff;
 	ElementPanel panel;
 	
 	@BeforeEach

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

@@ -0,0 +1,31 @@
+package org.leumasjaffe.recipe.view.summary;
+
+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;
+import org.leumasjaffe.recipe.model.RecipeCard;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+@RunWith(JUnitPlatform.class)
+class SummaryPanelTest {
+	
+	RecipeCard card = new RecipeCard();
+	@Mock ReplaceChildrenController<RecipeCard, Element> controller;
+	@InjectMocks SummaryPanel panel = new SummaryPanel(card);
+
+	@Test
+	void testUpdateToCardInvokesController() {
+		ObserverDispatch.notifySubscribers(card);
+		verify(controller).accept(any(), same(card));
+	}
+
+}