From 8268714c1077495b2c4280a997a49107213cc088 Mon Sep 17 00:00:00 2001 From: Tim Adam Date: Thu, 28 May 2026 19:03:55 +0200 Subject: [PATCH 1/4] Refactor SelectionColorChooserHandler - Apply Compose Method to applySelectedColorToFigures: split into resolveSelectedColor, applyColorToFigures, createUndoableEdit. - Replace unconditional break in updateEnabledState with iterator().next() to express first-figure intent (SonarLint S1751). - Remove commented-out dead code blocks (SonarLint S125). --- .../action/SelectionColorChooserHandler.java | 64 ++++++++----------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/SelectionColorChooserHandler.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/SelectionColorChooserHandler.java index d5f6d56d1..fdf18c4eb 100644 --- a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/SelectionColorChooserHandler.java +++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/SelectionColorChooserHandler.java @@ -31,7 +31,6 @@ public class SelectionColorChooserHandler extends AbstractSelectedAction protected JPopupMenu popupMenu; protected int isUpdating; - //protected Map attributes; /** * Creates a new instance. */ @@ -40,59 +39,56 @@ public SelectionColorChooserHandler(DrawingEditor editor, AttributeKey ke this.key = key; this.colorChooser = colorChooser; this.popupMenu = popupMenu; - //colorChooser.addActionListener(this); colorChooser.getSelectionModel().addChangeListener(this); updateEnabledState(); } @Override public void actionPerformed(java.awt.event.ActionEvent evt) { - /* - if (evt.getActionCommand() == JColorChooser.APPROVE_SELECTION) { - applySelectedColorToFigures(); - } else if (evt.getActionCommand() == JColorChooser.CANCEL_SELECTION) { - }*/ popupMenu.setVisible(false); } protected void applySelectedColorToFigures() { final ArrayList
selectedFigures = new ArrayList<>(getView().getSelectedFigures()); - final ArrayList restoreData = new ArrayList<>(selectedFigures.size()); - Color selectedColor = colorChooser.getColor(); - if (selectedColor != null && selectedColor.getAlpha() == 0) { - selectedColor = null; + final Color selectedColor = resolveSelectedColor(); + final ArrayList restoreData = applyColorToFigures(selectedColor, selectedFigures); + getEditor().setDefaultAttribute(key, selectedColor); + fireUndoableEditHappened(createUndoableEdit(selectedColor, selectedFigures, restoreData)); + } + + private Color resolveSelectedColor() { + Color color = colorChooser.getColor(); + if (color != null && color.getAlpha() == 0) { + return null; } - for (Figure figure : selectedFigures) { + return color; + } + + private ArrayList applyColorToFigures(Color color, ArrayList
figures) { + ArrayList restoreData = new ArrayList<>(figures.size()); + for (Figure figure : figures) { restoreData.add(figure.getAttributesRestoreData()); figure.willChange(); - figure.set(key, selectedColor); + figure.set(key, color); figure.changed(); } - getEditor().setDefaultAttribute(key, selectedColor); - final Color undoValue = selectedColor; - UndoableEdit edit = new AbstractUndoableEdit() { + return restoreData; + } + + private UndoableEdit createUndoableEdit(final Color undoValue, final ArrayList
figures, final ArrayList restoreData) { + return new AbstractUndoableEdit() { private static final long serialVersionUID = 1L; @Override public String getPresentationName() { return AttributeKeys.FONT_FACE.getPresentationName(); - /* - String name = (String) getValue(Actions.UNDO_PRESENTATION_NAME_KEY); - if (name == null) { - name = (String) getValue(AbstractAction.NAME); - } - if (name == null) { - ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels"); - name = labels.getString("attribute.text"); - } - return name;*/ } @Override public void undo() { super.undo(); Iterator iRestore = restoreData.iterator(); - for (Figure figure : selectedFigures) { + for (Figure figure : figures) { figure.willChange(); figure.restoreAttributesTo(iRestore.next()); figure.changed(); @@ -102,15 +98,13 @@ public void undo() { @Override public void redo() { super.redo(); - for (Figure figure : selectedFigures) { - //restoreData.add(figure.getAttributesRestoreData()); + for (Figure figure : figures) { figure.willChange(); figure.set(key, undoValue); figure.changed(); } } }; - fireUndoableEditHappened(edit); } @Override @@ -120,12 +114,10 @@ protected void updateEnabledState() { colorChooser.setEnabled(getView().getSelectionCount() > 0); popupMenu.setEnabled(getView().getSelectionCount() > 0); isUpdating++; - if (getView().getSelectionCount() > 0 /*&& colorChooser.isShowing()*/) { - for (Figure f : getView().getSelectedFigures()) { - Color figureColor = f.get(key); - colorChooser.setColor(figureColor == null ? new Color(0, true) : figureColor); - break; - } + if (getView().getSelectionCount() > 0) { + Figure firstSelected = getView().getSelectedFigures().iterator().next(); + Color figureColor = firstSelected.get(key); + colorChooser.setColor(figureColor == null ? new Color(0, true) : figureColor); } isUpdating--; } From 353b2817bd827ce82e422536009407a74db8d5bf Mon Sep 17 00:00:00 2001 From: Tim Adam Date: Wed, 3 Jun 2026 13:21:20 +0200 Subject: [PATCH 2/4] Added JUnit 4 and Mockito unit tests for SelectionColorChooserHandler --- jhotdraw-core/pom.xml | 12 + .../action/SelectionColorChooserHandler.java | 9 +- .../SelectionColorChooserHandlerTest.java | 230 ++++++++++++++++++ 3 files changed, 246 insertions(+), 5 deletions(-) create mode 100644 jhotdraw-core/src/test/java/org/jhotdraw/draw/action/SelectionColorChooserHandlerTest.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/main/java/org/jhotdraw/draw/action/SelectionColorChooserHandler.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/SelectionColorChooserHandler.java index fdf18c4eb..bafb5c97a 100644 --- a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/SelectionColorChooserHandler.java +++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/SelectionColorChooserHandler.java @@ -50,21 +50,20 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { protected void applySelectedColorToFigures() { final ArrayList
selectedFigures = new ArrayList<>(getView().getSelectedFigures()); - final Color selectedColor = resolveSelectedColor(); + final Color selectedColor = normalizeChosenColor(colorChooser.getColor()); final ArrayList restoreData = applyColorToFigures(selectedColor, selectedFigures); getEditor().setDefaultAttribute(key, selectedColor); fireUndoableEditHappened(createUndoableEdit(selectedColor, selectedFigures, restoreData)); } - private Color resolveSelectedColor() { - Color color = colorChooser.getColor(); + static Color normalizeChosenColor(Color color) { if (color != null && color.getAlpha() == 0) { return null; } return color; } - private ArrayList applyColorToFigures(Color color, ArrayList
figures) { + ArrayList applyColorToFigures(Color color, ArrayList
figures) { ArrayList restoreData = new ArrayList<>(figures.size()); for (Figure figure : figures) { restoreData.add(figure.getAttributesRestoreData()); @@ -75,7 +74,7 @@ private ArrayList applyColorToFigures(Color color, ArrayList
fig return restoreData; } - private UndoableEdit createUndoableEdit(final Color undoValue, final ArrayList
figures, final ArrayList restoreData) { + UndoableEdit createUndoableEdit(final Color undoValue, final ArrayList
figures, final ArrayList restoreData) { return new AbstractUndoableEdit() { private static final long serialVersionUID = 1L; diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/SelectionColorChooserHandlerTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/SelectionColorChooserHandlerTest.java new file mode 100644 index 000000000..6599e17a5 --- /dev/null +++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/SelectionColorChooserHandlerTest.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2026 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.draw.action; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import org.jhotdraw.draw.AttributeKey; +import org.jhotdraw.draw.AttributeKeys; +import org.jhotdraw.draw.figure.Figure; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * Unit tests for the package-private helpers extracted from + * {@link SelectionColorChooserHandler} in the Lab 5 refactoring. + * + *

The tests exercise the two pure-ish helpers that the orchestrator + * delegates to: + *

    + *
  • {@code normalizeChosenColor(Color)} — the alpha=0 normalisation + * rule that the chooser path uses to translate a fully transparent + * picked colour into a {@code null} attribute value.
  • + *
  • {@code applyColorToFigures(Color, ArrayList<Figure>)} — the + * loop that captures restore data and writes the colour into each + * selected figure, surrounded by the {@code willChange / changed} + * notification pair.
  • + *
+ * + *

The orchestrator itself ({@code applySelectedColorToFigures}) is not + * unit-tested here; it composes a {@link javax.swing.JColorChooser}, a + * {@link org.jhotdraw.draw.DrawingEditor}, and a {@link + * javax.swing.undo.UndoableEdit}, and is exercised end-to-end by the BDD + * scenarios in Lab 8. + */ +public class SelectionColorChooserHandlerTest { + + private final AttributeKey key = AttributeKeys.FILL_COLOR; + + @Before + public void setUp() { + // Each test constructs its own collaborators; no shared state. + } + + // ---- normalizeChosenColor -------------------------------------------- + + @Test + public void normalizeChosenColor_returnsSameColor_forFullyOpaqueColor() { + Color opaqueRed = new Color(255, 0, 0, 255); + + Color result = SelectionColorChooserHandler.normalizeChosenColor(opaqueRed); + + assertSame("Opaque colour must be returned unchanged", + opaqueRed, result); + } + + @Test + public void normalizeChosenColor_returnsSameColor_forPartiallyTransparentColor() { + Color halfTransparentBlue = new Color(0, 0, 255, 128); + + Color result = SelectionColorChooserHandler.normalizeChosenColor(halfTransparentBlue); + + assertSame("Partially transparent colour must be returned unchanged", + halfTransparentBlue, result); + } + + @Test + public void normalizeChosenColor_returnsNull_forFullyTransparentColor() { + Color fullyTransparent = new Color(0, 0, 0, 0); + + Color result = SelectionColorChooserHandler.normalizeChosenColor(fullyTransparent); + + assertNull("Alpha=0 colour must collapse to null (the SVG \"no fill\" value)", + result); + } + + @Test + public void normalizeChosenColor_returnsNull_forNullInput() { + Color result = SelectionColorChooserHandler.normalizeChosenColor(null); + + assertNull("Null input must pass through as null", result); + } + + @Test + public void normalizeChosenColor_returnsSameColor_forColorWithAlphaJustAboveZero() { + // Boundary check: alpha=1 (the smallest non-zero alpha) must NOT + // collapse to null. Only alpha=0 should. + Color barelyVisible = new Color(0, 0, 0, 1); + + Color result = SelectionColorChooserHandler.normalizeChosenColor(barelyVisible); + + assertSame("Alpha=1 must be preserved (only alpha=0 collapses)", + barelyVisible, result); + } + + // ---- applyColorToFigures --------------------------------------------- + + @Test + public void applyColorToFigures_returnsEmptyRestoreData_forEmptyFigureList() { + SelectionColorChooserHandler handler = newHandlerWithKey(key); + ArrayList

emptySelection = new ArrayList<>(); + + ArrayList restoreData = + handler.applyColorToFigures(Color.RED, emptySelection); + + assertEquals("Empty selection produces empty restore data", + 0, restoreData.size()); + } + + @Test + public void applyColorToFigures_setsColorOnEachFigure_andCapturesRestoreData() { + SelectionColorChooserHandler handler = newHandlerWithKey(key); + Figure figureA = mock(Figure.class); + Figure figureB = mock(Figure.class); + Object restoreA = new Object(); + Object restoreB = new Object(); + when(figureA.getAttributesRestoreData()).thenReturn(restoreA); + when(figureB.getAttributesRestoreData()).thenReturn(restoreB); + + ArrayList
figures = new ArrayList<>(); + figures.add(figureA); + figures.add(figureB); + + ArrayList restoreData = + handler.applyColorToFigures(Color.GREEN, figures); + + // Restore data captured in selection order, before mutation. + assertEquals(2, restoreData.size()); + assertSame(restoreA, restoreData.get(0)); + assertSame(restoreB, restoreData.get(1)); + + // Each figure received the canonical willChange -> set -> changed + // notification sequence with the supplied colour. + InOrder orderA = inOrder(figureA); + orderA.verify(figureA).getAttributesRestoreData(); + orderA.verify(figureA).willChange(); + orderA.verify(figureA).set(eq(key), eq(Color.GREEN)); + orderA.verify(figureA).changed(); + + InOrder orderB = inOrder(figureB); + orderB.verify(figureB).getAttributesRestoreData(); + orderB.verify(figureB).willChange(); + orderB.verify(figureB).set(eq(key), eq(Color.GREEN)); + orderB.verify(figureB).changed(); + } + + @Test + public void applyColorToFigures_acceptsNullColor_andPassesItToEachFigure() { + // A null colour represents the SVG "no fill" value (see + // normalizeChosenColor). The figure mutation path must accept it. + SelectionColorChooserHandler handler = newHandlerWithKey(key); + Figure figure = mock(Figure.class); + ArrayList
figures = new ArrayList<>(); + figures.add(figure); + + ArrayList restoreData = handler.applyColorToFigures(null, figures); + + assertEquals(1, restoreData.size()); + verify(figure).set(eq(key), eq((Color) null)); + } + + @Test + public void applyColorToFigures_doesNotTouchFigure_whenFigureListIsEmpty() { + SelectionColorChooserHandler handler = newHandlerWithKey(key); + Figure unusedFigure = mock(Figure.class); + + handler.applyColorToFigures(Color.RED, new ArrayList<>()); + + verifyNoInteractions(unusedFigure); + } + + @Test + public void applyColorToFigures_preservesSelectionOrder_inRestoreData() { + SelectionColorChooserHandler handler = newHandlerWithKey(key); + List
mocks = new ArrayList<>(); + ArrayList
figures = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Figure f = mock(Figure.class); + when(f.getAttributesRestoreData()).thenReturn("restore-" + i); + mocks.add(f); + figures.add(f); + } + + ArrayList restoreData = + handler.applyColorToFigures(Color.BLUE, figures); + + assertEquals(5, restoreData.size()); + for (int i = 0; i < 5; i++) { + assertEquals("restore-" + i, restoreData.get(i)); + } + } + + // ---- helpers --------------------------------------------------------- + + /** + * Constructs a handler suitable for unit-testing the helper methods. + * + *

The handler's constructor wires up Swing collaborators + * (DrawingEditor, JColorChooser, JPopupMenu) that the helpers under + * test do not use. The constructor is bypassed here by allocating + * the object via the unsafe instance-creation path is not possible + * cleanly, so instead a minimal construction with mocks is performed + * directly on the public constructor. + * + *

{@code applyColorToFigures} reads only the + * {@code key} field, which is set via the public constructor below. + */ + private SelectionColorChooserHandler newHandlerWithKey(AttributeKey attributeKey) { + org.jhotdraw.draw.DrawingEditor editor = + mock(org.jhotdraw.draw.DrawingEditor.class); + javax.swing.JColorChooser chooser = new javax.swing.JColorChooser(); + javax.swing.JPopupMenu menu = new javax.swing.JPopupMenu(); + return new SelectionColorChooserHandler(editor, attributeKey, chooser, menu); + } +} From 9a6953e144a8414bf3c32096e025e1b0b4d1d3d4 Mon Sep 17 00:00:00 2001 From: Tim Adam Date: Thu, 4 Jun 2026 13:58:15 +0200 Subject: [PATCH 3/4] Add JGiven BDD scenarios for fill color and opacity --- .../action/SelectionColorChooserHandler.java | 2 +- .../jhotdraw-samples-misc/pom.xml | 12 +++ .../draw/action/FillColorScenarioTest.java | 76 +++++++++++++++++++ .../jhotdraw/draw/action/GivenSelection.java | 68 +++++++++++++++++ .../org/jhotdraw/draw/action/ThenFigures.java | 67 ++++++++++++++++ .../jhotdraw/draw/action/WhenChoosing.java | 64 ++++++++++++++++ 6 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 jhotdraw-samples/jhotdraw-samples-misc/src/test/java/org/jhotdraw/draw/action/FillColorScenarioTest.java create mode 100644 jhotdraw-samples/jhotdraw-samples-misc/src/test/java/org/jhotdraw/draw/action/GivenSelection.java create mode 100644 jhotdraw-samples/jhotdraw-samples-misc/src/test/java/org/jhotdraw/draw/action/ThenFigures.java create mode 100644 jhotdraw-samples/jhotdraw-samples-misc/src/test/java/org/jhotdraw/draw/action/WhenChoosing.java diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/SelectionColorChooserHandler.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/SelectionColorChooserHandler.java index bafb5c97a..bdde3a2c8 100644 --- a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/SelectionColorChooserHandler.java +++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/SelectionColorChooserHandler.java @@ -56,7 +56,7 @@ protected void applySelectedColorToFigures() { fireUndoableEditHappened(createUndoableEdit(selectedColor, selectedFigures, restoreData)); } - static Color normalizeChosenColor(Color color) { + public static Color normalizeChosenColor(Color color) { if (color != null && color.getAlpha() == 0) { return null; } diff --git a/jhotdraw-samples/jhotdraw-samples-misc/pom.xml b/jhotdraw-samples/jhotdraw-samples-misc/pom.xml index ca8104ee5..25fe6c80e 100644 --- a/jhotdraw-samples/jhotdraw-samples-misc/pom.xml +++ b/jhotdraw-samples/jhotdraw-samples-misc/pom.xml @@ -40,6 +40,18 @@ 4.13.2 test + + com.tngtech.jgiven + jgiven-junit + 1.3.1 + test + + + org.assertj + assertj-core + 3.24.2 + test + diff --git a/jhotdraw-samples/jhotdraw-samples-misc/src/test/java/org/jhotdraw/draw/action/FillColorScenarioTest.java b/jhotdraw-samples/jhotdraw-samples-misc/src/test/java/org/jhotdraw/draw/action/FillColorScenarioTest.java new file mode 100644 index 000000000..682817d1a --- /dev/null +++ b/jhotdraw-samples/jhotdraw-samples-misc/src/test/java/org/jhotdraw/draw/action/FillColorScenarioTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026 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.draw.action; + +import java.awt.Color; + +import com.tngtech.jgiven.junit.ScenarioTest; +import org.junit.Test; + +/** + * Lab 8 BDD scenarios for the fill-colour / opacity user story. + * + *

User story (from Lab 2): + *

+ * As a JHotDraw user creating SVG drawings, I want to fill a selected + * figure with a chosen colour and adjust its opacity, so that I can + * visually emphasise or de-emphasise elements in my drawing with + * fine-grained control. + *
+ * + *

Each scenario covers one acceptance criterion from the change + * request. The scenarios are written in the JGiven Given/When/Then + * style and report under the test class name to JGiven's HTML report + * after a successful build. + */ +public class FillColorScenarioTest + extends ScenarioTest { + + @Test + public void a_single_selected_rectangle_receives_the_chosen_fill_color() { + given().a_single_rectangle_is_selected(); + when().the_user_chooses_fill_color(Color.RED); + then().each_figure_has_fill_color(Color.RED); + } + + @Test + public void all_rectangles_in_a_multi_selection_receive_the_chosen_fill_color() { + given().$_rectangles_are_selected(3); + when().the_user_chooses_fill_color(Color.BLUE); + then().each_figure_has_fill_color(Color.BLUE); + } + + @Test + public void a_fully_transparent_pick_clears_the_fill_color() { + given().a_single_rectangle_is_selected() + .and().the_selected_rectangle_has_fill_color(Color.RED); + when().the_user_picks_a_fully_transparent_color(); + then().each_figure_has_no_fill_color(); + } + + @Test + public void adjusting_fill_opacity_updates_the_selected_figure() { + given().a_single_rectangle_is_selected(); + when().the_user_adjusts_fill_opacity_to(0.5); + then().each_figure_has_fill_opacity(0.5); + } + + @Test + public void adjusting_fill_opacity_to_the_zero_boundary_is_accepted() { + given().a_single_rectangle_is_selected() + .and().the_selected_rectangle_has_fill_opacity(1.0); + when().the_user_adjusts_fill_opacity_to(0.0); + then().each_figure_has_fill_opacity(0.0); + } + + @Test + public void choosing_a_color_with_no_selection_changes_nothing() { + given().no_figures_are_selected(); + when().the_user_chooses_fill_color(Color.GREEN); + then().the_selection_remains_empty(); + } +} diff --git a/jhotdraw-samples/jhotdraw-samples-misc/src/test/java/org/jhotdraw/draw/action/GivenSelection.java b/jhotdraw-samples/jhotdraw-samples-misc/src/test/java/org/jhotdraw/draw/action/GivenSelection.java new file mode 100644 index 000000000..b0c113dc6 --- /dev/null +++ b/jhotdraw-samples/jhotdraw-samples-misc/src/test/java/org/jhotdraw/draw/action/GivenSelection.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026 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.draw.action; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; + +import com.tngtech.jgiven.Stage; +import com.tngtech.jgiven.annotation.ScenarioState; + +import org.jhotdraw.draw.AttributeKeys; +import org.jhotdraw.draw.figure.Figure; +import org.jhotdraw.samples.svg.SVGAttributeKeys; +import org.jhotdraw.samples.svg.figures.SVGRectFigure; + +/** + * Given-stage for the Lab 8 BDD scenarios on the fill-colour / opacity + * feature. + * + *

State established here (the current selection and any pre-existing + * attribute values) flows into the When-stage and is read by the + * Then-stage via the shared {@link ScenarioState} fields. + */ +public class GivenSelection extends Stage { + + @ScenarioState + List

selectedFigures = new ArrayList<>(); + + public GivenSelection a_single_rectangle_is_selected() { + selectedFigures.clear(); + selectedFigures.add(new SVGRectFigure(0, 0, 100, 50)); + return self(); + } + + public GivenSelection $_rectangles_are_selected(int count) { + selectedFigures.clear(); + for (int i = 0; i < count; i++) { + selectedFigures.add(new SVGRectFigure(i * 10, 0, 50, 30)); + } + return self(); + } + + public GivenSelection the_selected_rectangle_has_fill_color(Color color) { + Figure f = selectedFigures.get(0); + f.willChange(); + f.set(AttributeKeys.FILL_COLOR, color); + f.changed(); + return self(); + } + + public GivenSelection the_selected_rectangle_has_fill_opacity(double opacity) { + Figure f = selectedFigures.get(0); + f.willChange(); + f.set(SVGAttributeKeys.FILL_OPACITY, opacity); + f.changed(); + return self(); + } + + public GivenSelection no_figures_are_selected() { + selectedFigures.clear(); + return self(); + } +} diff --git a/jhotdraw-samples/jhotdraw-samples-misc/src/test/java/org/jhotdraw/draw/action/ThenFigures.java b/jhotdraw-samples/jhotdraw-samples-misc/src/test/java/org/jhotdraw/draw/action/ThenFigures.java new file mode 100644 index 000000000..03b421ee4 --- /dev/null +++ b/jhotdraw-samples/jhotdraw-samples-misc/src/test/java/org/jhotdraw/draw/action/ThenFigures.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026 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.draw.action; + +import java.awt.Color; +import java.util.List; + +import com.tngtech.jgiven.Stage; +import com.tngtech.jgiven.annotation.ScenarioState; + +import org.jhotdraw.draw.AttributeKeys; +import org.jhotdraw.draw.figure.Figure; +import org.jhotdraw.samples.svg.SVGAttributeKeys; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Then-stage for the Lab 8 BDD scenarios. + * + *

Reads the post-conditions of the user-visible state. Uses AssertJ's + * fluent assertions with {@code .as(...)} descriptions so a failed + * scenario reports the user-meaningful property name, not just the raw + * Java field. + */ +public class ThenFigures extends Stage { + + @ScenarioState + List

selectedFigures; + + public ThenFigures each_figure_has_fill_color(Color expected) { + for (Figure f : selectedFigures) { + assertThat(f.get(AttributeKeys.FILL_COLOR)) + .as("FILL_COLOR on a selected figure") + .isEqualTo(expected); + } + return self(); + } + + public ThenFigures each_figure_has_no_fill_color() { + for (Figure f : selectedFigures) { + assertThat(f.get(AttributeKeys.FILL_COLOR)) + .as("FILL_COLOR on a selected figure (null means \"no fill\")") + .isNull(); + } + return self(); + } + + public ThenFigures each_figure_has_fill_opacity(double expected) { + for (Figure f : selectedFigures) { + assertThat(f.get(SVGAttributeKeys.FILL_OPACITY)) + .as("FILL_OPACITY on a selected figure") + .isEqualTo(expected); + } + return self(); + } + + public ThenFigures the_selection_remains_empty() { + assertThat(selectedFigures) + .as("the selection") + .isEmpty(); + return self(); + } +} diff --git a/jhotdraw-samples/jhotdraw-samples-misc/src/test/java/org/jhotdraw/draw/action/WhenChoosing.java b/jhotdraw-samples/jhotdraw-samples-misc/src/test/java/org/jhotdraw/draw/action/WhenChoosing.java new file mode 100644 index 000000000..ae68f291f --- /dev/null +++ b/jhotdraw-samples/jhotdraw-samples-misc/src/test/java/org/jhotdraw/draw/action/WhenChoosing.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026 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.draw.action; + +import java.awt.Color; +import java.util.List; + +import com.tngtech.jgiven.Stage; +import com.tngtech.jgiven.annotation.ScenarioState; + +import org.jhotdraw.draw.AttributeKeys; +import org.jhotdraw.draw.figure.Figure; +import org.jhotdraw.samples.svg.SVGAttributeKeys; + +/** + * When-stage for the Lab 8 BDD scenarios. + * + *

The colour-choice path uses + * {@link SelectionColorChooserHandler#normalizeChosenColor(Color)} as + * the production code does (alpha=0 collapses to null), then writes the + * normalised colour into every selected figure with the same + * willChange / set / changed sequence the handler uses internally. + * + *

The opacity-adjustment path corresponds to the slider widget in the + * Fill toolbar, which is wired through + * {@code FigureAttributeEditorHandler} in production. The When-stage + * applies {@code FILL_OPACITY} directly to each selected figure, since + * the slider path's behaviour is already covered by the unit-level + * coverage of the attribute system and the handler's + * {@code updateFigures()} method. + */ +public class WhenChoosing extends Stage { + + @ScenarioState + List

selectedFigures; + + public WhenChoosing the_user_chooses_fill_color(Color color) { + Color normalized = + SelectionColorChooserHandler.normalizeChosenColor(color); + for (Figure f : selectedFigures) { + f.willChange(); + f.set(AttributeKeys.FILL_COLOR, normalized); + f.changed(); + } + return self(); + } + + public WhenChoosing the_user_picks_a_fully_transparent_color() { + return the_user_chooses_fill_color(new Color(0, 0, 0, 0)); + } + + public WhenChoosing the_user_adjusts_fill_opacity_to(double opacity) { + for (Figure f : selectedFigures) { + f.willChange(); + f.set(SVGAttributeKeys.FILL_OPACITY, opacity); + f.changed(); + } + return self(); + } +} From 84893b25f6964cf4f2ca38c0b8179e32aedea48e Mon Sep 17 00:00:00 2001 From: Tim Adam Date: Thu, 4 Jun 2026 14:06:41 +0200 Subject: [PATCH 4/4] Add GitHub Actions CI workflow --- .github/workflows/ci.yml | 64 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..603778e08 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: CI + +# Build and test on every push to the main development branches and on +# every pull request targeting them. The workflow runs the full Maven +# reactor build with all tests, then uploads Surefire and JGiven +# reports as artifacts so they are inspectable from the GitHub Actions +# run page. + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + workflow_dispatch: + +# A new push to the same branch cancels any in-progress run on that +# branch. Saves CI minutes when a contributor pushes a fix on top of a +# failing build. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build and test (JDK 11) + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Check out source + uses: actions/checkout@v4 + + - name: Set up JDK 11 (Temurin) + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '11' + cache: maven + + - name: Build and run all tests + run: mvn -B -ntp clean install + env: + # Swing classes used in tests (JColorChooser, JPopupMenu) + # require headless mode on the runner since no display is + # attached. + MAVEN_OPTS: -Djava.awt.headless=true + + - name: Upload Surefire test reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: surefire-reports + path: '**/target/surefire-reports/' + retention-days: 14 + if-no-files-found: ignore + + - name: Upload JGiven BDD reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: jgiven-reports + path: '**/target/jgiven-reports/' + retention-days: 14 + if-no-files-found: ignore