diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..8d38d0f99 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: Java CI with Maven + +on: + pull_request: + branches: [ "develop" ] + push: + branches: [ "develop", "feature-branch" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + cache: maven + + - name: Build with Maven + run: mvn clean install -DskipTests --file pom.xml --settings .maven-settings.xml + + - name: Run Tests + run: mvn test --file pom.xml --settings .maven-settings.xml + diff --git a/.maven-settings.xml b/.maven-settings.xml new file mode 100644 index 000000000..0de651b15 --- /dev/null +++ b/.maven-settings.xml @@ -0,0 +1,9 @@ + + + + github + ${env.GITHUB_ACTOR} + ${env.GITHUB_TOKEN} + + + diff --git a/jhotdraw-core/pom.xml b/jhotdraw-core/pom.xml index 7c276da85..8f23616c2 100644 --- a/jhotdraw-core/pom.xml +++ b/jhotdraw-core/pom.xml @@ -35,10 +35,43 @@ 6.8.21 test + + junit + junit + 4.13.2 + test + + + org.mockito + mockito-core + 4.11.0 + test + + + com.tngtech.jgiven + jgiven-junit + 0.18.2 + test + + + org.assertj + assertj-core + 3.24.2 + test + ${project.groupId} jhotdraw-actions ${project.version} + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + \ No newline at end of file diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/GroupAction.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/GroupAction.java index 08c5a0ada..d50f230f0 100644 --- a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/GroupAction.java +++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/GroupAction.java @@ -7,160 +7,73 @@ */ package org.jhotdraw.draw.action; -import org.jhotdraw.draw.figure.Figure; +import java.awt.event.ActionEvent; +import java.util.Collection; +import java.util.LinkedList; +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import javax.swing.undo.UndoableEdit; +import org.jhotdraw.draw.Drawing; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.DrawingView; import org.jhotdraw.draw.figure.CompositeFigure; +import org.jhotdraw.draw.figure.Figure; import org.jhotdraw.draw.figure.GroupFigure; -import java.util.*; -import javax.swing.undo.*; -import org.jhotdraw.draw.*; import org.jhotdraw.util.ResourceBundleUtil; -/** - * GroupAction. - * - * @author Werner Randelshofer - * @version $Id$ - */ public class GroupAction extends AbstractSelectedAction { private static final long serialVersionUID = 1L; public static final String ID = "edit.groupSelection"; private CompositeFigure prototype; - /** - * If this variable is true, this action groups figures. - * If this variable is false, this action ungroups figures. - */ - private boolean isGroupingAction; - - /** - * Creates a new instance. - */ + public GroupAction(DrawingEditor editor) { - this(editor, new GroupFigure(), true); + this(editor, new GroupFigure()); } public GroupAction(DrawingEditor editor, CompositeFigure prototype) { - this(editor, prototype, true); - } - - public GroupAction(DrawingEditor editor, CompositeFigure prototype, boolean isGroupingAction) { super(editor); this.prototype = prototype; - this.isGroupingAction = isGroupingAction; - ResourceBundleUtil labels - = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels"); + ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels"); labels.configureAction(this, ID); updateEnabledState(); } @Override protected void updateEnabledState() { - if (getView() != null) { - setEnabled(isGroupingAction ? canGroup() : canUngroup()); - } else { - setEnabled(false); - } + setEnabled(canGroup(getView())); } protected boolean canGroup() { - return getView() != null && getView().getSelectionCount() > 1; + return canGroup(getView()); } - protected boolean canUngroup() { - return getView() != null - && getView().getSelectionCount() == 1 - && prototype != null - && getView().getSelectedFigures().iterator().next().getClass().equals( - prototype.getClass()); + protected boolean canGroup(DrawingView v) { + return v != null && v.getSelectionCount() > 1; } @Override - public void actionPerformed(java.awt.event.ActionEvent e) { - if (isGroupingAction) { - if (canGroup()) { - final DrawingView view = getView(); - final LinkedList
ungroupedFigures = new LinkedList<>(view.getSelectedFigures()); - final CompositeFigure group = (CompositeFigure) prototype.clone(); - UndoableEdit edit = new AbstractUndoableEdit() { - private static final long serialVersionUID = 1L; - - @Override - public String getPresentationName() { - ResourceBundleUtil labels - = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels"); - return labels.getString("edit.groupSelection.text"); - } - - @Override - public void redo() throws CannotRedoException { - super.redo(); - groupFigures(view, group, ungroupedFigures); - } - - @Override - public void undo() throws CannotUndoException { - ungroupFigures(view, group); - super.undo(); - } - - @Override - public boolean addEdit(UndoableEdit anEdit) { - return super.addEdit(anEdit); - } - }; - groupFigures(view, group, ungroupedFigures); - fireUndoableEditHappened(edit); - } - } else { - if (canUngroup()) { - final DrawingView view = getView(); - final CompositeFigure group = (CompositeFigure) getView().getSelectedFigures().iterator().next(); - final LinkedList
ungroupedFigures = new LinkedList<>(); - UndoableEdit edit = new AbstractUndoableEdit() { - private static final long serialVersionUID = 1L; - - @Override - public String getPresentationName() { - ResourceBundleUtil labels - = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels"); - return labels.getString("edit.ungroupSelection.text"); - } - - @Override - public void redo() throws CannotRedoException { - super.redo(); - ungroupFigures(view, group); - } - - @Override - public void undo() throws CannotUndoException { - groupFigures(view, group, ungroupedFigures); - super.undo(); - } - }; - ungroupedFigures.addAll(ungroupFigures(view, group)); - fireUndoableEditHappened(edit); - } + public void actionPerformed(ActionEvent e) { + if (canGroup()) { + final DrawingView view = getView(); + final LinkedList
ungroupedFigures = new LinkedList<>(view.getSelectedFigures()); + assert prototype.clone() instanceof CompositeFigure : "prototype.clone() must be a CompositeFigure"; + final CompositeFigure group = (CompositeFigure) prototype.clone(); + + UndoableEdit edit = new GroupUndoableEdit(view, group, ungroupedFigures); + groupFigures(view, group, ungroupedFigures); + fireUndoableEditHappened(edit); } } - public Collection
ungroupFigures(DrawingView view, CompositeFigure group) { -// XXX - This code is redundant with UngroupAction - LinkedList
figures = new LinkedList<>(group.getChildren()); - view.clearSelection(); - group.basicRemoveAllChildren(); - view.getDrawing().basicAddAll(view.getDrawing().indexOf(group), figures); - view.getDrawing().remove(group); - view.addToSelection(figures); - return figures; - } - public void groupFigures(DrawingView view, CompositeFigure group, Collection
figures) { - Collection
sorted = view.getDrawing().sort(figures); - int index = view.getDrawing().indexOf(sorted.iterator().next()); - view.getDrawing().basicRemoveAll(figures); + Drawing drawing = view.getDrawing(); + Collection
sorted = drawing.sort(figures); + int index = drawing.indexOf(sorted.iterator().next()); + drawing.basicRemoveAll(figures); view.clearSelection(); - view.getDrawing().add(index, group); + drawing.add(index, group); group.willChange(); for (Figure f : sorted) { f.willChange(); @@ -169,4 +82,43 @@ public void groupFigures(DrawingView view, CompositeFigure group, Collection ungroupedFigures; + + GroupUndoableEdit(DrawingView view, CompositeFigure group, LinkedList
ungroupedFigures) { + this.view = view; + this.group = group; + this.ungroupedFigures = ungroupedFigures; + } + + @Override + public String getPresentationName() { + ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels"); + return labels.getString("edit.groupSelection.text"); + } + + @Override + public void redo() throws CannotRedoException { + super.redo(); + groupFigures(view, group, ungroupedFigures); + } + + @Override + public void undo() throws CannotUndoException { + Drawing drawing = view.getDrawing(); + LinkedList
figures = new LinkedList<>(group.getChildren()); + view.clearSelection(); + group.basicRemoveAllChildren(); + drawing.basicAddAll(drawing.indexOf(group), figures); + drawing.remove(group); + view.addToSelection(figures); + super.undo(); + } + } } diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/UngroupAction.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/UngroupAction.java index dc8956cfd..0fe7c9b07 100644 --- a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/UngroupAction.java +++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/UngroupAction.java @@ -7,40 +7,108 @@ */ package org.jhotdraw.draw.action; +import java.awt.event.ActionEvent; +import java.util.Collection; +import java.util.LinkedList; +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import javax.swing.undo.UndoableEdit; +import org.jhotdraw.draw.Drawing; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.DrawingView; import org.jhotdraw.draw.figure.CompositeFigure; +import org.jhotdraw.draw.figure.Figure; import org.jhotdraw.draw.figure.GroupFigure; -import org.jhotdraw.draw.*; import org.jhotdraw.util.ResourceBundleUtil; -/** - * UngroupAction. - * - * @author Werner Randelshofer - * @version $Id$ - */ -public class UngroupAction extends GroupAction { +public class UngroupAction extends AbstractSelectedAction { private static final long serialVersionUID = 1L; public static final String ID = "edit.ungroupSelection"; - /** - * Creates a new instance. - */ private CompositeFigure prototype; - /** - * Creates a new instance. - */ + // Constructor chaining eliminates the duplicate setup code smell public UngroupAction(DrawingEditor editor) { - super(editor, new GroupFigure(), false); - ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels"); - labels.configureAction(this, ID); - updateEnabledState(); + this(editor, new GroupFigure()); } public UngroupAction(DrawingEditor editor, CompositeFigure prototype) { - super(editor, prototype, false); + super(editor); + this.prototype = prototype; ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels"); labels.configureAction(this, ID); updateEnabledState(); } + + @Override + protected void updateEnabledState() { + setEnabled(canUngroup(getView())); + } + + protected boolean canUngroup() { + return canUngroup(getView()); + } + + protected boolean canUngroup(DrawingView v) { + return v != null + && v.getSelectionCount() == 1 + && prototype != null + && v.getSelectedFigures().iterator().next().getClass().equals( + prototype.getClass()); + } + + @Override + public void actionPerformed(ActionEvent e) { + if (canUngroup()) { + final DrawingView view = getView(); + final CompositeFigure group = (CompositeFigure) view.getSelectedFigures().iterator().next(); + final LinkedList
ungroupedFigures = new LinkedList<>(); + + UndoableEdit edit = new UngroupUndoableEdit(view, group); + ungroupedFigures.addAll(ungroupFigures(view, group)); + fireUndoableEditHappened(edit); + } + } + + public Collection
ungroupFigures(DrawingView view, CompositeFigure group) { + Drawing drawing = view.getDrawing(); + LinkedList
figures = new LinkedList<>(group.getChildren()); + view.clearSelection(); + group.basicRemoveAllChildren(); + drawing.basicAddAll(drawing.indexOf(group), figures); + drawing.remove(group); + view.addToSelection(figures); + return figures; + } + + // Extracted from anonymous inner class to eliminate Anonymous Inner Class smell + private class UngroupUndoableEdit extends AbstractUndoableEdit { + + private static final long serialVersionUID = 1L; + private final DrawingView view; + private final CompositeFigure group; + + UngroupUndoableEdit(DrawingView view, CompositeFigure group) { + this.view = view; + this.group = group; + } + + @Override + public String getPresentationName() { + ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels"); + return labels.getString("edit.ungroupSelection.text"); + } + + @Override + public void redo() throws CannotRedoException { + super.redo(); + ungroupFigures(view, group); + } + + @Override + public void undo() throws CannotUndoException { + super.undo(); + } + } } diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GivenGroupingStage.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GivenGroupingStage.java new file mode 100644 index 000000000..03886ca01 --- /dev/null +++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GivenGroupingStage.java @@ -0,0 +1,111 @@ +package org.jhotdraw.draw.action; + +import com.tngtech.jgiven.Stage; +import com.tngtech.jgiven.annotation.ScenarioState; +import org.jhotdraw.draw.Drawing; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.figure.Figure; +import org.jhotdraw.draw.figure.GroupFigure; +import org.mockito.Mockito; + +import java.awt.geom.Rectangle2D; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class GivenGroupingStage extends Stage { + + @ScenarioState + DrawingView view; + + @ScenarioState + GroupAction groupAction; + + @ScenarioState + UngroupAction ungroupAction; + + @ScenarioState + List
selectedFigures; + + @ScenarioState + GroupFigure selectedGroup; + + /** Returns a stub rectangle used wherever getDrawingArea() is needed. */ + private Rectangle2D.Double rect() { + return new Rectangle2D.Double(0, 0, 10, 10); + } + + /** Stubs both getDrawingArea() overloads on a mock figure. */ + private void stubDrawingArea(Figure mock) { + Mockito.when(mock.getDrawingArea()).thenReturn(rect()); + Mockito.when(mock.getDrawingArea(Mockito.anyDouble())).thenReturn(rect()); + } + + public GivenGroupingStage two_figures_are_selected() { + Drawing drawing = Mockito.mock(Drawing.class); + view = Mockito.mock(DrawingView.class); + Mockito.when(view.getDrawing()).thenReturn(drawing); + + Figure f1 = Mockito.mock(Figure.class); + Figure f2 = Mockito.mock(Figure.class); + stubDrawingArea(f1); + stubDrawingArea(f2); + selectedFigures = Arrays.asList(f1, f2); + + Mockito.when(drawing.sort(selectedFigures)).thenReturn(selectedFigures); + Mockito.when(drawing.indexOf(f1)).thenReturn(0); + + Set
selection = new HashSet<>(selectedFigures); + Mockito.when(view.getSelectionCount()).thenReturn(2); + Mockito.when(view.getSelectedFigures()).thenReturn(selection); + + groupAction = new GroupAction(null, new GroupFigure()); + return self(); + } + + public GivenGroupingStage one_GroupFigure_is_selected() { + Drawing drawing = Mockito.mock(Drawing.class); + view = Mockito.mock(DrawingView.class); + Mockito.when(view.getDrawing()).thenReturn(drawing); + + selectedGroup = new GroupFigure(); + + Figure child1 = Mockito.mock(Figure.class); + Figure child2 = Mockito.mock(Figure.class); + stubDrawingArea(child1); + stubDrawingArea(child2); + + selectedGroup.willChange(); + selectedGroup.basicAdd(child1); + selectedGroup.basicAdd(child2); + selectedGroup.changed(); + + Mockito.when(drawing.indexOf(selectedGroup)).thenReturn(0); + + Set
selection = new HashSet<>(Collections.
singletonList(selectedGroup)); + Mockito.when(view.getSelectionCount()).thenReturn(1); + Mockito.when(view.getSelectedFigures()).thenReturn(selection); + + ungroupAction = new UngroupAction(null, new GroupFigure()); + return self(); + } + + public GivenGroupingStage one_plain_figure_is_selected() { + Drawing drawing = Mockito.mock(Drawing.class); + view = Mockito.mock(DrawingView.class); + Mockito.when(view.getDrawing()).thenReturn(drawing); + + Figure f = Mockito.mock(Figure.class); + stubDrawingArea(f); + selectedFigures = Collections.singletonList(f); + + Set
selection = new HashSet<>(selectedFigures); + Mockito.when(view.getSelectionCount()).thenReturn(1); + Mockito.when(view.getSelectedFigures()).thenReturn(selection); + + groupAction = new GroupAction(null, new GroupFigure()); + return self(); + } +} diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GroupActionTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GroupActionTest.java new file mode 100644 index 000000000..4c5244fc4 --- /dev/null +++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GroupActionTest.java @@ -0,0 +1,273 @@ +/* + * JUnit 4 tests for GroupAction and UngroupAction domain logic. + * Swing dependencies (DrawingView, Drawing) are stubbed with Mockito + * so each test exercises a single code path through a single method. + */ +package org.jhotdraw.draw.action; + +import org.jhotdraw.draw.Drawing; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.figure.CompositeFigure; +import org.jhotdraw.draw.figure.Figure; +import org.jhotdraw.draw.figure.GroupFigure; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.*; +import org.mockito.InOrder; + +import static org.mockito.Mockito.*; + +public class GroupActionTest { + + // --- shared stubs --- + private DrawingView view; + private Drawing drawing; + private GroupAction groupAction; + private UngroupAction ungroupAction; + + @Before + public void setUp() { + view = mock(DrawingView.class); + drawing = mock(Drawing.class); + when(view.getDrawing()).thenReturn(drawing); + + // Construct actions without a real DrawingEditor by passing null; + // we only test the pure domain methods (groupFigures / ungroupFigures / + // canGroup / canUngroup) which do not touch the editor. + groupAction = new GroupAction(null, new GroupFigure()); + ungroupAction = new UngroupAction(null, new GroupFigure()); + } + + // ========================================================= + // GroupAction – canGroup + // ========================================================= + + @Test + public void canGroup_returnsFalse_whenViewIsNull() { + // view == null → cannot group + assertFalse(groupAction.canGroup(null)); + } + + @Test + public void canGroup_returnsFalse_whenSelectionCountIsOne() { + when(view.getSelectionCount()).thenReturn(1); + assertFalse(groupAction.canGroup(view)); + } + + @Test + public void canGroup_returnsTrue_whenSelectionCountIsTwo() { + when(view.getSelectionCount()).thenReturn(2); + assertTrue(groupAction.canGroup(view)); + } + + @Test + public void canGroup_returnsTrue_whenSelectionCountIsMany() { + // boundary: more than two figures selected + when(view.getSelectionCount()).thenReturn(10); + assertTrue(groupAction.canGroup(view)); + } + + // ========================================================= + // GroupAction – groupFigures + // ========================================================= + + @Test + public void groupFigures_addsGroupToDrawingAtCorrectIndex() { + Figure f1 = mock(Figure.class); + Figure f2 = mock(Figure.class); + List
figures = Arrays.asList(f1, f2); + CompositeFigure group = mock(CompositeFigure.class); + + when(drawing.sort(figures)).thenReturn(figures); + when(drawing.indexOf(f1)).thenReturn(3); + + groupAction.groupFigures(view, group, figures); + + verify(drawing).basicRemoveAll(figures); + verify(drawing).add(3, group); + } + + @Test + public void groupFigures_addsEachFigureToGroup() { + Figure f1 = mock(Figure.class); + Figure f2 = mock(Figure.class); + List
figures = Arrays.asList(f1, f2); + CompositeFigure group = mock(CompositeFigure.class); + + when(drawing.sort(figures)).thenReturn(figures); + when(drawing.indexOf(f1)).thenReturn(0); + + groupAction.groupFigures(view, group, figures); + + verify(group).basicAdd(f1); + verify(group).basicAdd(f2); + } + + @Test + public void groupFigures_selectsGroupAfterGrouping() { + Figure f1 = mock(Figure.class); + List
figures = Collections.singletonList(f1); + CompositeFigure group = mock(CompositeFigure.class); + + when(drawing.sort(figures)).thenReturn(figures); + when(drawing.indexOf(f1)).thenReturn(0); + + groupAction.groupFigures(view, group, figures); + + verify(view).addToSelection(group); + } + + @Test + public void groupFigures_callsWillChangeAndChangedOnGroup() { + Figure f1 = mock(Figure.class); + List
figures = Collections.singletonList(f1); + CompositeFigure group = mock(CompositeFigure.class); + + when(drawing.sort(figures)).thenReturn(figures); + when(drawing.indexOf(f1)).thenReturn(0); + + groupAction.groupFigures(view, group, figures); + + verify(group).willChange(); + verify(group).changed(); + } + + // ========================================================= + // UngroupAction – canUngroup + // ========================================================= + + @Test + public void canUngroup_returnsFalse_whenViewIsNull() { + assertFalse(ungroupAction.canUngroup(null)); + } + + @Test + public void canUngroup_returnsFalse_whenSelectionCountIsNotOne() { + when(view.getSelectionCount()).thenReturn(2); + assertFalse(ungroupAction.canUngroup(view)); + } + + @Test + public void canUngroup_returnsFalse_whenSelectionCountIsZero() { + // boundary: empty selection + when(view.getSelectionCount()).thenReturn(0); + assertFalse(ungroupAction.canUngroup(view)); + } + + @Test + public void canUngroup_returnsFalse_whenSelectedFigureIsWrongType() { + Figure plainFigure = mock(Figure.class); + Set
sel = new HashSet<>(Collections.singletonList(plainFigure)); + when(view.getSelectionCount()).thenReturn(1); + when(view.getSelectedFigures()).thenReturn(sel); + assertFalse(ungroupAction.canUngroup(view)); + } + + @Test + public void canUngroup_returnsTrue_whenExactlyOneGroupFigureSelected() { + GroupFigure gf = new GroupFigure(); + Set
sel = new HashSet<>(Collections.
singletonList(gf)); + when(view.getSelectionCount()).thenReturn(1); + when(view.getSelectedFigures()).thenReturn(sel); + assertTrue(ungroupAction.canUngroup(view)); + } + + // ========================================================= + // UngroupAction – ungroupFigures + // ========================================================= + + @Test + public void ungroupFigures_returnsChildrenOfGroup() { + Figure child1 = mock(Figure.class); + Figure child2 = mock(Figure.class); + CompositeFigure group = mock(CompositeFigure.class); + when(group.getChildren()).thenReturn(Arrays.asList(child1, child2)); + when(drawing.indexOf(group)).thenReturn(0); + + Collection
result = ungroupAction.ungroupFigures(view, group); + + assertTrue(result.contains(child1)); + assertTrue(result.contains(child2)); + } + + @Test + public void ungroupFigures_removesGroupFromDrawing() { + CompositeFigure group = mock(CompositeFigure.class); + when(group.getChildren()).thenReturn(Collections.emptyList()); + when(drawing.indexOf(group)).thenReturn(2); + + ungroupAction.ungroupFigures(view, group); + + verify(drawing).remove(group); + } + + @Test + public void ungroupFigures_addsChildrenBackToDrawingAtGroupIndex() { + Figure child = mock(Figure.class); + CompositeFigure group = mock(CompositeFigure.class); + List
children = Collections.singletonList(child); + when(group.getChildren()).thenReturn(children); + when(drawing.indexOf(group)).thenReturn(5); + + ungroupAction.ungroupFigures(view, group); + + verify(drawing).basicAddAll(5, children); + } + + @Test + public void ungroupFigures_clearsSelectionThenSelectsChildren() { + Figure child = mock(Figure.class); + CompositeFigure group = mock(CompositeFigure.class); + when(group.getChildren()).thenReturn(Collections.singletonList(child)); + when(drawing.indexOf(group)).thenReturn(0); + + ungroupAction.ungroupFigures(view, group); + + // clearSelection must happen before addToSelection + InOrder inOrder = inOrder(view); + inOrder.verify(view).clearSelection(); + inOrder.verify(view).addToSelection(anyCollection()); + } + + @Test + public void ungroupFigures_returnsEmptyCollection_whenGroupHasNoChildren() { + // boundary: empty group + CompositeFigure group = mock(CompositeFigure.class); + when(group.getChildren()).thenReturn(Collections.emptyList()); + when(drawing.indexOf(group)).thenReturn(0); + + Collection
result = ungroupAction.ungroupFigures(view, group); + + assertTrue(result.isEmpty()); + } + + // ========================================================= + // Java assertion (invariant) smoke test + // ========================================================= + + @Test + public void groupFigures_invariant_groupIsInDrawingAfterGrouping() { + Figure f1 = mock(Figure.class); + List
figures = Collections.singletonList(f1); + CompositeFigure group = mock(CompositeFigure.class); + + when(drawing.sort(figures)).thenReturn(figures); + when(drawing.indexOf(f1)).thenReturn(0); + // After add(0, group) the drawing "contains" group + when(drawing.contains(group)).thenReturn(true); + + groupAction.groupFigures(view, group, figures); + + // Invariant: the group must exist in the drawing after grouping + assert drawing.contains(group) : "Invariant violated: group must be in drawing after groupFigures"; + } +} diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GroupingBDDTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GroupingBDDTest.java new file mode 100644 index 000000000..1103df562 --- /dev/null +++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GroupingBDDTest.java @@ -0,0 +1,28 @@ +package org.jhotdraw.draw.action; + +import com.tngtech.jgiven.junit.ScenarioTest; +import org.junit.Test; + +public class GroupingBDDTest extends ScenarioTest { + + @Test + public void a_designer_can_group_multiple_figures() { + given().two_figures_are_selected(); + when().the_group_action_is_triggered(); + then().the_figures_are_combined_into_a_GroupFigure(); + } + + @Test + public void a_designer_can_ungroup_a_grouped_figure() { + given().one_GroupFigure_is_selected(); + when().the_ungroup_action_is_triggered(); + then().the_children_are_restored_to_the_drawing(); + } + + @Test + public void group_action_is_disabled_for_a_single_figure() { + given().one_plain_figure_is_selected(); + when().checking_if_grouping_is_allowed(); + then().canGroup_returns_false(); + } +} diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/ThenGroupingStage.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/ThenGroupingStage.java new file mode 100644 index 000000000..7c6c0f080 --- /dev/null +++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/ThenGroupingStage.java @@ -0,0 +1,53 @@ +package org.jhotdraw.draw.action; + +import com.tngtech.jgiven.Stage; +import com.tngtech.jgiven.annotation.ScenarioState; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.figure.CompositeFigure; +import org.jhotdraw.draw.figure.Figure; +import org.jhotdraw.draw.figure.GroupFigure; +import org.mockito.Mockito; + +import java.util.Collection; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ThenGroupingStage extends Stage { + + @ScenarioState + DrawingView view; + + @ScenarioState + CompositeFigure resultGroup; + + @ScenarioState + Collection
resultChildren; + + @ScenarioState + boolean canGroupResult; + + public ThenGroupingStage the_figures_are_combined_into_a_GroupFigure() { + assertThat(resultGroup) + .as("result must be a GroupFigure") + .isInstanceOf(GroupFigure.class); + // Verify the group was selected in the view + Mockito.verify(view).addToSelection(resultGroup); + return self(); + } + + public ThenGroupingStage the_children_are_restored_to_the_drawing() { + assertThat(resultChildren) + .as("ungroupFigures must return the children that were in the group") + .hasSize(2); + // Verify children were re-selected in the view + Mockito.verify(view).addToSelection(resultChildren); + return self(); + } + + public ThenGroupingStage canGroup_returns_false() { + assertThat(canGroupResult) + .as("canGroup should be false when only one figure is selected") + .isFalse(); + return self(); + } +} diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/WhenGroupingStage.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/WhenGroupingStage.java new file mode 100644 index 000000000..962ebd95b --- /dev/null +++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/WhenGroupingStage.java @@ -0,0 +1,56 @@ +package org.jhotdraw.draw.action; + +import com.tngtech.jgiven.Stage; +import com.tngtech.jgiven.annotation.ScenarioState; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.figure.CompositeFigure; +import org.jhotdraw.draw.figure.Figure; +import org.jhotdraw.draw.figure.GroupFigure; + +import java.util.Collection; +import java.util.List; + +public class WhenGroupingStage extends Stage { + + @ScenarioState + DrawingView view; + + @ScenarioState + GroupAction groupAction; + + @ScenarioState + UngroupAction ungroupAction; + + @ScenarioState + List
selectedFigures; + + @ScenarioState + GroupFigure selectedGroup; + + @ScenarioState + CompositeFigure resultGroup; + + @ScenarioState + Collection
resultChildren; + + @ScenarioState + boolean canGroupResult; + + public WhenGroupingStage the_group_action_is_triggered() { + resultGroup = new GroupFigure(); + // drawing stubs were set up in Given; groupFigures calls view.getDrawing() internally + groupAction.groupFigures(view, resultGroup, selectedFigures); + return self(); + } + + public WhenGroupingStage the_ungroup_action_is_triggered() { + // selectedGroup was pre-populated with children in Given + resultChildren = ungroupAction.ungroupFigures(view, selectedGroup); + return self(); + } + + public WhenGroupingStage checking_if_grouping_is_allowed() { + canGroupResult = groupAction.canGroup(view); + return self(); + } +} diff --git a/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/odg/action/CombineAction.java b/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/odg/action/CombineAction.java index 62b3bd1ec..f28256f5e 100644 --- a/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/odg/action/CombineAction.java +++ b/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/odg/action/CombineAction.java @@ -50,7 +50,6 @@ protected boolean canGroup() { return canCombine; } - @Override @SuppressWarnings("unchecked") public Collection
ungroupFigures(DrawingView view, CompositeFigure group) { LinkedList
figures = new LinkedList
(group.getChildren()); diff --git a/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/odg/action/SplitAction.java b/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/odg/action/SplitAction.java index a48a85f32..59a19cfbc 100644 --- a/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/odg/action/SplitAction.java +++ b/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/odg/action/SplitAction.java @@ -1,10 +1,3 @@ -/* - * @(#)SplitPathsAction.java - * - * Copyright (c) 2007 The authors and contributors of JHotDraw. - * You may not use, copy or modify this file, except in compliance with the - * accompanying license terms. - */ package org.jhotdraw.samples.odg.action; import org.jhotdraw.draw.figure.Figure; @@ -15,12 +8,6 @@ import org.jhotdraw.samples.odg.figures.ODGPathFigure; import org.jhotdraw.util.*; -/** - * SplitPathsAction. - * - * @author Werner Randelshofer - * @version $Id$ - */ public class SplitAction extends UngroupAction { private static final long serialVersionUID = 1L; @@ -28,9 +15,6 @@ public class SplitAction extends UngroupAction { private ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.samples.odg.Labels"); - /** - * Creates a new instance. - */ public SplitAction(DrawingEditor editor) { super(editor, new ODGPathFigure()); labels.configureAction(this, ID); @@ -65,26 +49,4 @@ public Collection
ungroupFigures(DrawingView view, CompositeFigure group view.addToSelection(paths); return figures; } - - @SuppressWarnings("unchecked") - @Override - public void groupFigures(DrawingView view, CompositeFigure group, Collection
figures) { - Collection
sorted = view.getDrawing().sort(figures); - view.getDrawing().basicRemoveAll(figures); - view.clearSelection(); - view.getDrawing().add(group); - group.willChange(); - ((ODGPathFigure) group).removeAllChildren(); - for (Map.Entry, Object> entry : figures.iterator().next().getAttributes().entrySet()) { - group.set((AttributeKey) entry.getKey(), entry.getValue()); - } - for (Figure f : sorted) { - ODGPathFigure path = (ODGPathFigure) f; - for (Figure child : path.getChildren()) { - group.basicAdd(child); - } - } - group.changed(); - view.addToSelection(group); - } -} +} \ No newline at end of file