From 87ac6958bef6686397ac7808c40ac841bd3b5c4b Mon Sep 17 00:00:00 2001 From: Gierka36 Date: Tue, 9 Jun 2026 15:44:48 +0200 Subject: [PATCH 1/5] Lab: Continuous Integration Setup --- .github/workflows/maven.yml | 27 +++++++++++++++++++++++++++ .maven-settings.xml | 12 ++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 .github/workflows/maven.yml create mode 100644 .maven-settings.xml diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 000000000..65c3cf7d0 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,27 @@ +name: Java CI with Maven + +on: + pull_request: + branches: [ "develop", "main" ] + push: + branches: [ "develop", "main" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + cache: maven + + - name: Build and test with Maven + run: mvn --batch-mode --update-snapshots verify --settings .maven-settings.xml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.maven-settings.xml b/.maven-settings.xml new file mode 100644 index 000000000..ee1c29924 --- /dev/null +++ b/.maven-settings.xml @@ -0,0 +1,12 @@ + + + + github + ${env.GITHUB_ACTOR} + ${env.GITHUB_TOKEN} + + + From b0c37374bfaea5b4f31e7a0c2455afbaf20aeac4 Mon Sep 17 00:00:00 2001 From: Gierka36 Date: Tue, 9 Jun 2026 16:32:52 +0200 Subject: [PATCH 2/5] Refactoring lab4 --- .../action/edit/AbstractUndoRedoAction.java | 100 ++++++++++++++++++ .../org/jhotdraw/action/edit/RedoAction.java | 89 ++-------------- .../org/jhotdraw/action/edit/UndoAction.java | 86 ++------------- 3 files changed, 115 insertions(+), 160 deletions(-) create mode 100644 jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/AbstractUndoRedoAction.java diff --git a/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/AbstractUndoRedoAction.java b/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/AbstractUndoRedoAction.java new file mode 100644 index 000000000..05c995c6c --- /dev/null +++ b/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/AbstractUndoRedoAction.java @@ -0,0 +1,100 @@ +/* + * @(#)AbstractUndoRedoAction.java + * + * Copyright (c) 1996-2010 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.action.edit; + +import java.awt.event.*; +import java.beans.*; +import javax.swing.*; +import org.jhotdraw.action.AbstractViewAction; +import org.jhotdraw.api.app.Application; +import org.jhotdraw.api.app.View; +import org.jhotdraw.util.ResourceBundleUtil; + +/** + * Abstract base for {@link UndoAction} and {@link RedoAction}. + *

+ * Both actions share identical listener management, enabled-state tracking, + * and delegation logic. The only variation is the action ID, supplied by + * subclasses via {@link #getActionId()}. + */ +abstract class AbstractUndoRedoAction extends AbstractViewAction { + + private static final long serialVersionUID = 1L; + protected final ResourceBundleUtil labels = + ResourceBundleUtil.getBundle("org.jhotdraw.action.Labels"); + + private final PropertyChangeListener actionPropertyListener = new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + String name = evt.getPropertyName(); + if ((name == null && AbstractAction.NAME == null) + || (name != null && name.equals(AbstractAction.NAME))) { + putValue(AbstractAction.NAME, evt.getNewValue()); + } else if ("enabled".equals(name)) { + updateEnabledState(); + } + } + }; + + public AbstractUndoRedoAction(Application app, View view) { + super(app, view); + } + + /** + * Returns the action map key that identifies this action within a view + * (e.g. {@code "edit.undo"} or {@code "edit.redo"}). + */ + protected abstract String getActionId(); + + protected void updateEnabledState() { + Action realAction = getRealAction(); + setEnabled(realAction != null && realAction != this && realAction.isEnabled()); + } + + @Override + protected void updateView(View oldValue, View newValue) { + super.updateView(oldValue, newValue); + if (newValue != null + && newValue.getActionMap().get(getActionId()) != null + && newValue.getActionMap().get(getActionId()) != this) { + putValue(AbstractAction.NAME, + newValue.getActionMap().get(getActionId()).getValue(AbstractAction.NAME)); + updateEnabledState(); + } + } + + @Override + protected void installViewListeners(View p) { + super.installViewListeners(p); + Action actionInView = p.getActionMap().get(getActionId()); + if (actionInView != null && actionInView != this) { + actionInView.addPropertyChangeListener(actionPropertyListener); + } + } + + @Override + protected void uninstallViewListeners(View p) { + super.uninstallViewListeners(p); + Action actionInView = p.getActionMap().get(getActionId()); + if (actionInView != null && actionInView != this) { + actionInView.removePropertyChangeListener(actionPropertyListener); + } + } + + @Override + public void actionPerformed(ActionEvent e) { + Action realAction = getRealAction(); + if (realAction != null && realAction != this) { + realAction.actionPerformed(e); + } + } + + private Action getRealAction() { + return (getActiveView() == null) ? null : getActiveView().getActionMap().get(getActionId()); + } +} diff --git a/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/RedoAction.java b/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/RedoAction.java index 794ab08c8..32fa600b5 100644 --- a/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/RedoAction.java +++ b/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/RedoAction.java @@ -7,110 +7,37 @@ */ package org.jhotdraw.action.edit; -import java.awt.event.*; -import java.beans.*; -import javax.swing.*; -import org.jhotdraw.action.AbstractViewAction; import org.jhotdraw.api.app.Application; import org.jhotdraw.api.app.View; -import org.jhotdraw.util.*; /** * Redoes the last user action on the active view. *

- * This action requires that the View returns a project - * specific redo action when invoking getActionMap("redo") on a View. + * This action requires that the View returns a project specific redo action + * when invoking getActionMap("edit.redo") on a View. *

- * This action is called when the user selects the Redo item in the Edit - * menu. The menu item is automatically created by the application. + * This action is called when the user selects the Redo item in the Edit menu. + * The menu item is automatically created by the application. *

* If you want this behavior in your application, you have to create an action * with this ID and put it in your {@code ApplicationModel} in method * {@link org.jhotdraw.app.ApplicationModel#initApplication}. * - * * @author Werner Randelshofer * @version $Id$ */ -public class RedoAction extends AbstractViewAction { +public class RedoAction extends AbstractUndoRedoAction { private static final long serialVersionUID = 1L; public static final String ID = "edit.redo"; - private ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.action.Labels"); - private PropertyChangeListener redoActionPropertyListener = new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - String name = evt.getPropertyName(); - if ((name == null && AbstractAction.NAME == null) || (name != null && name.equals(AbstractAction.NAME))) { - putValue(AbstractAction.NAME, evt.getNewValue()); - } else if ("enabled".equals(name)) { - updateEnabledState(); - } - } - }; - /** - * Creates a new instance. - */ public RedoAction(Application app, View view) { super(app, view); labels.configureAction(this, ID); } - protected void updateEnabledState() { - boolean isEnabled = false; - Action realRedoAction = getRealRedoAction(); - if (realRedoAction != null && realRedoAction != this) { - isEnabled = realRedoAction.isEnabled(); - } - setEnabled(isEnabled); - } - - @Override - protected void updateView(View oldValue, View newValue) { - super.updateView(oldValue, newValue); - if (newValue != null - && newValue.getActionMap().get(ID) != null - && newValue.getActionMap().get(ID) != this) { - putValue(AbstractAction.NAME, newValue.getActionMap().get(ID). - getValue(AbstractAction.NAME)); - updateEnabledState(); - } - } - - /** - * Installs listeners on the view object. - */ - @Override - protected void installViewListeners(View p) { - super.installViewListeners(p); - Action redoActionInView = p.getActionMap().get(ID); - if (redoActionInView != null && redoActionInView != this) { - redoActionInView.addPropertyChangeListener(redoActionPropertyListener); - } - } - - /** - * Installs listeners on the view object. - */ @Override - protected void uninstallViewListeners(View p) { - super.uninstallViewListeners(p); - Action redoActionInView = p.getActionMap().get(ID); - if (redoActionInView != null && redoActionInView != this) { - redoActionInView.removePropertyChangeListener(redoActionPropertyListener); - } - } - - @Override - public void actionPerformed(ActionEvent e) { - Action realAction = getRealRedoAction(); - if (realAction != null && realAction != this) { - realAction.actionPerformed(e); - } - } - - private Action getRealRedoAction() { - return (getActiveView() == null) ? null : getActiveView().getActionMap().get(ID); + protected String getActionId() { + return ID; } -} +} \ No newline at end of file diff --git a/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/UndoAction.java b/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/UndoAction.java index 74c4f2cea..5f10e4540 100644 --- a/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/UndoAction.java +++ b/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/UndoAction.java @@ -7,22 +7,17 @@ */ package org.jhotdraw.action.edit; -import java.awt.event.*; -import java.beans.*; -import javax.swing.*; -import org.jhotdraw.action.AbstractViewAction; import org.jhotdraw.api.app.Application; import org.jhotdraw.api.app.View; -import org.jhotdraw.util.*; /** * Undoes the last user action. *

- * This action requires that the View returns a project - * specific undo action when invoking getActionMap("redo") on a View. + * This action requires that the View returns a project specific undo action + * when invoking getActionMap("edit.undo") on a View. *

- * This action is called when the user selects the Undo item in the Edit - * menu. The menu item is automatically created by the application. + * This action is called when the user selects the Undo item in the Edit menu. + * The menu item is automatically created by the application. *

* If you want this behavior in your application, you have to create an action * with this ID and put it in your {@code ApplicationModel} in method @@ -31,85 +26,18 @@ * @author Werner Randelshofer * @version $Id$ */ -public class UndoAction extends AbstractViewAction { +public class UndoAction extends AbstractUndoRedoAction { private static final long serialVersionUID = 1L; public static final String ID = "edit.undo"; - private ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.action.Labels"); - private PropertyChangeListener redoActionPropertyListener = new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - String name = evt.getPropertyName(); - if ((name == null && AbstractAction.NAME == null) || (name != null && name.equals(AbstractAction.NAME))) { - putValue(AbstractAction.NAME, evt.getNewValue()); - } else if ("enabled".equals(name)) { - updateEnabledState(); - } - } - }; - /** - * Creates a new instance. - */ public UndoAction(Application app, View view) { super(app, view); labels.configureAction(this, ID); } - protected void updateEnabledState() { - boolean isEnabled = false; - Action realAction = getRealUndoAction(); - if (realAction != null && realAction != this) { - isEnabled = realAction.isEnabled(); - } - setEnabled(isEnabled); - } - @Override - protected void updateView(View oldValue, View newValue) { - super.updateView(oldValue, newValue); - if (newValue != null - && newValue.getActionMap().get(ID) != null - && newValue.getActionMap().get(ID) != this) { - putValue(AbstractAction.NAME, newValue.getActionMap().get(ID). - getValue(AbstractAction.NAME)); - updateEnabledState(); - } - } - - /** - * Installs listeners on the view object. - */ - @Override - protected void installViewListeners(View p) { - super.installViewListeners(p); - Action undoActionInView = p.getActionMap().get(ID); - if (undoActionInView != null && undoActionInView != this) { - undoActionInView.addPropertyChangeListener(redoActionPropertyListener); - } - } - - /** - * Installs listeners on the view object. - */ - @Override - protected void uninstallViewListeners(View p) { - super.uninstallViewListeners(p); - Action undoActionInView = p.getActionMap().get(ID); - if (undoActionInView != null && undoActionInView != this) { - undoActionInView.removePropertyChangeListener(redoActionPropertyListener); - } - } - - @Override - public void actionPerformed(ActionEvent e) { - Action realUndoAction = getRealUndoAction(); - if (realUndoAction != null && realUndoAction != this) { - realUndoAction.actionPerformed(e); - } - } - - private Action getRealUndoAction() { - return (getActiveView() == null) ? null : getActiveView().getActionMap().get(ID); + protected String getActionId() { + return ID; } } From cab66888853352b3f86476e5a63858882f141316 Mon Sep 17 00:00:00 2001 From: Gierka36 Date: Tue, 9 Jun 2026 18:30:53 +0200 Subject: [PATCH 3/5] Testing --- jhotdraw-core/pom.xml | 12 ++ .../draw/event/AttributeChangeEditTest.java | 65 ++++++++ .../draw/event/TransformEditTest.java | 85 +++++++++++ .../org/jhotdraw/undo/CompositeEditTest.java | 60 ++++++++ .../jhotdraw/undo/NonUndoableEditTest.java | 19 +++ .../jhotdraw/undo/UndoRedoManagerTest.java | 143 ++++++++++++++++++ .../java/org/jhotdraw/undo/CompositeEdit.java | 10 +- .../org/jhotdraw/undo/UndoRedoManager.java | 5 + 8 files changed, 391 insertions(+), 8 deletions(-) create mode 100644 jhotdraw-core/src/test/java/org/jhotdraw/draw/event/AttributeChangeEditTest.java create mode 100644 jhotdraw-core/src/test/java/org/jhotdraw/draw/event/TransformEditTest.java create mode 100644 jhotdraw-core/src/test/java/org/jhotdraw/undo/CompositeEditTest.java create mode 100644 jhotdraw-core/src/test/java/org/jhotdraw/undo/NonUndoableEditTest.java create mode 100644 jhotdraw-core/src/test/java/org/jhotdraw/undo/UndoRedoManagerTest.java diff --git a/jhotdraw-core/pom.xml b/jhotdraw-core/pom.xml index 7c276da85..4700e5495 100644 --- a/jhotdraw-core/pom.xml +++ b/jhotdraw-core/pom.xml @@ -35,6 +35,18 @@ 6.8.21 test + + junit + junit + 4.13.2 + test + + + org.mockito + mockito-core + 4.11.0 + test + ${project.groupId} jhotdraw-actions diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/event/AttributeChangeEditTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/event/AttributeChangeEditTest.java new file mode 100644 index 000000000..314bb56b8 --- /dev/null +++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/event/AttributeChangeEditTest.java @@ -0,0 +1,65 @@ +package org.jhotdraw.draw.event; + +import org.jhotdraw.draw.AttributeKey; +import org.jhotdraw.draw.figure.Figure; +import org.junit.Before; +import org.junit.Test; +import static org.mockito.Mockito.*; + +public class AttributeChangeEditTest { + + private Figure mockFigure; + private AttributeKey key; + private AttributeChangeEdit edit; + + @Before + public void setUp() { + mockFigure = mock(Figure.class); + key = new AttributeKey<>("testAttribute", String.class, null); + edit = new AttributeChangeEdit<>(mockFigure, key, "oldValue", "newValue"); + } + + // ----------------------------------------------------------------------- + // Best case scenarios + // ----------------------------------------------------------------------- + + @Test + public void testUndo_restoresOldAttributeValue() { + edit.undo(); + verify(mockFigure).set(key, "oldValue"); + } + + @Test + public void testRedo_appliesNewAttributeValue() { + edit.undo(); + edit.redo(); + verify(mockFigure).set(key, "newValue"); + } + + // ----------------------------------------------------------------------- + // Boundary cases — lifecycle callbacks + // ----------------------------------------------------------------------- + + @Test + public void testUndo_callsWillChangeBeforeSet() { + edit.undo(); + // willChange() must be called before the figure is mutated + verify(mockFigure).willChange(); + verify(mockFigure).set(key, "oldValue"); + } + + @Test + public void testUndo_callsChangedAfterSet() { + edit.undo(); + verify(mockFigure).changed(); + } + + @Test + public void testRedo_callsWillChangeAndChangedTwiceTotal() { + edit.undo(); + edit.redo(); + // Both undo and redo call willChange()/changed() + verify(mockFigure, times(2)).willChange(); + verify(mockFigure, times(2)).changed(); + } +} \ No newline at end of file diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/event/TransformEditTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/event/TransformEditTest.java new file mode 100644 index 000000000..0c133f029 --- /dev/null +++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/event/TransformEditTest.java @@ -0,0 +1,85 @@ +package org.jhotdraw.draw.event; + +import org.jhotdraw.draw.figure.Figure; +import java.awt.geom.AffineTransform; +import java.util.LinkedList; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class TransformEditTest { + + private Figure mockFigure; + private AffineTransform translation; + private TransformEdit edit; + + @Before + public void setUp() { + mockFigure = mock(Figure.class); + // A pure translation is lossless and invertible — the correct use case for TransformEdit + translation = AffineTransform.getTranslateInstance(10, 20); + edit = new TransformEdit(mockFigure, translation); + } + + // ----------------------------------------------------------------------- + // Best case scenarios + // ----------------------------------------------------------------------- + + @Test + public void testUndo_callsTransformOnFigure() { + edit.undo(); + verify(mockFigure).transform(any(AffineTransform.class)); + } + + @Test + public void testRedo_afterUndo_callsTransformTwice() { + edit.undo(); + edit.redo(); + verify(mockFigure, times(2)).transform(any(AffineTransform.class)); + } + + @Test + public void testUndo_callsWillChangeAndChanged() { + edit.undo(); + verify(mockFigure).willChange(); + verify(mockFigure).changed(); + } + + // ----------------------------------------------------------------------- + // Boundary cases + // ----------------------------------------------------------------------- + + @Test + public void testAddEdit_sameFigureCollection_mergesTransforms() { + // Both edits must share the same Collection reference to merge + List

sharedFigures = new LinkedList<>(); + sharedFigures.add(mockFigure); + + AffineTransform tx1 = AffineTransform.getTranslateInstance(10, 0); + AffineTransform tx2 = AffineTransform.getTranslateInstance(5, 0); + + TransformEdit edit1 = new TransformEdit(sharedFigures, tx1); + TransformEdit edit2 = new TransformEdit(sharedFigures, tx2); + + boolean merged = edit1.addEdit(edit2); + assertTrue(merged); + } + + @Test + public void testAddEdit_differentFigureCollection_doesNotMerge() { + // Different collection references — merging must be refused + List
figures1 = new LinkedList<>(); + figures1.add(mockFigure); + List
figures2 = new LinkedList<>(); + figures2.add(mockFigure); + + TransformEdit edit1 = new TransformEdit(figures1, translation); + TransformEdit edit2 = new TransformEdit(figures2, translation); + + boolean merged = edit1.addEdit(edit2); + assertFalse(merged); + } +} \ No newline at end of file diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/undo/CompositeEditTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/undo/CompositeEditTest.java new file mode 100644 index 000000000..32091c475 --- /dev/null +++ b/jhotdraw-core/src/test/java/org/jhotdraw/undo/CompositeEditTest.java @@ -0,0 +1,60 @@ +package org.jhotdraw.undo; + +import javax.swing.undo.AbstractUndoableEdit; +import org.junit.Test; +import static org.junit.Assert.*; + +public class CompositeEditTest { + + // ----------------------------------------------------------------------- + // Best case scenarios + // ----------------------------------------------------------------------- + + @Test + public void testNewCompositeEdit_isInProgress() { + CompositeEdit edit = new CompositeEdit(); + assertTrue(edit.isInProgress()); + } + + @Test + public void testAddNormalEdit_whileInProgress_remainsOpen() { + CompositeEdit edit = new CompositeEdit("test"); + edit.addEdit(new AbstractUndoableEdit() { + private static final long serialVersionUID = 1L; + }); + assertTrue(edit.isInProgress()); + } + + @Test + public void testPresentationName_whenSet_isReturned() { + CompositeEdit edit = new CompositeEdit("My Operation"); + assertEquals("My Operation", edit.getPresentationName()); + } + + // ----------------------------------------------------------------------- + // Boundary cases + // ----------------------------------------------------------------------- + + @Test + public void testAddEditToItself_endsComposite() { + CompositeEdit edit = new CompositeEdit(); + edit.addEdit(edit); + assertFalse(edit.isInProgress()); + } + + @Test + public void testIsSignificant_whenConstructedFalse_returnsFalse() { + CompositeEdit edit = new CompositeEdit(false); + assertFalse(edit.isSignificant()); + } + + @Test + public void testIsSignificant_whenConstructedTrue_defaultsTrue() { + CompositeEdit edit = new CompositeEdit(true); + // A composite with no children has no significant children, + // so CompoundEdit.isSignificant() returns false even with flag=true. + // Verify that the flag alone does not override child significance. + edit.end(); + assertFalse(edit.isSignificant()); + } +} diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/undo/NonUndoableEditTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/undo/NonUndoableEditTest.java new file mode 100644 index 000000000..7e1e5c765 --- /dev/null +++ b/jhotdraw-core/src/test/java/org/jhotdraw/undo/NonUndoableEditTest.java @@ -0,0 +1,19 @@ +package org.jhotdraw.undo; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class NonUndoableEditTest { + + @Test + public void testCanUndo_alwaysReturnsFalse() { + NonUndoableEdit edit = new NonUndoableEdit(); + assertFalse(edit.canUndo()); + } + + @Test + public void testCanRedo_alwaysReturnsFalse() { + NonUndoableEdit edit = new NonUndoableEdit(); + assertFalse(edit.canRedo()); + } +} \ No newline at end of file diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/undo/UndoRedoManagerTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/undo/UndoRedoManagerTest.java new file mode 100644 index 000000000..94ec287cb --- /dev/null +++ b/jhotdraw-core/src/test/java/org/jhotdraw/undo/UndoRedoManagerTest.java @@ -0,0 +1,143 @@ +package org.jhotdraw.undo; + +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; + +public class UndoRedoManagerTest { + + private UndoRedoManager manager; + + @Before + public void setUp() { + manager = new UndoRedoManager(); + } + + // ----------------------------------------------------------------------- + // Best case scenarios + // ----------------------------------------------------------------------- + + @Test + public void testAddEdit_canUndoBecomesTrue() { + manager.addEdit(new SimpleEdit()); + assertTrue(manager.canUndo()); + } + + @Test + public void testUndo_canRedoBecomesTrue() throws CannotUndoException { + manager.addEdit(new SimpleEdit()); + manager.undo(); + assertTrue(manager.canRedo()); + } + + @Test + public void testUndo_canUndoBecomesFalse() throws CannotUndoException { + manager.addEdit(new SimpleEdit()); + manager.undo(); + assertFalse(manager.canUndo()); + } + + @Test + public void testRedo_afterUndo_canUndoAgain() throws CannotUndoException, CannotRedoException { + manager.addEdit(new SimpleEdit()); + manager.undo(); + manager.redo(); + assertTrue(manager.canUndo()); + } + + @Test + public void testHasSignificantEdits_afterSignificantEdit_isTrue() { + manager.addEdit(new SimpleEdit()); + assertTrue(manager.hasSignificantEdits()); + } + + @Test + public void testUndoAction_enablesAfterAddEdit() { + assertFalse(manager.getUndoAction().isEnabled()); + manager.addEdit(new SimpleEdit()); + assertTrue(manager.getUndoAction().isEnabled()); + } + + @Test + public void testRedoAction_enablesAfterUndoEdit() throws CannotUndoException { + assertFalse(manager.getRedoAction().isEnabled()); + manager.addEdit(new SimpleEdit()); + manager.undo(); + assertTrue(manager.getRedoAction().isEnabled()); + } + + // ----------------------------------------------------------------------- + // Boundary cases + // ----------------------------------------------------------------------- + + @Test(expected = CannotUndoException.class) + public void testUndo_emptyStack_throwsCannotUndoException() { + manager.undo(); + } + + @Test(expected = CannotRedoException.class) + public void testRedo_nothingUndone_throwsCannotRedoException() { + manager.redo(); + } + + @Test + public void testDiscardAllEdits_canUndo_becomesFalse() { + manager.addEdit(new SimpleEdit()); + manager.discardAllEdits(); + assertFalse(manager.canUndo()); + } + + @Test + public void testDiscardAllEdits_canRedo_becomesFalse() throws CannotUndoException { + manager.addEdit(new SimpleEdit()); + manager.undo(); + manager.discardAllEdits(); + assertFalse(manager.canRedo()); + } + + @Test + public void testDiscardAllEdits_hasSignificantEdits_becomesFalse() { + manager.addEdit(new SimpleEdit()); + manager.discardAllEdits(); + assertFalse(manager.hasSignificantEdits()); + } + + @Test + public void testHasSignificantEdits_nonSignificantEdit_staysFalse() { + manager.addEdit(new NonSignificantEdit()); + assertFalse(manager.hasSignificantEdits()); + } + + @Test + public void testAddDiscardAllEdits_disablesUndoAndRedo() { + manager.addEdit(new SimpleEdit()); + manager.addEdit(UndoRedoManager.DISCARD_ALL_EDITS); + assertFalse(manager.canUndo()); + assertFalse(manager.canRedo()); + } + + // ----------------------------------------------------------------------- + // Helper stubs + // ----------------------------------------------------------------------- + + private static class SimpleEdit extends AbstractUndoableEdit { + private static final long serialVersionUID = 1L; + + @Override + public String getPresentationName() { + return "Simple Edit"; + } + } + + private static class NonSignificantEdit extends AbstractUndoableEdit { + private static final long serialVersionUID = 1L; + + @Override + public boolean isSignificant() { + return false; + } + } +} \ No newline at end of file diff --git a/jhotdraw-utils/src/main/java/org/jhotdraw/undo/CompositeEdit.java b/jhotdraw-utils/src/main/java/org/jhotdraw/undo/CompositeEdit.java index dc097d75f..cef7916f9 100644 --- a/jhotdraw-utils/src/main/java/org/jhotdraw/undo/CompositeEdit.java +++ b/jhotdraw-utils/src/main/java/org/jhotdraw/undo/CompositeEdit.java @@ -37,12 +37,6 @@ public class CompositeEdit extends CompoundEdit { private static final long serialVersionUID = 1L; private String presentationName; private boolean isSignificant; - private boolean isVerbose; - - public void setVerbose(boolean b) { - isVerbose = b; - } - /** * Creates a new {@code CompositeEdit} which uses CompoundEdit#getPresentationName() * and is significant.. @@ -144,6 +138,7 @@ public String getRedoPresentationName() { */ @Override public boolean addEdit(UndoableEdit anEdit) { + assert anEdit != null : "Cannot add a null edit to CompositeEdit"; //NOSONAR java:S4274 if (anEdit == this) { end(); return true; @@ -161,8 +156,7 @@ public boolean addEdit(UndoableEdit anEdit) { */ @Override public boolean isSignificant() { - return (isSignificant) ? super.isSignificant() : false; - //return isSignificant; + return isSignificant && super.isSignificant(); } public void setSignificant(boolean newValue) { diff --git a/jhotdraw-utils/src/main/java/org/jhotdraw/undo/UndoRedoManager.java b/jhotdraw-utils/src/main/java/org/jhotdraw/undo/UndoRedoManager.java index 40ac0b4ce..0d3f44a03 100644 --- a/jhotdraw-utils/src/main/java/org/jhotdraw/undo/UndoRedoManager.java +++ b/jhotdraw-utils/src/main/java/org/jhotdraw/undo/UndoRedoManager.java @@ -187,6 +187,9 @@ public boolean hasSignificantEdits() { */ @Override public boolean addEdit(UndoableEdit anEdit) { + // Invariant: callers must never pass null. + // assert is intentional (enabled with -ea) to catch programming errors during development. //NOSONAR java:S4274 + assert anEdit != null : "UndoableEdit must not be null"; if (DEBUG) { System.out.println("UndoRedoManager@" + hashCode() + ".add " + anEdit); } @@ -255,6 +258,7 @@ private void updateActions() { @Override public void undo() throws CannotUndoException { + assert canUndo() : "undo() called with nothing to undo — check canUndo() first"; //NOSONAR java:S4274 undoOrRedoInProgress = true; try { super.undo(); @@ -272,6 +276,7 @@ public void undo() @Override public void redo() throws CannotUndoException { + assert canRedo() : "redo() called with nothing to redo — check canRedo() first"; //NOSONAR java:S4274 undoOrRedoInProgress = true; try { super.redo(); From cb6f2b61adefa9ae198cb5bd2537d0554830d854 Mon Sep 17 00:00:00 2001 From: Gierka36 Date: Wed, 10 Jun 2026 17:36:10 +0200 Subject: [PATCH 4/5] BDD tests --- jhotdraw-core/pom.xml | 12 + .../org/jhotdraw/undo/UndoRedoBddTest.java | 434 ++++++++++++++++++ 2 files changed, 446 insertions(+) create mode 100644 jhotdraw-core/src/test/java/org/jhotdraw/undo/UndoRedoBddTest.java diff --git a/jhotdraw-core/pom.xml b/jhotdraw-core/pom.xml index 4700e5495..192f5899d 100644 --- a/jhotdraw-core/pom.xml +++ b/jhotdraw-core/pom.xml @@ -47,6 +47,18 @@ 4.11.0 test + + com.tngtech.jgiven + jgiven-junit + 1.2.5 + test + + + org.assertj + assertj-core + 3.24.2 + test + ${project.groupId} jhotdraw-actions diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/undo/UndoRedoBddTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/undo/UndoRedoBddTest.java new file mode 100644 index 000000000..ae356ec5d --- /dev/null +++ b/jhotdraw-core/src/test/java/org/jhotdraw/undo/UndoRedoBddTest.java @@ -0,0 +1,434 @@ +package org.jhotdraw.undo; + +import com.tngtech.jgiven.Stage; +import com.tngtech.jgiven.annotation.ExpectedScenarioState; +import com.tngtech.jgiven.annotation.ProvidedScenarioState; +import com.tngtech.jgiven.junit.ScenarioTest; +import java.util.ArrayList; +import java.util.List; +import javax.swing.Action; +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Behavior-Driven tests for Undo/Redo — TestLab2. + * + * User story: "As a drawing application user, I want to undo and redo + * drawing actions so that I can correct mistakes and restore reverted changes." + * + * Each test method maps one Acceptance Criterion to a Given–When–Then scenario + * automated with JGiven and verified with AssertJ. + */ +public class UndoRedoBddTest extends ScenarioTest< + UndoRedoBddTest.GivenUndoManager, + UndoRedoBddTest.WhenUndoRedo, + UndoRedoBddTest.ThenUndoRedo> { + + // ========================================================================= + // AC1 — Undo Action Execution + // ========================================================================= + + @Test + public void ac1_undo_reverts_the_most_recent_drawing_action() { + given().a_drawing_action_named_$_has_been_performed("Create Rectangle"); + when().the_user_triggers_undo(); + then().the_edit_is_reverted() + .and().redo_is_available(); + } + + // ========================================================================= + // AC2 — Redo Action Execution + // ========================================================================= + + @Test + public void ac2_redo_reapplies_the_most_recently_undone_action() { + given().a_drawing_action_named_$_has_been_performed("Move Shape") + .and().the_action_has_been_undone(); + when().the_user_triggers_redo(); + then().the_edit_is_reapplied() + .and().undo_is_available(); + } + + // ========================================================================= + // AC3 — Undo/Redo History Stack (configurable limit) + // ========================================================================= + + @Test + public void ac3_oldest_edit_is_discarded_when_history_limit_is_reached() { + given().a_fresh_manager_with_limit_$(2) + .and().$_drawing_actions_have_been_performed(3); + when().the_user_undoes_all_available_edits(); + then().exactly_$_edits_were_undone(2); + } + + // ========================================================================= + // AC4a — Visual Feedback: Undo button disabled when stack is empty + // ========================================================================= + + @Test + public void ac4_undo_action_is_disabled_when_there_is_nothing_to_undo() { + given().a_fresh_undo_manager(); + when().the_user_observes_the_toolbar(); + then().the_undo_action_is_disabled(); + } + + // ========================================================================= + // AC4b — Visual Feedback: Undo button enabled with tooltip after an edit + // ========================================================================= + + @Test + public void ac4_undo_action_is_enabled_and_shows_action_name_after_edit() { + given().a_drawing_action_named_$_has_been_performed("Create Rectangle"); + when().the_user_observes_the_toolbar(); + then().the_undo_action_is_enabled() + .and().the_undo_action_tooltip_contains("Create Rectangle"); + } + + // ========================================================================= + // AC4c — Visual Feedback: Redo button disabled when nothing has been undone + // ========================================================================= + + @Test + public void ac4_redo_action_is_disabled_when_nothing_has_been_undone() { + given().a_fresh_undo_manager(); + when().the_user_observes_the_toolbar(); + then().the_redo_action_is_disabled(); + } + + // ========================================================================= + // AC4d — Visual Feedback: Redo button enabled after an undo + // ========================================================================= + + @Test + public void ac4_redo_action_is_enabled_after_an_undo() { + given().a_drawing_action_named_$_has_been_performed("Create Rectangle") + .and().the_action_has_been_undone(); + when().the_user_observes_the_toolbar(); + then().the_redo_action_is_enabled(); + } + + // ========================================================================= + // AC5 — Action Grouping (CompositeEdit) + // ========================================================================= + + @Test + public void ac5_sequential_similar_actions_grouped_into_one_undo_step() { + given().a_composite_of_$_sub_edits_named_$(3, "Move Shape"); + when().the_user_triggers_undo(); + then().all_$_grouped_edits_are_reverted(3) + .and().undo_is_not_available(); + } + + // ========================================================================= + // AC6 — Redo Stack Clearing on New Action + // ========================================================================= + + @Test + public void ac6_redo_stack_is_cleared_when_new_action_is_performed_after_undo() { + given().a_drawing_action_named_$_has_been_performed("Create Shape A") + .and().the_action_has_been_undone(); + when().a_new_action_named_$_is_performed("Create Shape B"); + then().redo_is_not_available(); + } + + // ========================================================================= + // AC7 — Undo/Redo with Multiple Objects + // ========================================================================= + + @Test + public void ac7_undo_atomically_reverts_changes_to_multiple_selected_objects() { + given().a_group_edit_modifying_$_objects(3); + when().the_user_triggers_undo(); + then().all_$_grouped_edits_are_reverted(3); + } + + // ========================================================================= + // AC8 — Persistent State After Save + // ========================================================================= + + @Test + public void ac8_undo_redo_history_is_cleared_after_saving() { + given().a_fresh_undo_manager() + .and().$_drawing_actions_have_been_performed(3); + when().the_drawing_is_saved(); + then().undo_is_not_available() + .and().redo_is_not_available(); + } + + // ========================================================================= + // Stage: Given + // ========================================================================= + + public static class GivenUndoManager extends Stage { + + @ProvidedScenarioState + UndoRedoManager manager; + + @ProvidedScenarioState + TrackableEdit lastEdit; + + @ProvidedScenarioState + List groupedEdits = new ArrayList<>(); + + public GivenUndoManager a_fresh_undo_manager() { + manager = new UndoRedoManager(); + return self(); + } + + public GivenUndoManager a_fresh_manager_with_limit_$(int limit) { + manager = new UndoRedoManager(); + manager.setLimit(limit); + return self(); + } + + public GivenUndoManager a_drawing_action_named_$_has_been_performed(String name) { + if (manager == null) { + manager = new UndoRedoManager(); + } + lastEdit = new TrackableEdit(name); + manager.addEdit(lastEdit); + return self(); + } + + public GivenUndoManager the_action_has_been_undone() { + manager.undo(); + return self(); + } + + public GivenUndoManager $_drawing_actions_have_been_performed(int count) { + for (int i = 1; i <= count; i++) { + manager.addEdit(new TrackableEdit("Edit " + i)); + } + return self(); + } + + /** + * Creates a CompositeEdit grouping {@code count} sub-edits so that + * a single undo reverts all of them (AC5). + * + * Pattern: fire composite once to open it, fire sub-edits, fire the + * same composite instance again to close it (see CompositeEdit Javadoc). + */ + public GivenUndoManager a_composite_of_$_sub_edits_named_$(int count, String name) { + manager = new UndoRedoManager(); + groupedEdits = new ArrayList<>(); + CompositeEdit composite = new CompositeEdit(name); + manager.addEdit(composite); + for (int i = 0; i < count; i++) { + TrackableEdit sub = new TrackableEdit(name + " step " + (i + 1)); + groupedEdits.add(sub); + manager.addEdit(sub); + } + manager.addEdit(composite); + return self(); + } + + /** + * Creates a CompositeEdit wrapping simultaneous edits to N objects (AC7). + */ + public GivenUndoManager a_group_edit_modifying_$_objects(int objectCount) { + manager = new UndoRedoManager(); + groupedEdits = new ArrayList<>(); + CompositeEdit group = new CompositeEdit("Move " + objectCount + " Objects"); + manager.addEdit(group); + for (int i = 0; i < objectCount; i++) { + TrackableEdit edit = new TrackableEdit("Object " + (i + 1) + " change"); + groupedEdits.add(edit); + manager.addEdit(edit); + } + manager.addEdit(group); + return self(); + } + } + + // ========================================================================= + // Stage: When + // ========================================================================= + + public static class WhenUndoRedo extends Stage { + + @ExpectedScenarioState + UndoRedoManager manager; + + @ProvidedScenarioState + int successfulUndoCount; + + public WhenUndoRedo the_user_triggers_undo() { + manager.undo(); + return self(); + } + + public WhenUndoRedo the_user_triggers_redo() { + manager.redo(); + return self(); + } + + public WhenUndoRedo the_user_undoes_all_available_edits() { + successfulUndoCount = 0; + while (manager.canUndo()) { + manager.undo(); + successfulUndoCount++; + } + return self(); + } + + public WhenUndoRedo the_user_observes_the_toolbar() { + return self(); + } + + public WhenUndoRedo a_new_action_named_$_is_performed(String name) { + manager.addEdit(new TrackableEdit(name)); + return self(); + } + + public WhenUndoRedo the_drawing_is_saved() { + manager.discardAllEdits(); + return self(); + } + } + + // ========================================================================= + // Stage: Then + // ========================================================================= + + public static class ThenUndoRedo extends Stage { + + @ExpectedScenarioState + UndoRedoManager manager; + + @ExpectedScenarioState + TrackableEdit lastEdit; + + @ExpectedScenarioState + List groupedEdits; + + @ExpectedScenarioState + int successfulUndoCount; + + public ThenUndoRedo undo_is_available() { + assertThat(manager.canUndo()) + .as("undo should be available").isTrue(); + return self(); + } + + public ThenUndoRedo undo_is_not_available() { + assertThat(manager.canUndo()) + .as("undo should not be available").isFalse(); + return self(); + } + + public ThenUndoRedo redo_is_available() { + assertThat(manager.canRedo()) + .as("redo should be available").isTrue(); + return self(); + } + + public ThenUndoRedo redo_is_not_available() { + assertThat(manager.canRedo()) + .as("redo should not be available").isFalse(); + return self(); + } + + public ThenUndoRedo the_edit_is_reverted() { + assertThat(lastEdit.undoCount) + .as("edit undo invocation count").isEqualTo(1); + return self(); + } + + public ThenUndoRedo the_edit_is_reapplied() { + assertThat(lastEdit.redoCount) + .as("edit redo invocation count").isEqualTo(1); + return self(); + } + + public ThenUndoRedo exactly_$_edits_were_undone(int expected) { + assertThat(successfulUndoCount) + .as("number of edits successfully undone").isEqualTo(expected); + return self(); + } + + public ThenUndoRedo the_undo_action_is_disabled() { + assertThat(manager.getUndoAction().isEnabled()) + .as("undo Action.isEnabled()").isFalse(); + return self(); + } + + public ThenUndoRedo the_undo_action_is_enabled() { + assertThat(manager.getUndoAction().isEnabled()) + .as("undo Action.isEnabled()").isTrue(); + return self(); + } + + public ThenUndoRedo the_undo_action_tooltip_contains(String text) { + String tooltip = (String) manager.getUndoAction() + .getValue(Action.SHORT_DESCRIPTION); + assertThat(tooltip) + .as("undo action tooltip").contains(text); + return self(); + } + + public ThenUndoRedo the_redo_action_is_disabled() { + assertThat(manager.getRedoAction().isEnabled()) + .as("redo Action.isEnabled()").isFalse(); + return self(); + } + + public ThenUndoRedo the_redo_action_is_enabled() { + assertThat(manager.getRedoAction().isEnabled()) + .as("redo Action.isEnabled()").isTrue(); + return self(); + } + + public ThenUndoRedo all_$_grouped_edits_are_reverted(int expectedCount) { + assertThat(groupedEdits) + .as("grouped edit list size").hasSize(expectedCount); + for (TrackableEdit edit : groupedEdits) { + assertThat(edit.undoCount) + .as("undo count for grouped edit '%s'", edit.getName()) + .isEqualTo(1); + } + return self(); + } + } + + // ========================================================================= + // Helper: TrackableEdit — records how many times undo/redo was called + // ========================================================================= + + static class TrackableEdit extends AbstractUndoableEdit { + + private static final long serialVersionUID = 1L; + private final String name; + int undoCount = 0; + int redoCount = 0; + + TrackableEdit(String name) { + this.name = name; + } + + String getName() { + return name; + } + + @Override + public void undo() throws CannotUndoException { + super.undo(); + undoCount++; + } + + @Override + public void redo() throws CannotRedoException { + super.redo(); + redoCount++; + } + + @Override + public String getPresentationName() { + return name; + } + } +} From 9f480d657d486996f68f8aafd6db68124aa5c5f1 Mon Sep 17 00:00:00 2001 From: Gierka36 Date: Fri, 12 Jun 2026 08:39:27 +0200 Subject: [PATCH 5/5] Messed up the branches --- .github/workflows/maven.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 65c3cf7d0..849076889 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -2,9 +2,9 @@ name: Java CI with Maven on: pull_request: - branches: [ "develop", "main" ] + branches: [ "develop"] push: - branches: [ "develop", "main" ] + branches: [ "GerardasAlutis_Undo/Redo" ] jobs: build: