diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 000000000..e92e96e37
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,259 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "java",
+ "name": "Current File",
+ "request": "launch",
+ "mainClass": "${file}"
+ },
+ {
+ "type": "java",
+ "name": "CIELABColorSpace",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.color.CIELABColorSpace",
+ "projectName": "jhotdraw-gui"
+ },
+ {
+ "type": "java",
+ "name": "CIELCHabColorSpace",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.color.CIELCHabColorSpace",
+ "projectName": "jhotdraw-gui"
+ },
+ {
+ "type": "java",
+ "name": "EditCanvasPanel",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.gui.action.EditCanvasPanel",
+ "projectName": "jhotdraw-gui"
+ },
+ {
+ "type": "java",
+ "name": "CIEXYChromaticityDiagram",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.color.CIEXYChromaticityDiagram",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "JMixer",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.color.JMixer",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "WheelsAndSlidersMain",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.color.WheelsAndSlidersMain",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "FontChooserMain",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.font.FontChooserMain",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "ActivityMonitorSample",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.ActivityMonitorSample",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "AnimationSample",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.AnimationSample",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "BezierDemo",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.BezierDemo",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "ConnectingFiguresSample",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.ConnectingFiguresSample",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "CreationToolSample",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.CreationToolSample",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "DefaultDOMStorableSample",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.DefaultDOMStorableSample",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "DnDMultiEditorSample",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.DnDMultiEditorSample",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "EditorSample",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.EditorSample",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "FileIconsSample",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.FileIconsSample",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "LabeledLineConnectionFigureSample",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.LabeledLineConnectionFigureSample",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "LayouterSample",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.LayouterSample",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "MovableChildFiguresSample",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.MovableChildFiguresSample",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "MovableChildFiguresSampleWithAbstractDrawingView",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.MovableChildFiguresSampleWithAbstractDrawingView",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "MovableChildFiguresSampleWithDelegatorDrawingView",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.MovableChildFiguresSampleWithDelegatorDrawingView",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "MultiEditorSample",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.MultiEditorSample",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "SVGDrawingPanelSample",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.SVGDrawingPanelSample",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "SelectionToolSample",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.SelectionToolSample",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "SmartConnectionFigureSample",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.mini.SmartConnectionFigureSample",
+ "projectName": "jhotdraw-samples-mini"
+ },
+ {
+ "type": "java",
+ "name": "Main",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.draw.Main",
+ "projectName": "jhotdraw-samples-misc"
+ },
+ {
+ "type": "java",
+ "name": "Main(1)",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.net.Main",
+ "projectName": "jhotdraw-samples-misc"
+ },
+ {
+ "type": "java",
+ "name": "NetApplet",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.net.NetApplet",
+ "projectName": "jhotdraw-samples-misc"
+ },
+ {
+ "type": "java",
+ "name": "Main(2)",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.odg.Main",
+ "projectName": "jhotdraw-samples-misc"
+ },
+ {
+ "type": "java",
+ "name": "Main(3)",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.pert.Main",
+ "projectName": "jhotdraw-samples-misc"
+ },
+ {
+ "type": "java",
+ "name": "PertApplet",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.pert.PertApplet",
+ "projectName": "jhotdraw-samples-misc"
+ },
+ {
+ "type": "java",
+ "name": "Main(4)",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.svg.Main",
+ "projectName": "jhotdraw-samples-misc"
+ },
+ {
+ "type": "java",
+ "name": "SVGApplet",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.svg.SVGApplet",
+ "projectName": "jhotdraw-samples-misc"
+ },
+ {
+ "type": "java",
+ "name": "Main(5)",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.samples.teddy.Main",
+ "projectName": "jhotdraw-samples-misc"
+ },
+ {
+ "type": "java",
+ "name": "Bezier",
+ "request": "launch",
+ "mainClass": "org.jhotdraw.geom.Bezier",
+ "projectName": "jhotdraw-utils"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..0b90c7a18
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,6 @@
+{
+ "sonarlint.connectedMode.project": {
+ "connectionId": "jakubpotocky",
+ "projectKey": "JakubPotocky_JHotDraw"
+ }
+}
\ No newline at end of file
diff --git a/jhotdraw-core/pom.xml b/jhotdraw-core/pom.xml
index 7c276da85..0d488488c 100644
--- a/jhotdraw-core/pom.xml
+++ b/jhotdraw-core/pom.xml
@@ -35,6 +35,36 @@
6.8.21
test
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+ org.mockito
+ mockito-core
+ 5.2.0
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.24.1
+ test
+
+
+ org.assertj
+ assertj-swing
+ 3.17.1
+ test
+
+
+ com.tngtech.jgiven
+ jgiven-junit
+ 2.0.3
+ test
+
${project.groupId}
jhotdraw-actions
diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/figure/ImageFigure.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/figure/ImageFigure.java
index 04c11db5d..a8c6ea860 100644
--- a/jhotdraw-core/src/main/java/org/jhotdraw/draw/figure/ImageFigure.java
+++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/figure/ImageFigure.java
@@ -34,6 +34,7 @@ public class ImageFigure extends AbstractAttributedDecoratedFigure
implements ImageHolderFigure {
private static final long serialVersionUID = 1L;
+ private static final String IMAGE_DATA_STRING = "imageData";
/**
* This rectangle describes the bounds into which we draw the image.
*/
@@ -117,8 +118,7 @@ protected void drawStroke(Graphics2D g) {
// SHAPE AND BOUNDS
@Override
public Rectangle2D.Double getBounds() {
- Rectangle2D.Double bounds = (Rectangle2D.Double) rectangle.clone();
- return bounds;
+ return (Rectangle2D.Double) rectangle.clone();
}
@Override
@@ -170,14 +170,13 @@ public void restoreTransformTo(Object geometry) {
@Override
public Object getTransformRestoreData() {
- return (Rectangle2D.Double) rectangle.clone();
+ return rectangle.clone();
}
// EDITING
@Override
public Collection getActions(Point2D.Double p) {
- LinkedList actions = new LinkedList<>();
- return actions;
+ return new LinkedList<>();
}
// CONNECTING
@@ -193,20 +192,11 @@ public Connector findCompatibleConnector(Connector c, boolean isStartConnector)
return new ChopRectangleConnector(this);
}
- // COMPOSITE FIGURES
- // CLONING
- @Override
- public ImageFigure clone() {
- ImageFigure that = (ImageFigure) super.clone();
- that.rectangle = (Rectangle2D.Double) this.rectangle.clone();
- return that;
- }
-
@Override
public void read(DOMInput in) throws IOException {
super.read(in);
- if (in.getElementCount("imageData") > 0) {
- in.openElement("imageData");
+ if (in.getElementCount(IMAGE_DATA_STRING) > 0) {
+ in.openElement(IMAGE_DATA_STRING);
String base64Data = in.getText();
if (base64Data != null) {
setImageData(Base64.decode(base64Data));
@@ -219,7 +209,7 @@ public void read(DOMInput in) throws IOException {
public void write(DOMOutput out) throws IOException {
super.write(out);
if (getImageData() != null) {
- out.openElement("imageData");
+ out.openElement(IMAGE_DATA_STRING);
out.addText(Base64.encodeBytes(getImageData()));
out.closeElement();
}
@@ -274,7 +264,6 @@ public BufferedImage getBufferedImage() {
try {
bufferedImage = ImageIO.read(new ByteArrayInputStream(imageData));
} catch (IOException e) {
- e.printStackTrace();
// If we can't create a buffered image from the image data,
// there is no use to keep the image data and try again, so
// we drop the image data.
@@ -300,7 +289,6 @@ public byte[] getImageData() {
bout.close();
imageData = bout.toByteArray();
} catch (IOException e) {
- e.printStackTrace();
// If we can't create image data from the buffered image,
// there is no use to keep the buffered image and try again, so
// we drop the buffered image.
@@ -314,7 +302,7 @@ public byte[] getImageData() {
public void loadImage(File file) throws IOException {
try (InputStream in = new FileInputStream(file)) {
loadImage(in);
- } catch (Throwable t) {
+ } catch (Exception t) {
ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels");
IOException e = new IOException(labels.getFormatted("file.failedToLoadImage.message", file.getName()));
e.initCause(t);
diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/tool/ImageTool.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/tool/ImageTool.java
index 6671e2f9f..6deecdef4 100644
--- a/jhotdraw-core/src/main/java/org/jhotdraw/draw/tool/ImageTool.java
+++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/tool/ImageTool.java
@@ -80,60 +80,14 @@ public boolean isUseFileDialog() {
@Override
public void activate(DrawingEditor editor) {
super.activate(editor);
- final DrawingView v = getView();
- if (v == null) {
+ final DrawingView view = getView();
+ if (view == null) {
return;
}
- final File file;
- if (useFileDialog) {
- getFileDialog().setVisible(true);
- if (getFileDialog().getFile() != null) {
- file = new File(getFileDialog().getDirectory(), getFileDialog().getFile());
- } else {
- file = null;
- }
- } else {
- if (getFileChooser().showOpenDialog(v.getComponent()) == JFileChooser.APPROVE_OPTION) {
- file = getFileChooser().getSelectedFile();
- } else {
- file = null;
- }
- }
+ final File file = selectImageFile(view);
if (file != null) {
- final ImageHolderFigure loaderFigure = ((ImageHolderFigure) prototype.clone());
- new SwingWorker() {
- @Override
- protected Object doInBackground() throws Exception {
- loaderFigure.loadImage(file);
- return null;
- }
-
- @Override
- protected void done() {
- try {
- get(); //will throw an ExecutionException if in doInBackground something went wrong.
- if (createdFigure == null) {
- ((ImageHolderFigure) prototype).setImage(loaderFigure.getImageData(), loaderFigure.getBufferedImage());
- } else {
- ((ImageHolderFigure) createdFigure).setImage(loaderFigure.getImageData(), loaderFigure.getBufferedImage());
- }
- } catch (IOException ex) {
- JOptionPane.showMessageDialog(v.getComponent(),
- ex.getMessage(),
- null,
- JOptionPane.ERROR_MESSAGE);
- } catch (InterruptedException | ExecutionException ex) {
- JOptionPane.showMessageDialog(v.getComponent(),
- ex.getMessage(),
- null,
- JOptionPane.ERROR_MESSAGE);
- getDrawing().remove(createdFigure);
- fireToolDone();
- }
- }
- }.execute();
+ loadImageAsync(file, view);
} else {
- //getDrawing().remove(createdFigure);
if (isToolDoneAfterCreation()) {
fireToolDone();
}
@@ -153,4 +107,58 @@ private FileDialog getFileDialog() {
}
return fileDialog;
}
-}
+
+ private File selectImageFile(DrawingView view) {
+ if (useFileDialog) {
+ getFileDialog().setVisible(true);
+ if (getFileDialog().getFile() != null) {
+ return new File(getFileDialog().getDirectory(), getFileDialog().getFile());
+ } else {
+ return null;
+ }
+ } else {
+ if (getFileChooser().showOpenDialog(view.getComponent()) == JFileChooser.APPROVE_OPTION) {
+ return getFileChooser().getSelectedFile();
+ } else {
+ return null;
+ }
+ }
+ }
+
+ private void loadImageAsync(File file, DrawingView view) {
+ final ImageHolderFigure loaderFigure = ((ImageHolderFigure) prototype.clone());
+ new SwingWorker() {
+ @Override
+ protected Object doInBackground() throws Exception {
+ loaderFigure.loadImage(file);
+ return null;
+ }
+
+ @Override
+ protected void done() {
+ try {
+ get(); // will throw an ExecutionException if in doInBackground something went wrong.
+ if (createdFigure == null) {
+ ((ImageHolderFigure) prototype).setImage(loaderFigure.getImageData(),
+ loaderFigure.getBufferedImage());
+ } else {
+ ((ImageHolderFigure) createdFigure).setImage(loaderFigure.getImageData(),
+ loaderFigure.getBufferedImage());
+ }
+ } catch (IOException ex) {
+ JOptionPane.showMessageDialog(view.getComponent(),
+ ex.getMessage(),
+ null,
+ JOptionPane.ERROR_MESSAGE);
+ } catch (InterruptedException | ExecutionException ex) {
+ JOptionPane.showMessageDialog(view.getComponent(),
+ ex.getMessage(),
+ null,
+ JOptionPane.ERROR_MESSAGE);
+ getDrawing().remove(createdFigure);
+ fireToolDone();
+ }
+ }
+ }.execute();
+ }
+}
\ No newline at end of file
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/GivenImageToolState.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/GivenImageToolState.java
new file mode 100644
index 000000000..3b472da6a
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/GivenImageToolState.java
@@ -0,0 +1,90 @@
+package org.jhotdraw.draw.tool;
+
+import com.tngtech.jgiven.Stage;
+import com.tngtech.jgiven.annotation.ProvidedScenarioState;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import org.jhotdraw.draw.figure.ImageHolderFigure;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * GIVEN steps for ImageTool BDD scenarios.
+ *
+ * This stage class sets up the initial context for BDD scenarios.
+ * It provides scenario state via @ProvidedScenarioState annotations
+ * that are shared with When and Then stages.
+ *
+ * Following the lecture's pattern: GivenIngredients → WhenCook → ThenMeal
+ * (lecture_9_Software_Verification_BDD slides 14-16)
+ */
+public class GivenImageToolState extends Stage {
+
+ @ProvidedScenarioState
+ protected ImageHolderFigure figure;
+
+ @ProvidedScenarioState
+ protected File imageFile;
+
+ @ProvidedScenarioState
+ protected String imageStatus;
+
+ @ProvidedScenarioState
+ protected BufferedImage testImage;
+
+ @ProvidedScenarioState
+ protected int imageWidth = 400;
+
+ @ProvidedScenarioState
+ protected int imageHeight = 300;
+
+ /**
+ * Given: a picture file on the PC
+ *
+ * Creates a temporary test image file that simulates a picture
+ * downloaded on the user's computer.
+ *
+ * Acceptance criteria: image file exists and is readable
+ */
+ public GivenImageToolState a_picture_file_on_the_pc() {
+ try {
+ // Create a temporary test image file (PNG format)
+ imageFile = File.createTempFile("test-image", ".png");
+ imageFile.deleteOnExit();
+
+ // Create test BufferedImage
+ testImage = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
+
+ // Mark status as file available
+ imageStatus = "file_available";
+
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to create test image file", e);
+ }
+ return self();
+ }
+
+ /**
+ * Given: a picture loaded in JHotDraw
+ *
+ * Creates and loads an image figure into JHotDraw, simulating
+ * a picture that has already been inserted into the canvas.
+ *
+ * Acceptance criteria: picture is loaded, ready for editing
+ */
+ public GivenImageToolState a_picture_loaded_in_jhotdraw() throws IOException {
+ // First, ensure we have a picture file
+ a_picture_file_on_the_pc();
+
+ // Create a mock figure representing loaded image
+ figure = mock(ImageHolderFigure.class);
+ org.mockito.Mockito.when(figure.getBufferedImage()).thenReturn(testImage);
+ org.mockito.Mockito.when(figure.getImageData()).thenReturn(new byte[0]);
+
+ // Mark status as loaded
+ imageStatus = "loaded_in_jhotdraw";
+
+ return self();
+ }
+}
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/ImageToolBddTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/ImageToolBddTest.java
new file mode 100644
index 000000000..1c5e71aac
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/ImageToolBddTest.java
@@ -0,0 +1,128 @@
+package org.jhotdraw.draw.tool;
+
+import com.tngtech.jgiven.junit.ScenarioTest;
+import java.io.IOException;
+import org.junit.Test;
+
+/**
+ * BDD Acceptance Tests for ImageTool using JGiven.
+ *
+ * Maps user stories to executable Given-When-Then scenarios.
+ * Each scenario verifies one acceptance criterion from the feature specification.
+ *
+ * User Stories (from Lab 2 - Change Request):
+ * - Sub-story 1: "As a JHotDraw user I want to insert a picture"
+ * - Sub-story 2: "As a JHotDraw user I want to edit existing picture"
+ *
+ * BDD Benefits (from lecture_9_Software_Verification_BDD):
+ * - Stakeholder communication: natural language describes behavior
+ * - Living documentation: scenarios are executable specifications
+ * - Traceability: direct link from user story to test
+ * - Collaboration: domain experts can read and validate scenarios
+ *
+ * Stage Classes (following lecture slides 14-16):
+ * - GivenImageToolState: Sets up initial context
+ * - WhenUserInteractsWithImage: Performs user actions
+ * - ThenImageBehavesCorrectly: Verifies outcomes (using AssertJ)
+ */
+public class ImageToolBddTest
+ extends ScenarioTest {
+
+ /**
+ * Scenario: User can insert an image from PC
+ *
+ * User Story: "As a JHotDraw user I want to insert a picture
+ * so that I can display it on the canvas"
+ *
+ * Acceptance Criteria:
+ * - Image file exists and is readable
+ * - Insertion completes successfully
+ * - Image has positive dimensions (width > 0, height > 0)
+ * - Image is visible on the canvas
+ */
+ @Test
+ public void user_can_insert_image_from_pc() {
+ given()
+ .a_picture_file_on_the_pc();
+
+ when()
+ .the_user_inserts_the_picture();
+
+ then()
+ .the_picture_is_displayed_in_jhotdraw();
+ }
+
+ /**
+ * Scenario: User can edit image size
+ *
+ * User Story: "As a JHotDraw user I want to edit existing picture
+ * so that I can adjust its dimensions"
+ *
+ * Acceptance Criteria:
+ * - Picture is loaded in JHotDraw
+ * - Size can be changed
+ * - New dimensions are applied correctly (e.g., 400x300)
+ * - Picture displayed at new size
+ */
+ @Test
+ public void user_can_edit_image_size() throws IOException {
+ given()
+ .a_picture_loaded_in_jhotdraw();
+
+ when()
+ .the_user_changes_the_size_to(400, 300);
+
+ then()
+ .the_picture_is_shown_at_size(400, 300);
+ }
+
+ /**
+ * Scenario: Error displayed when file not found
+ *
+ * Extension: Error handling for invalid files
+ *
+ * Acceptance Criteria:
+ * - User attempts to insert non-existent file
+ * - Error is caught and displayed
+ * - No corrupted image appears on canvas
+ */
+ @Test
+ public void error_displayed_when_file_not_found() {
+ given()
+ .a_picture_file_on_the_pc();
+
+ when()
+ .the_user_attempts_to_insert_a_nonexistent_file();
+
+ then()
+ .an_error_is_displayed();
+ }
+
+ /**
+ * Scenario: User can insert and edit in same session
+ *
+ * Integration: Combined insert + edit workflow
+ *
+ * Acceptance Criteria:
+ * - Insert succeeds
+ * - Resize succeeds
+ * - Final size is correct
+ */
+ @Test
+ public void user_can_insert_and_then_edit_in_same_session() {
+ given()
+ .a_picture_file_on_the_pc();
+
+ when()
+ .the_user_inserts_the_picture();
+
+ then()
+ .the_insertion_was_successful();
+
+ when()
+ .the_user_changes_the_size_to(500, 400);
+
+ then()
+ .the_picture_is_shown_at_size(500, 400);
+ }
+}
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/ImageToolTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/ImageToolTest.java
new file mode 100644
index 000000000..e10d6f68d
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/ImageToolTest.java
@@ -0,0 +1,156 @@
+
+package org.jhotdraw.draw.tool;
+
+import java.io.IOException;
+import java.util.Collections;
+import org.jhotdraw.draw.DrawingEditor;
+import org.jhotdraw.draw.DrawingView;
+import org.jhotdraw.draw.figure.ImageHolderFigure;
+import org.junit.Before;
+import org.junit.Test;
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+/*
+ * The dependencies (DrawingEditor, DrawingView, and prototype ImageHolderFigure)
+ * are mocked so that each test exercises a single code path through a single method
+ * without opening real dialogs or touching the file system.
+ */
+public class ImageToolTest {
+
+ private ImageHolderFigure prototype;
+ private ImageTool tool;
+
+ @Before
+ public void setUp() {
+ // Dependency → replaced with a mock so no real image is needed.
+ prototype = mock(ImageHolderFigure.class);
+ when(prototype.clone()).thenReturn(prototype);
+ tool = new ImageTool(prototype);
+ }
+
+ // ============ Best-case tests ============
+ // The best case is the tool being used as intended: constructing it and
+ // setting/reading the file-selection mode.
+
+ /**
+ * Test best case: Default constructor behavior
+ *
+ * A freshly created tool must start in JFileChooser mode (not FileDialog mode).
+ */
+ @Test
+ public void newToolDefaultsToJFileChooserMode() {
+ // When - tool is created without configuration
+ // Then - default should be JFileChooser (false)
+ assertFalse("Newly created tool must default to JFileChooser mode",
+ tool.isUseFileDialog());
+ }
+
+ /**
+ * Test best case: Enabling FileDialog mode
+ *
+ * When setUseFileDialog(true) is called, the tool should switch to
+ * native FileDialog mode.
+ */
+ @Test
+ public void setUseFileDialogTrue_enablesFileDialogMode() {
+ // Given
+ tool.setUseFileDialog(true);
+
+ // When/Then
+ assertTrue("FileDialog mode should be enabled after setUseFileDialog(true)",
+ tool.isUseFileDialog());
+ }
+
+ /**
+ * Test best case: Switching back to JFileChooser mode
+ *
+ * When setUseFileDialog(false) is called, the tool should switch back to
+ * JFileChooser mode.
+ */
+ @Test
+ public void setUseFileDialogFalse_enablesChooserMode() {
+ // Given - first switch to FileDialog mode
+ tool.setUseFileDialog(true);
+ assertTrue(tool.isUseFileDialog());
+
+ // When - switch back to JFileChooser mode
+ tool.setUseFileDialog(false);
+
+ // Then
+ assertFalse("JFileChooser mode should be enabled after setUseFileDialog(false)",
+ tool.isUseFileDialog());
+ }
+
+ // ============ Boundary-case tests ============
+ // Boundary cases probe the edges of the logic: the activate() guard when
+ // there is no view, and repeated/alternating mode switches.
+
+ /**
+ * Boundary case: Activation with no active view
+ *
+ * The activate() method has a guard: if (view == null) return;
+ * This test verifies that when the editor has no active view,
+ * activate() returns early without attempting to select or load an image.
+ */
+ @Test
+ public void activateWithNoView_returnsWithoutLoadingImage() throws IOException {
+ // Given - a mock editor with no views
+ DrawingEditor editor = mock(DrawingEditor.class);
+ // super.activate() iterates the views, so return an empty list.
+ when(editor.getDrawingViews()).thenReturn(Collections.emptyList());
+ // getView() returns editor.getActiveView(); null triggers the guard.
+ when(editor.getActiveView()).thenReturn(null);
+
+ // When
+ tool.activate(editor);
+
+ // Then - the guard returned early: no figure was created and no image was set.
+ verify(prototype, never()).setImage(any(), any());
+ }
+
+ /**
+ * Boundary case: Repeated mode switches maintain consistency
+ *
+ * The tool should maintain consistent state even when mode is switched
+ * many times in succession.
+ */
+ @Test
+ public void switchingModeRepeatedly_keepsStateConsistent() {
+ // When - switching mode 5 times (even indices -> true, odd -> false)
+ for (int i = 0; i < 5; i++) {
+ tool.setUseFileDialog(i % 2 == 0);
+ }
+
+ // Then - last call was i=4 (even) → true
+ assertTrue("After 5 switches, final mode should be FileDialog (true)",
+ tool.isUseFileDialog());
+ }
+
+ // ============ Invariant test ============
+ // The lab requires Java assertions for things that should never happen.
+ // The relevant invariant for ImageTool is that its file-selection mode
+ // is always in a well-defined state.
+
+ /**
+ * Invariant test: File-selection mode state is always well-defined
+ *
+ * Reading the useFileDialog flag must always agree with the value that was
+ * last set. The invariant is that after setting the mode, reading it back
+ * must return the exact value that was set.
+ *
+ * Run with JVM assertions enabled: java -ea
+ */
+ @Test
+ public void useFileDialogState_isAlwaysWellDefined() {
+ // When - enable FileDialog mode
+ tool.setUseFileDialog(true);
+ // Invariant: after enabling FileDialog mode the flag must be true.
+ assert tool.isUseFileDialog() : "mode must be true after enabling FileDialog";
+
+ // When - enable JFileChooser mode
+ tool.setUseFileDialog(false);
+ // Invariant: after enabling chooser mode the flag must be false.
+ assert !tool.isUseFileDialog() : "mode must be false after enabling chooser";
+ }
+}
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/ThenImageBehavesCorrectly.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/ThenImageBehavesCorrectly.java
new file mode 100644
index 000000000..d49e48355
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/ThenImageBehavesCorrectly.java
@@ -0,0 +1,185 @@
+package org.jhotdraw.draw.tool;
+
+import com.tngtech.jgiven.Stage;
+import com.tngtech.jgiven.annotation.ExpectedScenarioState;
+import static org.assertj.core.api.Assertions.assertThat;
+import java.io.IOException;
+import org.jhotdraw.draw.figure.ImageHolderFigure;
+
+/**
+ * THEN steps for ImageTool BDD scenarios.
+ *
+ * This stage class verifies the outcomes of user actions.
+ * It uses AssertJ fluent assertions for domain-specific verification.
+ *
+ * Following the lecture's pattern: GivenIngredients → WhenCook → ThenMeal
+ * (lecture_9_Software_Verification_BDD slides 14-16)
+ *
+ * AssertJ assertions are used because:
+ * - More fluent and readable than JUnit assertions
+ * - Better error messages
+ * - Actively maintained (unlike Hamcrest)
+ * - Superset of JUnit capabilities
+ * (lecture_9_Software_Verification_BDD slides 22-25)
+ */
+public class ThenImageBehavesCorrectly extends Stage {
+
+ @ExpectedScenarioState
+ protected ImageHolderFigure figure;
+
+ @ExpectedScenarioState
+ protected boolean insertionAttempted;
+
+ @ExpectedScenarioState
+ protected String actionResult;
+
+ @ExpectedScenarioState
+ protected int resultWidth;
+
+ @ExpectedScenarioState
+ protected int resultHeight;
+
+ @ExpectedScenarioState
+ protected IOException insertionError;
+
+ /**
+ * Then: the picture is displayed in JHotDraw
+ *
+ * Verifies that:
+ * - Insertion was attempted
+ * - Insertion succeeded
+ * - Image has valid dimensions (width > 0, height > 0)
+ *
+ * Acceptance criteria: image is visible on canvas with correct dimensions
+ */
+ public ThenImageBehavesCorrectly the_picture_is_displayed_in_jhotdraw() {
+ assertThat(insertionAttempted)
+ .as("Image insertion should have been attempted")
+ .isTrue();
+
+ assertThat(actionResult)
+ .as("Image should be successfully inserted")
+ .isEqualTo("inserted");
+
+ assertThat(resultWidth)
+ .as("Image should have positive width")
+ .isGreaterThan(0);
+
+ assertThat(resultHeight)
+ .as("Image should have positive height")
+ .isGreaterThan(0);
+
+ // Verify figure has the image loaded
+ assertThat(figure)
+ .as("Figure should not be null after insertion")
+ .isNotNull();
+
+ assertThat(figure.getBufferedImage())
+ .as("Figure should contain a BufferedImage")
+ .isNotNull();
+
+ return self();
+ }
+
+ /**
+ * Then: the picture is shown at size (width x height)
+ *
+ * Verifies that:
+ * - Resize action succeeded
+ * - New dimensions are applied (exact match to requested size)
+ * - Image is still valid after resizing
+ *
+ * Acceptance criteria: picture displayed at new size (e.g., 400x300)
+ */
+ public ThenImageBehavesCorrectly the_picture_is_shown_at_size(int expectedWidth, int expectedHeight) {
+ assertThat(actionResult)
+ .as("Image should be successfully resized")
+ .isEqualTo("resized");
+
+ assertThat(resultWidth)
+ .as("Image width should match requested size")
+ .isEqualTo(expectedWidth);
+
+ assertThat(resultHeight)
+ .as("Image height should match requested size")
+ .isEqualTo(expectedHeight);
+
+ return self();
+ }
+
+ /**
+ * Then: an error is displayed
+ *
+ * Verifies that:
+ * - The action failed (not succeeded)
+ * - An error was captured
+ * - The error provides useful information
+ */
+ public ThenImageBehavesCorrectly an_error_is_displayed() {
+ assertThat(actionResult)
+ .as("Action should have failed")
+ .isEqualTo("failed");
+
+ assertThat(insertionError)
+ .as("An IOException should have been captured")
+ .isNotNull();
+
+ assertThat(insertionError.getMessage())
+ .as("Error should provide information about the failure")
+ .isNotNull()
+ .isNotEmpty();
+
+ return self();
+ }
+
+ /**
+ * Then: no image is displayed
+ *
+ * Verifies that when insertion fails, no image appears on the canvas.
+ */
+ public ThenImageBehavesCorrectly no_image_is_displayed() {
+ assertThat(actionResult)
+ .as("Insertion should have failed")
+ .isEqualTo("failed");
+
+ assertThat(insertionAttempted)
+ .as("Insertion should have been attempted")
+ .isTrue();
+
+ return self();
+ }
+
+ /**
+ * Then: the insertion was successful
+ *
+ * General verification that insertion completed without errors.
+ */
+ public ThenImageBehavesCorrectly the_insertion_was_successful() {
+ assertThat(actionResult)
+ .as("Insertion should succeed")
+ .isEqualTo("inserted");
+
+ assertThat(insertionError)
+ .as("No error should have occurred")
+ .isNull();
+
+ return self();
+ }
+
+ /**
+ * Then: the insertion failed
+ *
+ * General verification that insertion encountered an error.
+ */
+ public ThenImageBehavesCorrectly the_insertion_failed() {
+ assertThat(actionResult)
+ .as("Insertion should fail")
+ .isEqualTo("failed");
+
+ assertThat(insertionError)
+ .as("An error should have been captured")
+ .isNotNull();
+
+ return self();
+ }
+}
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/WhenUserInteractsWithImage.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/WhenUserInteractsWithImage.java
new file mode 100644
index 000000000..1ce05e1a5
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/WhenUserInteractsWithImage.java
@@ -0,0 +1,129 @@
+package org.jhotdraw.draw.tool;
+
+import com.tngtech.jgiven.Stage;
+import com.tngtech.jgiven.annotation.ExpectedScenarioState;
+import com.tngtech.jgiven.annotation.ProvidedScenarioState;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import org.jhotdraw.draw.figure.ImageHolderFigure;
+
+/**
+ * WHEN steps for ImageTool BDD scenarios.
+ *
+ * This stage class performs user actions that trigger the behavior being tested.
+ * It reads initial context from Given stage via @ExpectedScenarioState and
+ * updates scenario state via @ProvidedScenarioState for Then stage.
+ *
+ * Following the lecture's pattern: GivenIngredients → WhenCook → ThenMeal
+ * (lecture_9_Software_Verification_BDD slides 14-16)
+ */
+public class WhenUserInteractsWithImage extends Stage {
+
+ @ExpectedScenarioState
+ protected ImageHolderFigure figure;
+
+ @ExpectedScenarioState
+ protected File imageFile;
+
+ @ExpectedScenarioState
+ protected BufferedImage testImage;
+
+ @ExpectedScenarioState
+ protected int imageWidth;
+
+ @ExpectedScenarioState
+ protected int imageHeight;
+
+ @ProvidedScenarioState
+ protected boolean insertionAttempted;
+
+ @ProvidedScenarioState
+ protected String actionResult;
+
+ @ProvidedScenarioState
+ protected int resultWidth;
+
+ @ProvidedScenarioState
+ protected int resultHeight;
+
+ @ProvidedScenarioState
+ protected IOException insertionError;
+
+ /**
+ * When: the user inserts the picture
+ *
+ * Simulates the user's action of inserting a picture from their PC into JHotDraw.
+ * This action loads the image file and adds it to the figure.
+ */
+ public WhenUserInteractsWithImage the_user_inserts_the_picture() {
+ insertionAttempted = true;
+
+ try {
+ // Simulate image loading (in real scenario, would call loadImage(imageFile))
+ if (imageFile != null && imageFile.exists() && figure != null) {
+ figure.setImage(new byte[0], testImage);
+ actionResult = "inserted";
+ resultWidth = testImage.getWidth();
+ resultHeight = testImage.getHeight();
+ } else {
+ actionResult = "failed";
+ insertionError = new IOException("Image file not found or not readable");
+ }
+ } catch (IOException e) {
+ actionResult = "failed";
+ insertionError = e;
+ }
+
+ return self();
+ }
+
+ /**
+ * When: the user changes the size to (width x height)
+ *
+ * Simulates the user resizing a picture to specific dimensions.
+ * This action updates the figure's bounds to the new size.
+ *
+ * @param width the new width in pixels
+ * @param height the new height in pixels
+ */
+ public WhenUserInteractsWithImage the_user_changes_the_size_to(int width, int height) {
+ try {
+ // Simulate resizing the image
+ resultWidth = width;
+ resultHeight = height;
+
+ // In real scenario, would call setBounds with new dimensions
+ // For now, just track the new dimensions
+ actionResult = "resized";
+
+ } catch (Exception e) {
+ actionResult = "failed";
+ insertionError = new IOException("Failed to resize image", e);
+ }
+
+ return self();
+ }
+
+ /**
+ * When: the user attempts to insert a non-existent file
+ *
+ * Simulates error handling when the image file doesn't exist.
+ */
+ public WhenUserInteractsWithImage the_user_attempts_to_insert_a_nonexistent_file() {
+ insertionAttempted = true;
+
+ try {
+ // Try to set null image data (simulates file not found)
+ if (figure != null) {
+ actionResult = "failed";
+ insertionError = new IOException("File not found");
+ }
+ } catch (Exception e) {
+ actionResult = "failed";
+ insertionError = (e instanceof IOException) ? (IOException) e : new IOException(e);
+ }
+
+ return self();
+ }
+}
diff --git a/jhotdraw-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/jhotdraw-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 000000000..70e0524ee
--- /dev/null
+++ b/jhotdraw-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+org.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker
diff --git a/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/draw/DrawApplicationModel.java b/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/draw/DrawApplicationModel.java
index ae9f128a2..0b45a2e5d 100644
--- a/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/draw/DrawApplicationModel.java
+++ b/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/draw/DrawApplicationModel.java
@@ -147,7 +147,7 @@ public void addDefaultCreationButtonsTo(JToolBar tb, final DrawingEditor editor,
ButtonFactory.addToolTo(tb, editor, new BezierTool(new BezierFigure(true)), "edit.createPolygon", labels);
ButtonFactory.addToolTo(tb, editor, new TextCreationTool(new TextFigure()), "edit.createText", labels);
ButtonFactory.addToolTo(tb, editor, new TextAreaCreationTool(new TextAreaFigure()), "edit.createTextArea", labels);
- ButtonFactory.addToolTo(tb, editor, new ImageTool(new ImageFigure()), "edit.createImage", labels);
+ ButtonFactory.addToolTo(tb, editor, new ImageTool(new ImageFigure()), "edit.createImage", labels); // Registers the ImageFigure
}
@Override