diff --git a/jhotdraw-app/pom.xml b/jhotdraw-app/pom.xml
index a36a532b5..c2a483b51 100644
--- a/jhotdraw-app/pom.xml
+++ b/jhotdraw-app/pom.xml
@@ -24,5 +24,24 @@
jhotdraw-gui
${project.version}
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+ org.mockito
+ mockito-core
+ 3.12.4
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.21.0
+ test
+
\ No newline at end of file
diff --git a/jhotdraw-app/src/main/java/org/jhotdraw/app/internal/ChooserManager.java b/jhotdraw-app/src/main/java/org/jhotdraw/app/internal/ChooserManager.java
new file mode 100644
index 000000000..b326bdbb7
--- /dev/null
+++ b/jhotdraw-app/src/main/java/org/jhotdraw/app/internal/ChooserManager.java
@@ -0,0 +1,166 @@
+
+package org.jhotdraw.app.internal;
+
+import org.jhotdraw.api.app.Application;
+import org.jhotdraw.api.app.ApplicationModel;
+import org.jhotdraw.api.app.View;
+import org.jhotdraw.api.gui.URIChooser;
+public class ChooserManager {
+
+ private URIChooser openChooser;
+ private URIChooser saveChooser;
+ private URIChooser importChooser;
+ private URIChooser exportChooser;
+
+ private final Application application;
+ private final ApplicationModel model;
+
+ public ChooserManager(Application application, ApplicationModel model) {
+ this.application = application;
+ this.model = model;
+ }
+
+ /**
+ * Gets an open chooser for the specified view or for the application.
+ * Implements Lazy Initialization pattern.
+ */
+ public URIChooser getOpenChooser(View v) {
+ if (v == null) {
+ return getApplicationOpenChooser();
+ } else {
+ return getViewOpenChooser(v);
+ }
+ }
+
+ /**
+ * Gets a save chooser for the specified view or for the application.
+ */
+ public URIChooser getSaveChooser(View v) {
+ if (v == null) {
+ return getApplicationSaveChooser();
+ } else {
+ return getViewSaveChooser(v);
+ }
+ }
+
+ /**
+ * Gets an import chooser for the specified view or for the application.
+ */
+ public URIChooser getImportChooser(View v) {
+ if (v == null) {
+ return getApplicationImportChooser();
+ } else {
+ return getViewImportChooser(v);
+ }
+ }
+
+ /**
+ * Gets an export chooser for the specified view or for the application.
+ */
+ public URIChooser getExportChooser(View v) {
+ if (v == null) {
+ return getApplicationExportChooser();
+ } else {
+ return getViewExportChooser(v);
+ }
+ }
+
+ // Application-level choosers (singletons)
+
+ private URIChooser getApplicationOpenChooser() {
+ if (openChooser == null) {
+ openChooser = createAndConfigureChooser(() -> model.createOpenChooser(application, null));
+ }
+ return openChooser;
+ }
+
+ private URIChooser getApplicationSaveChooser() {
+ if (saveChooser == null) {
+ saveChooser = createAndConfigureChooser(() -> model.createSaveChooser(application, null));
+ }
+ return saveChooser;
+ }
+
+ private URIChooser getApplicationImportChooser() {
+ if (importChooser == null) {
+ importChooser = createAndConfigureChooser(() -> model.createImportChooser(application, null));
+ }
+ return importChooser;
+ }
+
+ private URIChooser getApplicationExportChooser() {
+ if (exportChooser == null) {
+ exportChooser = createAndConfigureChooser(() -> model.createExportChooser(application, null));
+ }
+ return exportChooser;
+ }
+
+ // View-specific choosers
+
+ private URIChooser getViewOpenChooser(View v) {
+ return getOrCreateViewChooser(v, "openChooser", () -> model.createOpenChooser(application, v));
+ }
+
+ private URIChooser getViewSaveChooser(View v) {
+ return getOrCreateViewChooser(v, "saveChooser", () -> model.createSaveChooser(application, v));
+ }
+
+ private URIChooser getViewImportChooser(View v) {
+ return getOrCreateViewChooser(v, "importChooser", () -> model.createImportChooser(application, v));
+ }
+
+ private URIChooser getViewExportChooser(View v) {
+ return getOrCreateViewChooser(v, "exportChooser", () -> model.createExportChooser(application, v));
+ }
+
+ // Helper methods using Strategy pattern for chooser creation
+
+ /**
+ * Gets or creates a view-specific chooser using the provided factory.
+ * Implements Template Method pattern for view chooser management.
+ */
+ private URIChooser getOrCreateViewChooser(View v, String clientPropertyKey, ChooserFactory factory) {
+ URIChooser chooser = (URIChooser) v.getComponent().getClientProperty(clientPropertyKey);
+ if (chooser == null) {
+ chooser = factory.createChooser();
+ v.getComponent().putClientProperty(clientPropertyKey, chooser);
+ setupViewChooserProperties(chooser, v);
+ }
+ return chooser;
+ }
+
+ /**
+ * Creates and configures an application-level chooser.
+ */
+ private URIChooser createAndConfigureChooser(ChooserFactory factory) {
+ URIChooser chooser = factory.createChooser();
+ chooser.getComponent().putClientProperty("application", application);
+ return chooser;
+ }
+
+ /**
+ * Sets up client properties for view-specific choosers.
+ */
+ private void setupViewChooserProperties(URIChooser chooser, View view) {
+ chooser.getComponent().putClientProperty("view", view);
+ chooser.getComponent().putClientProperty("application", application);
+ }
+
+ /**
+ * Functional interface for chooser creation strategy.
+ */
+ @FunctionalInterface
+ private interface ChooserFactory {
+ URIChooser createChooser();
+ }
+
+ /**
+ * Clears all cached choosers - useful for testing or configuration changes.
+ */
+ public void clearCachedChoosers() {
+ openChooser = null;
+ saveChooser = null;
+ importChooser = null;
+ exportChooser = null;
+ }
+}
diff --git a/jhotdraw-app/src/main/java/org/jhotdraw/app/internal/RecentFilesManager.java b/jhotdraw-app/src/main/java/org/jhotdraw/app/internal/RecentFilesManager.java
new file mode 100644
index 000000000..c29a06a9a
--- /dev/null
+++ b/jhotdraw-app/src/main/java/org/jhotdraw/app/internal/RecentFilesManager.java
@@ -0,0 +1,141 @@
+/*
+ * @(#)RecentFilesManager.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.app.internal;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.prefs.Preferences;
+
+public class RecentFilesManager {
+
+ private static final int MAX_RECENT_FILES_COUNT = 10;
+ private final LinkedList recentURIs = new LinkedList<>();
+ private final Preferences preferences;
+ private static final int RECENT_FILE_COUNT = 0;
+ private static final String RECENT_FILES_KEY = "recentFile.";
+ public RecentFilesManager(Preferences preferences) throws URISyntaxException {
+ this.preferences = preferences;
+ loadRecentFiles();
+ }
+
+ /**
+ * Loads recent files from preferences using Template Method pattern.
+ */
+ private void loadRecentFiles() throws URISyntaxException {
+ int count = preferences.getInt(String.valueOf(RECENT_FILE_COUNT), 0);
+ for (int i = 0; i < count; i++) {
+ String path = preferences.get(RECENT_FILES_KEY + i, null);
+ if (path != null) {
+ addRecentURIInternal(new URI(path));
+ }
+ }
+ }
+
+ /**
+ * Saves recent files to preferences.
+ */
+ public void saveRecentFiles() {
+ preferences.putInt(String.valueOf(RECENT_FILE_COUNT), recentURIs.size());
+ for (int i = 0; i < recentURIs.size(); i++) {
+ preferences.put(RECENT_FILES_KEY + i, recentURIs.get(i).toString());
+ }
+ }
+
+ /**
+ * Adds a URI to the recent files list.
+ * Implements the strategy of keeping most recent at the front.
+ */
+ public void addRecentURI(URI uri) {
+ if (uri == null) {
+ return;
+ }
+
+ // Remove if already exists to avoid duplicates
+ recentURIs.remove(uri);
+
+ // Add to front
+ recentURIs.addFirst(uri);
+
+ // Limit size
+ while (recentURIs.size() > MAX_RECENT_FILES_COUNT) {
+ recentURIs.removeLast();
+ }
+
+ saveRecentFiles();
+ }
+
+ /**
+ * Internal method for adding without saving (used during loading).
+ */
+ private void addRecentURIInternal(URI uri) {
+ if (uri != null && !recentURIs.contains(uri)) {
+ recentURIs.add(uri);
+ }
+ }
+
+ /**
+ * Gets the list of recent URIs.
+ * Returns a copy to prevent external modification.
+ */
+ public List getRecentURIs() {
+ return new LinkedList<>(recentURIs);
+ }
+
+ /**
+ * Clears all recent files.
+ */
+ public void clearRecentFiles() {
+ recentURIs.clear();
+ preferences.putInt(String.valueOf(RECENT_FILE_COUNT), 0);
+ // Remove all stored recent file entries
+ for (int i = 0; i < MAX_RECENT_FILES_COUNT; i++) {
+ preferences.remove(RECENT_FILES_KEY + i);
+ }
+ }
+
+ /**
+ * Removes a specific URI from the recent files list.
+ */
+ public boolean removeRecentURI(URI uri) {
+ boolean removed = recentURIs.remove(uri);
+ if (removed) {
+ saveRecentFiles();
+ }
+ return removed;
+ }
+
+ /**
+ * Gets the most recent URI, or null if none exists.
+ */
+ public URI getMostRecentURI() {
+ return recentURIs.isEmpty() ? null : recentURIs.getFirst();
+ }
+
+ /**
+ * Checks if the recent files list is empty.
+ */
+ public boolean isEmpty() {
+ return recentURIs.isEmpty();
+ }
+
+ /**
+ * Gets the maximum number of recent files that can be stored.
+ */
+ public int getMaxRecentFilesCount() {
+ return MAX_RECENT_FILES_COUNT;
+ }
+
+ /**
+ * Gets the current number of recent files.
+ */
+ public int getRecentFilesCount() {
+ return recentURIs.size();
+ }
+}
diff --git a/jhotdraw-app/src/main/java/org/jhotdraw/app/internal/ViewManager.java b/jhotdraw-app/src/main/java/org/jhotdraw/app/internal/ViewManager.java
new file mode 100644
index 000000000..9b5f917d2
--- /dev/null
+++ b/jhotdraw-app/src/main/java/org/jhotdraw/app/internal/ViewManager.java
@@ -0,0 +1,158 @@
+package org.jhotdraw.app.internal;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import org.jhotdraw.api.app.Application;
+import org.jhotdraw.api.app.ApplicationModel;
+import org.jhotdraw.api.app.View;
+
+public class ViewManager {
+
+ public static final String ACTIVE_VIEW_PROPERTY = "activeView";
+ public static final String VIEW_COUNT_PROPERTY = "viewCount";
+
+ private final LinkedList views = new LinkedList<>();
+ private Collection unmodifiableViews;
+ private View activeView;
+ private final PropertyChangeSupport propertyChangeSupport;
+ private final Application application;
+ private final ApplicationModel model;
+
+ public ViewManager(Application application, ApplicationModel model) {
+ this.application = application;
+ this.model = model;
+ this.propertyChangeSupport = new PropertyChangeSupport(this);
+ }
+
+ /**
+ * Sets the active view. Calls deactivate on the previously
+ * active view, and then calls activate on the given view.
+ *
+ * @param newValue Active view, can be null.
+ */
+ public void setActiveView(View newValue) {
+ View oldValue = activeView;
+ if (activeView != null) {
+ activeView.deactivate();
+ }
+ activeView = newValue;
+ if (activeView != null) {
+ activeView.activate();
+ }
+ firePropertyChange(ACTIVE_VIEW_PROPERTY, oldValue, newValue);
+ }
+
+ /**
+ * Gets the active view.
+ *
+ * @return The active view can be null.
+ */
+ public View getActiveView() {
+ return activeView;
+ }
+
+ /**
+ * Adds a view to the application.
+ * Follows the Single Responsibility Principle by focusing only on view management.
+ */
+ public void add(View v) {
+ if (v.getApplication() != application) {
+ int oldCount = views.size();
+ views.add(v);
+ v.setApplication(application);
+ v.init();
+ model.initView(application, v);
+ firePropertyChange(VIEW_COUNT_PROPERTY, oldCount, views.size());
+ }
+ }
+
+ /**
+ * Removes a view from the application.
+ */
+ public void remove(View v) {
+ if (v == getActiveView()) {
+ setActiveView(null);
+ }
+ int oldCount = views.size();
+ views.remove(v);
+ v.setApplication(null);
+ firePropertyChange(VIEW_COUNT_PROPERTY, oldCount, views.size());
+ }
+
+ /**
+ * Gets an unmodifiable list of all views.
+ */
+ public List getViews() {
+ return Collections.unmodifiableList(views);
+ }
+
+ /**
+ * Gets an unmodifiable collection of all views.
+ * Uses lazy initialization pattern.
+ */
+ public Collection views() {
+ if (unmodifiableViews == null) {
+ unmodifiableViews = Collections.unmodifiableCollection(views);
+ }
+ return unmodifiableViews;
+ }
+
+ /**
+ * Gets the count of currently managed views.
+ */
+ public int getViewCount() {
+ return views.size();
+ }
+
+ /**
+ * Checks if a view is currently managed by this manager.
+ */
+ public boolean containsView(View view) {
+ return views.contains(view);
+ }
+
+ /**
+ * Clears all views - used during application shutdown.
+ */
+ public void clearAllViews() {
+ // Create a copy to avoid concurrent modification
+ List viewsCopy = new LinkedList<>(views);
+ for (View view : viewsCopy) {
+ remove(view);
+ }
+ setActiveView(null);
+ }
+
+ // Property change support methods
+ public void addPropertyChangeListener(PropertyChangeListener listener) {
+ propertyChangeSupport.addPropertyChangeListener(listener);
+ }
+
+ public void removePropertyChangeListener(PropertyChangeListener listener) {
+ propertyChangeSupport.removePropertyChangeListener(listener);
+ }
+
+ public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
+ propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
+ }
+
+ public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
+ propertyChangeSupport.removePropertyChangeListener(propertyName, listener);
+ }
+
+ protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
+ propertyChangeSupport.firePropertyChange(propertyName, oldValue, newValue);
+ }
+
+ protected void firePropertyChange(String propertyName, int oldValue, int newValue) {
+ propertyChangeSupport.firePropertyChange(propertyName, oldValue, newValue);
+ }
+
+ protected void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) {
+ propertyChangeSupport.firePropertyChange(propertyName, oldValue, newValue);
+ }
+}
diff --git a/jhotdraw-app/src/test/java/org/jhotdraw/app/internal/ViewManagerTest.java b/jhotdraw-app/src/test/java/org/jhotdraw/app/internal/ViewManagerTest.java
new file mode 100644
index 000000000..70cde7e9b
--- /dev/null
+++ b/jhotdraw-app/src/test/java/org/jhotdraw/app/internal/ViewManagerTest.java
@@ -0,0 +1,313 @@
+package org.jhotdraw.app.internal;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.Collection;
+import java.util.List;
+import org.jhotdraw.api.app.Application;
+import org.jhotdraw.api.app.ApplicationModel;
+import org.jhotdraw.api.app.View;
+import org.junit.*;
+import static org.assertj.core.api.Assertions.*;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import static org.mockito.Mockito.*;
+
+public class ViewManagerTest {
+
+ @Mock
+ private Application mockApplication;
+
+ @Mock
+ private ApplicationModel mockModel;
+
+ @Mock
+ private View mockView1;
+
+ @Mock
+ private View mockView2;
+
+ @Mock
+ private PropertyChangeListener mockPropertyListener;
+
+ private ViewManager viewManager;
+
+ @BeforeClass
+ public static void setUpClass() {
+ }
+
+ @AfterClass
+ public static void tearDownClass() {
+ }
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.openMocks(this);
+ viewManager = new ViewManager(mockApplication, mockModel);
+
+ // Setup mock behavior
+ when(mockView1.getApplication()).thenReturn(null).thenReturn(mockApplication);
+ when(mockView2.getApplication()).thenReturn(null).thenReturn(mockApplication);
+ }
+
+ @After
+ public void tearDown() {
+ viewManager = null;
+ }
+
+ // BASIC FUNCTIONALITY TESTS
+
+ /**
+ * Test initial state of ViewManager.
+ */
+ @Test
+ public void testInitialState() {
+ assertThat(viewManager.getViewCount()).isZero();
+ assertThat(viewManager.getActiveView()).isNull();
+ assertThat(viewManager.getViews()).isEmpty();
+ assertThat(viewManager.views()).isEmpty();
+ }
+
+ /**
+ * Test adding a single view.
+ */
+ @Test
+ public void testAddView() {
+ viewManager.addPropertyChangeListener(ViewManager.VIEW_COUNT_PROPERTY, mockPropertyListener);
+
+ viewManager.add(mockView1);
+
+ assertThat(viewManager.getViewCount()).isEqualTo(1);
+ assertThat(viewManager.getViews()).containsExactly(mockView1);
+ assertThat(viewManager.containsView(mockView1)).isTrue();
+
+ // Verify proper initialization sequence
+ verify(mockView1).setApplication(mockApplication);
+ verify(mockView1).init();
+ verify(mockModel).initView(mockApplication, mockView1);
+
+ // Verify property change event
+ verify(mockPropertyListener).propertyChange(any(PropertyChangeEvent.class));
+ }
+
+ /**
+ * Test adding multiple views.
+ */
+ @Test
+ public void testAddMultipleViews() {
+ viewManager.add(mockView1);
+ viewManager.add(mockView2);
+
+ assertThat(viewManager.getViewCount()).isEqualTo(2);
+ assertThat(viewManager.getViews()).containsExactly(mockView1, mockView2);
+ }
+
+ /**
+ * Test removing a view.
+ */
+ @Test
+ public void testRemoveView() {
+ viewManager.add(mockView1);
+ viewManager.addPropertyChangeListener(ViewManager.VIEW_COUNT_PROPERTY, mockPropertyListener);
+
+ viewManager.remove(mockView1);
+
+ assertThat(viewManager.getViewCount()).isZero();
+ assertThat(viewManager.containsView(mockView1)).isFalse();
+
+ verify(mockView1).setApplication(null);
+ verify(mockPropertyListener).propertyChange(any(PropertyChangeEvent.class));
+ }
+
+ // ACTIVE VIEW TESTS
+
+ /**
+ * Test setting and getting active view.
+ */
+ @Test
+ public void testSetActiveView() {
+ viewManager.addPropertyChangeListener(ViewManager.ACTIVE_VIEW_PROPERTY, mockPropertyListener);
+
+ viewManager.setActiveView(mockView1);
+
+ assertThat(viewManager.getActiveView()).isEqualTo(mockView1);
+ verify(mockView1).activate();
+ verify(mockPropertyListener).propertyChange(any(PropertyChangeEvent.class));
+ }
+
+ /**
+ * Test changing active view deactivates previous view.
+ */
+ @Test
+ public void testChangeActiveViewDeactivatesPrevious() {
+ viewManager.setActiveView(mockView1);
+
+ viewManager.setActiveView(mockView2);
+
+ assertThat(viewManager.getActiveView()).isEqualTo(mockView2);
+ verify(mockView1).deactivate();
+ verify(mockView2).activate();
+ }
+
+ /**
+ * Test removing active view sets active view to null.
+ */
+ @Test
+ public void testRemoveActiveViewSetsActiveToNull() {
+ viewManager.add(mockView1);
+ viewManager.setActiveView(mockView1);
+
+ viewManager.remove(mockView1);
+
+ assertThat(viewManager.getActiveView()).isNull();
+ }
+
+ /**
+ * Test setting active view to null deactivates current view.
+ */
+ @Test
+ public void testSetActiveViewToNull() {
+ viewManager.setActiveView(mockView1);
+
+ viewManager.setActiveView(null);
+
+ assertThat(viewManager.getActiveView()).isNull();
+ verify(mockView1).deactivate();
+ }
+
+ // EDGE CASES AND ERROR CONDITIONS
+
+ /**
+ * Test adding same view twice does nothing.
+ */
+ @Test
+ public void testAddSameViewTwiceDoesNothing() {
+ when(mockView1.getApplication()).thenReturn(null).thenReturn(mockApplication);
+
+ viewManager.add(mockView1);
+ viewManager.add(mockView1); // Second add should do nothing
+
+ assertThat(viewManager.getViewCount()).isEqualTo(1);
+ verify(mockView1, times(1)).setApplication(mockApplication);
+ verify(mockView1, times(1)).init();
+ }
+
+ /**
+ * Test removing view that is not managed.
+ */
+ @Test
+ public void testRemoveUnmanagedView() {
+ int initialCount = viewManager.getViewCount();
+
+ viewManager.remove(mockView1);
+
+ assertThat(viewManager.getViewCount()).isEqualTo(initialCount);
+ }
+
+ /**
+ * Test clearing all views.
+ */
+ @Test
+ public void testClearAllViews() {
+ viewManager.add(mockView1);
+ viewManager.add(mockView2);
+ viewManager.setActiveView(mockView1);
+
+ viewManager.clearAllViews();
+
+ assertThat(viewManager.getViewCount()).isZero();
+ assertThat(viewManager.getActiveView()).isNull();
+ verify(mockView1).setApplication(null);
+ verify(mockView2).setApplication(null);
+ }
+
+ // COLLECTION IMMUTABILITY TESTS
+
+ /**
+ * Test that getViews returns immutable list.
+ */
+ @Test
+ public void testGetViewsReturnsImmutableList() {
+ viewManager.add(mockView1);
+ List views = viewManager.getViews();
+
+ assertThatThrownBy(() -> views.add(mockView2))
+ .isInstanceOf(UnsupportedOperationException.class);
+ }
+
+ /**
+ * Test that views() returns immutable collection.
+ */
+ @Test
+ public void testViewsReturnsImmutableCollection() {
+ viewManager.add(mockView1);
+ Collection views = viewManager.views();
+
+ assertThatThrownBy(() -> views.add(mockView2))
+ .isInstanceOf(UnsupportedOperationException.class);
+ }
+
+ // PROPERTY CHANGE TESTS
+
+ /**
+ * Test property change listener registration and removal.
+ */
+ @Test
+ public void testPropertyChangeListenerManagement() {
+ viewManager.addPropertyChangeListener(mockPropertyListener);
+ viewManager.removePropertyChangeListener(mockPropertyListener);
+
+ viewManager.setActiveView(mockView1);
+
+ // Should not receive events after removal
+ verifyNoInteractions(mockPropertyListener);
+ }
+
+ /**
+ * Test property-specific listener registration.
+ */
+ @Test
+ public void testPropertySpecificListener() {
+ viewManager.addPropertyChangeListener(ViewManager.ACTIVE_VIEW_PROPERTY, mockPropertyListener);
+
+ viewManager.add(mockView1); // Should not trigger listener
+ viewManager.setActiveView(mockView1); // Should trigger listener
+
+ verify(mockPropertyListener, times(1)).propertyChange(any(PropertyChangeEvent.class));
+ }
+
+ // JAVA ASSERTIONS FOR INVARIANTS
+
+ /**
+ * Test invariant: active view must be in the views collection or null.
+ */
+ @Test
+ public void testActiveViewInvariant() {
+ viewManager.add(mockView1);
+ viewManager.setActiveView(mockView1);
+
+ assert viewManager.getActiveView() == null || viewManager.containsView(viewManager.getActiveView())
+ : "Active view must be in views collection or null";
+
+ viewManager.remove(mockView1);
+ assert viewManager.getActiveView() == null
+ : "Active view should be null after removing it from collection";
+ }
+
+ /**
+ * Test invariant: view count should match actual collection size.
+ */
+ @Test
+ public void testViewCountInvariant() {
+ assert viewManager.getViewCount() == viewManager.getViews().size()
+ : "View count should match collection size";
+
+ viewManager.add(mockView1);
+ assert viewManager.getViewCount() == viewManager.getViews().size()
+ : "View count should match collection size after add";
+
+ viewManager.remove(mockView1);
+ assert viewManager.getViewCount() == viewManager.getViews().size()
+ : "View count should match collection size after remove";
+ }
+}
diff --git a/jhotdraw-core/pom.xml b/jhotdraw-core/pom.xml
index 7c276da85..866c102bc 100644
--- a/jhotdraw-core/pom.xml
+++ b/jhotdraw-core/pom.xml
@@ -29,10 +29,39 @@
jhotdraw-datatransfer
${project.version}
+
- org.testng
- testng
- 6.8.21
+ junit
+ junit
+ 4.13.2
+ test
+
+
+
+ org.mockito
+ mockito-core
+ 3.12.4
+ test
+
+
+
+ com.tngtech.jgiven
+ jgiven-junit
+ 1.2.5
+ test
+
+
+
+ org.assertj
+ assertj-core
+ 3.21.0
+ test
+
+
+
+ org.assertj
+ assertj-swing-junit
+ 3.17.1
test
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/AbstractFigureNGTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/AbstractFigureNGTest.java
deleted file mode 100644
index 91daec493..000000000
--- a/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/AbstractFigureNGTest.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright (C) 2015 JHotDraw.
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301 USA
- */
-package org.jhotdraw.draw.figure;
-
-import java.awt.Graphics2D;
-import java.awt.geom.AffineTransform;
-import java.awt.geom.Point2D;
-import java.awt.geom.Rectangle2D;
-import java.util.Map;
-import org.jhotdraw.draw.AttributeKey;
-import static org.testng.Assert.*;
-import org.testng.annotations.AfterClass;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-/**
- *
- * @author tw
- */
-public class AbstractFigureNGTest {
-
- public AbstractFigureNGTest() {
- }
-
- @BeforeClass
- public static void setUpClass() throws Exception {
- }
-
- @AfterClass
- public static void tearDownClass() throws Exception {
- }
-
- @BeforeMethod
- public void setUpMethod() throws Exception {
- }
-
- @AfterMethod
- public void tearDownMethod() throws Exception {
- }
-
- @Test(expectedExceptions = IllegalStateException.class)
- public void testChangedWithoutWillChange() {
- new AbstractFigureImpl().changed();
- }
-
- @Test
- public void testWillChangeChangedEvents() {
- AbstractFigure figure = new AbstractFigureImpl();
- assertEquals(figure.getChangingDepth(), 0);
- figure.willChange();
- assertEquals(figure.getChangingDepth(), 1);
- figure.willChange();
- assertEquals(figure.getChangingDepth(), 2);
- figure.changed();
- assertEquals(figure.getChangingDepth(), 1);
- figure.changed();
- assertEquals(figure.getChangingDepth(), 0);
- }
-
- public class AbstractFigureImpl extends AbstractFigure {
-
- @Override
- public void draw(Graphics2D g) {
- }
-
- @Override
- public Rectangle2D.Double getBounds() {
- return null;
- }
-
- @Override
- public Rectangle2D.Double getDrawingArea() {
- return null;
- }
-
- @Override
- public boolean contains(Point2D.Double p) {
- return true;
- }
-
- @Override
- public Object getTransformRestoreData() {
- return null;
- }
-
- @Override
- public void restoreTransformTo(Object restoreData) {
- }
-
- @Override
- public void transform(AffineTransform tx) {
- }
-
- @Override
- public void set(AttributeKey key, T value) {
- }
-
- @Override
- public T get(AttributeKey key) {
- throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
- }
-
- @Override
- public Map, Object> getAttributes() {
- throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
- }
-
- @Override
- public Object getAttributesRestoreData() {
- throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
- }
-
- @Override
- public void restoreAttributesTo(Object restoreData) {
- throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
- }
-
- @Override
- public Rectangle2D.Double getDrawingArea(double factor) {
- return null;
- }
- }
-}
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/AbstractFigureTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/AbstractFigureTest.java
new file mode 100644
index 000000000..605d7f92b
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/AbstractFigureTest.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2015 JHotDraw.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.jhotdraw.draw.figure;
+
+import java.awt.Graphics2D;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.util.Map;
+import org.jhotdraw.draw.AttributeKey;
+import org.junit.*;
+import static org.assertj.core.api.Assertions.*;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for AbstractFigure using JUnit 4 and AssertJ.
+ * Tests focus on core figure behavior, attribute management, and geometric operations.
+ *
+ * @author tw
+ */
+public class AbstractFigureTest {
+
+ @Mock
+ private Graphics2D mockGraphics;
+
+ private TestFigure figure;
+
+ @BeforeClass
+ public static void setUpClass() throws Exception {
+ }
+
+ @AfterClass
+ public static void tearDownClass() throws Exception {
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.openMocks(this);
+ figure = new TestFigure();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ }
+
+ /**
+ * Test that calling changed() without willChange() throws exception.
+ * This is a crucial invariant that should never be violated.
+ */
+ @Test(expected = IllegalStateException.class)
+ public void testChangedWithoutWillChange() {
+ new TestFigure().changed();
+ }
+
+ /**
+ * Test the willChange/changed event depth tracking.
+ * Validates proper nesting of change events.
+ */
+ @Test
+ public void testWillChangeChangedEvents() {
+ TestFigure testFigure = new TestFigure();
+
+ // Initially no changes are pending
+ assertThat(testFigure.getChangingDepth()).isZero();
+
+ // First willChange call
+ testFigure.willChange();
+ assertThat(testFigure.getChangingDepth()).isEqualTo(1);
+
+ // Nested willChange call
+ testFigure.willChange();
+ assertThat(testFigure.getChangingDepth()).isEqualTo(2);
+
+ // First changed call
+ testFigure.changed();
+ assertThat(testFigure.getChangingDepth()).isEqualTo(1);
+
+ // Final changed call
+ testFigure.changed();
+ assertThat(testFigure.getChangingDepth()).isZero();
+ }
+
+ /**
+ * Test boundary condition: multiple nested willChange calls.
+ */
+ @Test
+ public void testDeepNestedWillChangeEvents() {
+ TestFigure testFigure = new TestFigure();
+
+ // Create deep nesting
+ for (int i = 1; i <= 10; i++) {
+ testFigure.willChange();
+ assertThat(testFigure.getChangingDepth()).isEqualTo(i);
+ }
+
+ // Unwind the nesting
+ for (int i = 9; i >= 0; i--) {
+ testFigure.changed();
+ assertThat(testFigure.getChangingDepth()).isEqualTo(i);
+ }
+ }
+
+ /**
+ * Test figure drawing behavior with mock graphics context.
+ */
+ @Test
+ public void testDrawWithMockGraphics() {
+ // This test demonstrates mocking for isolating drawing behavior
+ figure.draw(mockGraphics);
+ // In a real implementation, we would verify specific drawing calls
+ // For now, just ensure no exceptions are thrown
+ assertThat(figure).isNotNull();
+ }
+
+ /**
+ * Test figure bounds calculation - edge case with null bounds.
+ */
+ @Test
+ public void testGetBoundsReturnsNull() {
+ Rectangle2D.Double bounds = figure.getBounds();
+ assertThat(bounds).isNull(); // Current implementation returns null
+ }
+
+ /**
+ * Test figure containment - current implementation always returns true.
+ */
+ @Test
+ public void testContainsPoint() {
+ Point2D.Double point = new Point2D.Double(10, 10);
+ boolean contains = figure.contains(point);
+ assertThat(contains).isTrue(); // Current implementation
+ }
+
+ /**
+ * Test helper class that extends AbstractFigure for testing purposes.
+ * This class provides minimal implementations needed for testing.
+ */
+ private static class TestFigure extends AbstractFigure {
+
+ @Override
+ public void draw(Graphics2D g) {
+ // Minimal implementation for testing
+ }
+
+ @Override
+ public Rectangle2D.Double getBounds() {
+ return null; // Simplified for testing
+ }
+
+ @Override
+ public Rectangle2D.Double getDrawingArea() {
+ return null; // Simplified for testing
+ }
+
+ @Override
+ public boolean contains(Point2D.Double p) {
+ return true; // Simplified for testing
+ }
+
+ @Override
+ public Object getTransformRestoreData() {
+ return null; // Simplified for testing
+ }
+
+ @Override
+ public void restoreTransformTo(Object restoreData) {
+ // Minimal implementation for testing
+ }
+
+ @Override
+ public void transform(AffineTransform tx) {
+ // Minimal implementation for testing
+ }
+
+ @Override
+ public void set(AttributeKey key, T value) {
+ // Minimal implementation for testing
+ }
+
+ @Override
+ public T get(AttributeKey key) {
+ return null; // Simplified for testing
+ }
+
+ @Override
+ public Map, Object> getAttributes() {
+ return java.util.Collections.emptyMap(); // Simplified for testing
+ }
+
+ @Override
+ public Object getAttributesRestoreData() {
+ return null; // Simplified for testing
+ }
+
+ @Override
+ public void restoreAttributesTo(Object restoreData) {
+ // Minimal implementation for testing
+ }
+
+ @Override
+ public Rectangle2D.Double getDrawingArea(double factor) {
+ return null; // Simplified for testing
+ }
+ }
+}
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/RectangleFigureTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/RectangleFigureTest.java
new file mode 100644
index 000000000..1e2175e37
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/RectangleFigureTest.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2015 JHotDraw.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.jhotdraw.draw.figure;
+
+import java.awt.Graphics2D;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import org.junit.*;
+import static org.assertj.core.api.Assertions.*;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import static org.mockito.Mockito.*;
+
+/**
+ * Comprehensive unit tests for RectangleFigure using JUnit 4, AssertJ, and Mockito.
+ * Tests focus on geometric operations, boundary conditions, and drawing behavior.
+ *
+ * @author JHotDraw Team
+ */
+public class RectangleFigureTest {
+
+ @Mock
+ private Graphics2D mockGraphics;
+
+ private RectangleFigure rectangle;
+
+ @BeforeClass
+ public static void setUpClass() {
+ // Class-level setup if needed
+ }
+
+ @AfterClass
+ public static void tearDownClass() {
+ // Class-level cleanup if needed
+ }
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.openMocks(this);
+ rectangle = new RectangleFigure();
+ }
+
+ @After
+ public void tearDown() {
+ rectangle = null;
+ }
+
+ // CONSTRUCTOR TESTS
+
+ /**
+ * Test default constructor creates rectangle at origin with zero dimensions.
+ */
+ @Test
+ public void testDefaultConstructor() {
+ RectangleFigure rect = new RectangleFigure();
+ Rectangle2D.Double bounds = rect.getBounds();
+
+ assertThat(bounds.x).isZero();
+ assertThat(bounds.y).isZero();
+ assertThat(bounds.width).isZero();
+ assertThat(bounds.height).isZero();
+ }
+
+ /**
+ * Test parameterized constructor sets dimensions correctly.
+ */
+ @Test
+ public void testParameterizedConstructor() {
+ RectangleFigure rect = new RectangleFigure(10, 20, 100, 50);
+ Rectangle2D.Double bounds = rect.getBounds();
+
+ assertThat(bounds.x).isEqualTo(10.0);
+ assertThat(bounds.y).isEqualTo(20.0);
+ assertThat(bounds.width).isEqualTo(100.0);
+ assertThat(bounds.height).isEqualTo(50.0);
+ }
+
+ // BOUNDARY TESTS
+
+ /**
+ * Test setBounds with normal positive values.
+ */
+ @Test
+ public void testSetBounds() {
+ Point2D.Double anchor = new Point2D.Double(10, 10);
+ Point2D.Double lead = new Point2D.Double(50, 30);
+
+ rectangle.setBounds(anchor, lead);
+ Rectangle2D.Double bounds = rectangle.getBounds();
+
+ assertThat(bounds.x).isEqualTo(10.0);
+ assertThat(bounds.y).isEqualTo(10.0);
+ assertThat(bounds.width).isEqualTo(40.0);
+ assertThat(bounds.height).isEqualTo(20.0);
+ }
+
+ /**
+ * Test setBounds with inverted anchor and lead points (lead before anchor).
+ */
+ @Test
+ public void testSetBoundsInvertedPoints() {
+ Point2D.Double anchor = new Point2D.Double(50, 30);
+ Point2D.Double lead = new Point2D.Double(10, 10);
+
+ rectangle.setBounds(anchor, lead);
+ Rectangle2D.Double bounds = rectangle.getBounds();
+
+ // Should normalize to proper rectangle
+ assertThat(bounds.x).isEqualTo(10.0);
+ assertThat(bounds.y).isEqualTo(10.0);
+ assertThat(bounds.width).isEqualTo(40.0);
+ assertThat(bounds.height).isEqualTo(20.0);
+ }
+
+ /**
+ * Test setBounds with zero-width rectangle enforces minimum width.
+ */
+ @Test
+ public void testSetBoundsZeroWidth() {
+ Point2D.Double anchor = new Point2D.Double(10, 10);
+ Point2D.Double lead = new Point2D.Double(10, 30); // Same x-coordinate
+
+ rectangle.setBounds(anchor, lead);
+ Rectangle2D.Double bounds = rectangle.getBounds();
+
+ assertThat(bounds.width).isEqualTo(0.1); // Minimum width enforced
+ assertThat(bounds.height).isEqualTo(20.0);
+ }
+
+ /**
+ * Test setBounds with zero-height rectangle enforces minimum height.
+ */
+ @Test
+ public void testSetBoundsZeroHeight() {
+ Point2D.Double anchor = new Point2D.Double(10, 10);
+ Point2D.Double lead = new Point2D.Double(30, 10); // Same y-coordinate
+
+ rectangle.setBounds(anchor, lead);
+ Rectangle2D.Double bounds = rectangle.getBounds();
+
+ assertThat(bounds.width).isEqualTo(20.0);
+ assertThat(bounds.height).isEqualTo(0.1); // Minimum height enforced
+ }
+
+ // CONTAINMENT TESTS
+
+ /**
+ * Test contains method for point inside rectangle.
+ */
+ @Test
+ public void testContainsPointInside() {
+ RectangleFigure rect = new RectangleFigure(10, 10, 20, 15);
+ Point2D.Double insidePoint = new Point2D.Double(20, 17);
+
+ assertThat(rect.contains(insidePoint)).isTrue();
+ }
+
+ /**
+ * Test contains method for point outside rectangle.
+ */
+ @Test
+ public void testContainsPointOutside() {
+ RectangleFigure rect = new RectangleFigure(10, 10, 20, 15);
+ Point2D.Double outsidePoint = new Point2D.Double(5, 5);
+
+ assertThat(rect.contains(outsidePoint)).isFalse();
+ }
+
+ /**
+ * Test contains method for point on rectangle boundary.
+ */
+ @Test
+ public void testContainsPointOnBoundary() {
+ RectangleFigure rect = new RectangleFigure(10, 10, 20, 15);
+ Point2D.Double boundaryPoint = new Point2D.Double(10, 10); // Top-left corner
+
+ // Due to hit growth, boundary points should be contained
+ assertThat(rect.contains(boundaryPoint)).isTrue();
+ }
+
+ // TRANSFORMATION TESTS
+
+ /**
+ * Test transform with identity transformation.
+ */
+ @Test
+ public void testTransformIdentity() {
+ RectangleFigure rect = new RectangleFigure(10, 10, 20, 15);
+ Rectangle2D.Double originalBounds = rect.getBounds();
+
+ AffineTransform identity = new AffineTransform();
+ rect.transform(identity);
+
+ Rectangle2D.Double newBounds = rect.getBounds();
+ assertThat(newBounds).isEqualTo(originalBounds);
+ }
+
+ /**
+ * Test transform with translation.
+ */
+ @Test
+ public void testTransformTranslation() {
+ RectangleFigure rect = new RectangleFigure(10, 10, 20, 15);
+
+ AffineTransform translation = AffineTransform.getTranslateInstance(5, -3);
+ rect.transform(translation);
+
+ Rectangle2D.Double bounds = rect.getBounds();
+ assertThat(bounds.x).isEqualTo(15.0);
+ assertThat(bounds.y).isEqualTo(7.0);
+ assertThat(bounds.width).isEqualTo(20.0);
+ assertThat(bounds.height).isEqualTo(15.0);
+ }
+
+ /**
+ * Test transform with scaling.
+ */
+ @Test
+ public void testTransformScaling() {
+ RectangleFigure rect = new RectangleFigure(10, 10, 20, 15);
+
+ AffineTransform scaling = AffineTransform.getScaleInstance(2.0, 0.5);
+ rect.transform(scaling);
+
+ Rectangle2D.Double bounds = rect.getBounds();
+ assertThat(bounds.x).isEqualTo(20.0);
+ assertThat(bounds.y).isEqualTo(5.0);
+ assertThat(bounds.width).isEqualTo(40.0);
+ assertThat(bounds.height).isEqualTo(7.5);
+ }
+
+ // DRAWING AREA TESTS
+
+ /**
+ * Test that drawing area is larger than bounds due to stroke growth.
+ */
+ @Test
+ public void testGetDrawingAreaLargerThanBounds() {
+ RectangleFigure rect = new RectangleFigure(10, 10, 20, 15);
+
+ Rectangle2D.Double bounds = rect.getBounds();
+ Rectangle2D.Double drawingArea = rect.getDrawingArea();
+
+ assertThat(drawingArea.width).isGreaterThan(bounds.width);
+ assertThat(drawingArea.height).isGreaterThan(bounds.height);
+ }
+
+ // DRAWING BEHAVIOR TESTS (with mocking)
+
+ /**
+ * Test that draw method calls appropriate Graphics2D methods.
+ * Note: This is a simplified test - in reality we'd need to set up proper attributes.
+ */
+ @Test
+ public void testDrawCallsGraphicsMethods() {
+ RectangleFigure rect = new RectangleFigure(10, 10, 20, 15);
+
+ // Create a real graphics context for testing
+ BufferedImage image = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
+ Graphics2D g2d = image.createGraphics();
+
+ // This should not throw any exceptions
+ assertThatCode(() -> rect.draw(g2d)).doesNotThrowAnyException();
+
+ g2d.dispose();
+ }
+
+ // EDGE CASES AND ERROR CONDITIONS
+
+ /**
+ * Test drawing with null graphics context should throw NPE.
+ */
+ @Test
+ public void testDrawWithNullGraphics() {
+ RectangleFigure rect = new RectangleFigure(10, 10, 20, 15);
+
+ // Should throw NullPointerException - this is the correct behavior
+ assertThatThrownBy(() -> rect.draw(null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ /**
+ * Test negative dimensions behavior.
+ */
+ @Test
+ public void testNegativeDimensions() {
+ RectangleFigure rect = new RectangleFigure(10, 10, -20, -15);
+ Rectangle2D.Double bounds = rect.getBounds();
+
+ // Should handle negative dimensions appropriately
+ assertThat(bounds.width).isEqualTo(-20.0); // Current behavior - might need adjustment
+ assertThat(bounds.height).isEqualTo(-15.0);
+ }
+
+ // JAVA ASSERTIONS FOR INVARIANTS
+
+ /**
+ * Test invariant: bounds should never be null.
+ */
+ @Test
+ public void testBoundsInvariant() {
+ assert rectangle.getBounds() != null : "Bounds should never be null";
+
+ rectangle.setBounds(new Point2D.Double(0, 0), new Point2D.Double(10, 10));
+ assert rectangle.getBounds() != null : "Bounds should never be null after setBounds";
+
+ rectangle.transform(AffineTransform.getScaleInstance(2, 2));
+ assert rectangle.getBounds() != null : "Bounds should never be null after transform";
+ }
+
+ /**
+ * Test invariant: drawing area should always contain bounds.
+ */
+ @Test
+ public void testDrawingAreaContainsBoundsInvariant() {
+ RectangleFigure rect = new RectangleFigure(10, 10, 20, 15);
+
+ Rectangle2D.Double bounds = rect.getBounds();
+ Rectangle2D.Double drawingArea = rect.getDrawingArea();
+
+ assert drawingArea.contains(bounds) : "Drawing area should always contain bounds";
+ }
+}
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/bdd/GivenFigureCreation.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/bdd/GivenFigureCreation.java
new file mode 100644
index 000000000..e75719eca
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/bdd/GivenFigureCreation.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2015 JHotDraw.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.jhotdraw.draw.figure.bdd;
+
+import com.tngtech.jgiven.Stage;
+import com.tngtech.jgiven.annotation.ScenarioState;
+import com.tngtech.jgiven.annotation.ScenarioState.Resolution;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import org.jhotdraw.draw.figure.RectangleFigure;
+import static org.assertj.core.api.Assertions.*;
+
+public class GivenFigureCreation extends Stage {
+
+ @ScenarioState
+ protected RectangleFigure rectangle;
+
+ @ScenarioState(resolution = Resolution.NAME)
+ protected Point2D.Double startPoint;
+
+ @ScenarioState(resolution = Resolution.NAME)
+ protected Point2D.Double endPoint;
+
+ public GivenFigureCreation a_new_rectangle_figure() {
+ rectangle = new RectangleFigure();
+ return self();
+ }
+
+ public GivenFigureCreation a_rectangle_figure_with_dimensions(double x, double y, double width, double height) {
+ rectangle = new RectangleFigure(x, y, width, height);
+ return self();
+ }
+
+ public GivenFigureCreation an_existing_rectangle_at_position(double x, double y) {
+ rectangle = new RectangleFigure(x, y, 50, 30); // Default size
+ return self();
+ }
+
+ public GivenFigureCreation a_start_point_at(double x, double y) {
+ startPoint = new Point2D.Double(x, y);
+ return self();
+ }
+
+ public GivenFigureCreation an_end_point_at(double x, double y) {
+ endPoint = new Point2D.Double(x, y);
+ return self();
+ }
+
+ public GivenFigureCreation a_rectangle_with_zero_dimensions() {
+ rectangle = new RectangleFigure(10, 10, 0, 0);
+ return self();
+ }
+}
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/bdd/RectangleFigureBDDTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/bdd/RectangleFigureBDDTest.java
new file mode 100644
index 000000000..f5d1f292a
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/bdd/RectangleFigureBDDTest.java
@@ -0,0 +1,124 @@
+package org.jhotdraw.draw.figure.bdd;
+
+import com.tngtech.jgiven.junit.ScenarioTest;
+import org.junit.Test;
+
+public class RectangleFigureBDDTest extends ScenarioTest {
+
+ @Test
+ public void user_can_create_rectangle_by_dragging() {
+ given().a_new_rectangle_figure()
+ .and().a_start_point_at(10, 10)
+ .and().an_end_point_at(50, 40);
+
+ when().the_user_creates_a_rectangle_by_dragging_from_start_to_end();
+
+ then().the_rectangle_should_have_position(10, 10)
+ .and().the_rectangle_should_have_dimensions(40, 30);
+ }
+
+ @Test
+ public void user_can_resize_existing_rectangle() {
+ given().a_rectangle_figure_with_dimensions(10, 10, 50, 30);
+
+ when().the_user_resizes_the_rectangle_to(100, 60);
+
+ then().the_rectangle_should_have_dimensions(100, 60)
+ .and().the_rectangle_should_be_positioned_at(10, 10);
+ }
+
+ @Test
+ public void user_can_move_rectangle_to_new_position() {
+ given().a_rectangle_figure_with_dimensions(10, 10, 50, 30);
+
+ when().the_user_moves_the_figure_by(20, 15);
+
+ then().the_rectangle_should_have_position(30, 25)
+ .and().the_rectangle_should_have_dimensions(50, 30);
+ }
+
+ @Test
+ public void user_can_scale_rectangle_proportionally() {
+ given().a_rectangle_figure_with_dimensions(10, 10, 40, 20);
+
+ when().the_user_scales_the_figure_by(2.0, 2.0);
+
+ then().the_rectangle_should_maintain_aspect_ratio_after_uniform_scaling(40, 20, 2.0);
+ }
+
+ @Test
+ public void rectangle_enforces_minimum_dimensions_when_dragging() {
+ given().a_new_rectangle_figure()
+ .and().a_start_point_at(10, 10)
+ .and().an_end_point_at(10, 10); // Same point - zero dimensions
+
+ when().the_user_creates_a_rectangle_by_dragging_from_start_to_end();
+
+ then().the_rectangle_should_enforce_minimum_width(0.1)
+ .and().the_rectangle_should_enforce_minimum_height(0.1);
+ }
+
+ @Test
+ public void rectangle_normalizes_bounds_when_dragging_backwards() {
+ given().a_new_rectangle_figure()
+ .and().a_start_point_at(50, 40) // Start from bottom-right
+ .and().an_end_point_at(10, 10); // End at top-left
+
+ when().the_user_creates_a_rectangle_by_dragging_from_start_to_end();
+
+ then().the_rectangle_should_have_position(10, 10) // Should normalize to top-left
+ .and().the_rectangle_should_have_dimensions(40, 30)
+ .and().the_rectangle_should_be_normalized();
+ }
+
+ @Test
+ public void point_inside_rectangle_is_detected_correctly() {
+ given().a_rectangle_figure_with_dimensions(10, 10, 40, 30);
+
+ when().checking_if_point_$_$_is_contained(25, 20); // Point inside
+
+ then().the_point_should_be_contained();
+ }
+
+ @Test
+ public void point_outside_rectangle_is_detected_correctly() {
+ given().a_rectangle_figure_with_dimensions(10, 10, 40, 30);
+
+ when().checking_if_point_$_$_is_contained(5, 5); // Point outside
+
+ then().the_point_should_not_be_contained();
+ }
+
+ @Test
+ public void rectangle_drawing_area_accommodates_stroke_width() {
+ given().a_rectangle_figure_with_dimensions(10, 10, 40, 30);
+
+ when().the_user_moves_the_figure_by(0, 0); // Trigger bounds calculation
+
+ then().the_drawing_area_should_be_larger_than_bounds();
+ }
+
+ @Test
+ public void user_can_create_thin_horizontal_line() {
+ given().a_new_rectangle_figure()
+ .and().a_start_point_at(10, 20)
+ .and().an_end_point_at(50, 20); // Same y-coordinate
+
+ when().the_user_creates_a_rectangle_by_dragging_from_start_to_end();
+
+ then().the_rectangle_should_have_position(10, 20)
+ .and().the_rectangle_should_have_dimensions(40, 0.1); // Minimum height enforced
+ }
+
+ @Test
+ public void user_can_create_thin_vertical_line() {
+ given().a_new_rectangle_figure()
+ .and().a_start_point_at(15, 10)
+ .and().an_end_point_at(15, 40); // Same x-coordinate
+
+ when().the_user_creates_a_rectangle_by_dragging_from_start_to_end();
+
+ then().the_rectangle_should_have_position(15, 10)
+ .and().the_rectangle_should_have_dimensions(0.1, 30); // Minimum width enforced
+ }
+}
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/bdd/ThenFigureBehavior.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/bdd/ThenFigureBehavior.java
new file mode 100644
index 000000000..f578300ef
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/bdd/ThenFigureBehavior.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2015 JHotDraw.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.jhotdraw.draw.figure.bdd;
+
+import com.tngtech.jgiven.Stage;
+import com.tngtech.jgiven.annotation.ScenarioState;
+import com.tngtech.jgiven.annotation.ScenarioState.Resolution;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import org.jhotdraw.draw.figure.RectangleFigure;
+import static org.assertj.core.api.Assertions.*;
+
+public class ThenFigureBehavior extends Stage {
+
+ @ScenarioState
+ protected RectangleFigure rectangle;
+
+ @ScenarioState
+ protected Rectangle2D.Double resultingBounds;
+
+ @ScenarioState
+ protected boolean containmentResult;
+
+ @ScenarioState(resolution = Resolution.NAME)
+ protected Point2D.Double testPoint;
+
+ public ThenFigureBehavior the_rectangle_should_have_position(double x, double y) {
+ assertThat(resultingBounds.x).isEqualTo(x);
+ assertThat(resultingBounds.y).isEqualTo(y);
+ return self();
+ }
+
+ public ThenFigureBehavior the_rectangle_should_have_dimensions(double width, double height) {
+ assertThat(resultingBounds.width).isEqualTo(width);
+ assertThat(resultingBounds.height).isEqualTo(height);
+ return self();
+ }
+
+ public ThenFigureBehavior the_rectangle_should_be_positioned_at(double x, double y) {
+ Rectangle2D.Double bounds = rectangle.getBounds();
+ assertThat(bounds.x).isEqualTo(x);
+ assertThat(bounds.y).isEqualTo(y);
+ return self();
+ }
+
+ public ThenFigureBehavior the_rectangle_should_have_size(double width, double height) {
+ Rectangle2D.Double bounds = rectangle.getBounds();
+ assertThat(bounds.width).isEqualTo(width);
+ assertThat(bounds.height).isEqualTo(height);
+ return self();
+ }
+
+ public ThenFigureBehavior the_point_should_be_contained() {
+ assertThat(containmentResult).as("Point %s should be contained in rectangle", testPoint).isTrue();
+ return self();
+ }
+
+ public ThenFigureBehavior the_point_should_not_be_contained() {
+ assertThat(containmentResult).as("Point %s should not be contained in rectangle", testPoint).isFalse();
+ return self();
+ }
+
+ public ThenFigureBehavior the_rectangle_should_enforce_minimum_width(double minimumWidth) {
+ assertThat(resultingBounds.width).isGreaterThanOrEqualTo(minimumWidth);
+ return self();
+ }
+
+ public ThenFigureBehavior the_rectangle_should_enforce_minimum_height(double minimumHeight) {
+ assertThat(resultingBounds.height).isGreaterThanOrEqualTo(minimumHeight);
+ return self();
+ }
+
+ public ThenFigureBehavior the_rectangle_should_be_normalized() {
+ // A normalized rectangle should have positive width and height
+ assertThat(resultingBounds.width).isGreaterThanOrEqualTo(0);
+ assertThat(resultingBounds.height).isGreaterThanOrEqualTo(0);
+ return self();
+ }
+
+ public ThenFigureBehavior the_drawing_area_should_be_larger_than_bounds() {
+ Rectangle2D.Double bounds = rectangle.getBounds();
+ Rectangle2D.Double drawingArea = rectangle.getDrawingArea();
+
+ assertThat(drawingArea.width).isGreaterThan(bounds.width);
+ assertThat(drawingArea.height).isGreaterThan(bounds.height);
+ return self();
+ }
+
+ public ThenFigureBehavior the_rectangle_should_maintain_aspect_ratio_after_uniform_scaling(double originalWidth, double originalHeight, double scaleFactor) {
+ double expectedWidth = originalWidth * scaleFactor;
+ double expectedHeight = originalHeight * scaleFactor;
+
+ assertThat(resultingBounds.width).isCloseTo(expectedWidth, within(0.01));
+ assertThat(resultingBounds.height).isCloseTo(expectedHeight, within(0.01));
+ return self();
+ }
+}
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/bdd/WhenFigureManipulation.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/bdd/WhenFigureManipulation.java
new file mode 100644
index 000000000..9160d95ae
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/figure/bdd/WhenFigureManipulation.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2015 JHotDraw.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.jhotdraw.draw.figure.bdd;
+
+import com.tngtech.jgiven.Stage;
+import com.tngtech.jgiven.annotation.ScenarioState;
+import com.tngtech.jgiven.annotation.ScenarioState.Resolution;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import org.jhotdraw.draw.figure.RectangleFigure;
+
+public class WhenFigureManipulation extends Stage {
+
+ @ScenarioState
+ protected RectangleFigure rectangle;
+
+ @ScenarioState(resolution = Resolution.NAME)
+ protected Point2D.Double startPoint;
+
+ @ScenarioState(resolution = Resolution.NAME)
+ protected Point2D.Double endPoint;
+
+ @ScenarioState
+ protected Rectangle2D.Double resultingBounds;
+
+ @ScenarioState
+ protected boolean containmentResult;
+
+ @ScenarioState(resolution = Resolution.NAME)
+ protected Point2D.Double testPoint;
+
+ public WhenFigureManipulation the_user_sets_bounds_from_start_to_end_point() {
+ rectangle.setBounds(startPoint, endPoint);
+ resultingBounds = rectangle.getBounds();
+ return self();
+ }
+
+ public WhenFigureManipulation the_user_moves_the_figure_by(double deltaX, double deltaY) {
+ AffineTransform translation = AffineTransform.getTranslateInstance(deltaX, deltaY);
+ rectangle.transform(translation);
+ resultingBounds = rectangle.getBounds();
+ return self();
+ }
+
+ public WhenFigureManipulation the_user_scales_the_figure_by(double scaleX, double scaleY) {
+ AffineTransform scaling = AffineTransform.getScaleInstance(scaleX, scaleY);
+ rectangle.transform(scaling);
+ resultingBounds = rectangle.getBounds();
+ return self();
+ }
+
+ public WhenFigureManipulation checking_if_point_$_$_is_contained(double x, double y) {
+ testPoint = new Point2D.Double(x, y);
+ containmentResult = rectangle.contains(testPoint);
+ return self();
+ }
+
+ public WhenFigureManipulation the_user_resizes_the_rectangle_to(double width, double height) {
+ Rectangle2D.Double currentBounds = rectangle.getBounds();
+ Point2D.Double anchor = new Point2D.Double(currentBounds.x, currentBounds.y);
+ Point2D.Double lead = new Point2D.Double(currentBounds.x + width, currentBounds.y + height);
+ rectangle.setBounds(anchor, lead);
+ resultingBounds = rectangle.getBounds();
+ return self();
+ }
+
+ public WhenFigureManipulation the_user_creates_a_rectangle_by_dragging_from_start_to_end() {
+ rectangle.setBounds(startPoint, endPoint);
+ resultingBounds = rectangle.getBounds();
+ return self();
+ }
+}
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/io/ImageFormatRegistryTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/io/ImageFormatRegistryTest.java
index 2108adce2..83da0cbf5 100644
--- a/jhotdraw-core/src/test/java/org/jhotdraw/draw/io/ImageFormatRegistryTest.java
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/io/ImageFormatRegistryTest.java
@@ -8,36 +8,41 @@
package org.jhotdraw.draw.io;
import org.jhotdraw.draw.figure.ImageFigure;
-import org.testng.annotations.Test;
+import org.junit.Test;
import java.util.List;
-import static org.testng.Assert.*;
+import static org.assertj.core.api.Assertions.*;
/**
- * Tests for the modular ImageFormatRegistry.
+ * Tests for the modular ImageFormatRegistry using JUnit 4 and AssertJ.
*/
public class ImageFormatRegistryTest {
@Test
public void testPngFormatSupported() {
- assertTrue(ImageFormatRegistry.isFormatSupported("png"),
- "PNG format should be supported");
- assertTrue(ImageFormatRegistry.isFormatSupported("PNG"),
- "PNG format should be supported (uppercase)");
+ assertThat(ImageFormatRegistry.isFormatSupported("png"))
+ .as("PNG format should be supported")
+ .isTrue();
+ assertThat(ImageFormatRegistry.isFormatSupported("PNG"))
+ .as("PNG format should be supported (uppercase)")
+ .isTrue();
}
@Test
public void testJpegFormatSupported() {
- assertTrue(ImageFormatRegistry.isFormatSupported("jpg"),
- "JPG format should be supported");
- assertTrue(ImageFormatRegistry.isFormatSupported("jpeg"),
- "JPEG format should be supported");
+ assertThat(ImageFormatRegistry.isFormatSupported("jpg"))
+ .as("JPG format should be supported")
+ .isTrue();
+ assertThat(ImageFormatRegistry.isFormatSupported("jpeg"))
+ .as("JPEG format should be supported")
+ .isTrue();
}
@Test
public void testUnsupportedFormat() {
- assertFalse(ImageFormatRegistry.isFormatSupported("xyz"),
- "XYZ format should not be supported");
+ assertThat(ImageFormatRegistry.isFormatSupported("xyz"))
+ .as("XYZ format should not be supported")
+ .isFalse();
}
@Test
@@ -45,42 +50,52 @@ public void testCreateInputFormats() {
ImageFigure prototype = new ImageFigure();
List formats = ImageFormatRegistry.createInputFormats(prototype);
- assertNotNull(formats, "Input formats should not be null");
- assertTrue(formats.size() >= 2,
- "Should have at least 2 input formats (PNG, JPEG)");
+ assertThat(formats)
+ .as("Input formats should not be null")
+ .isNotNull();
+ assertThat(formats)
+ .as("Should have at least 2 input formats (PNG, JPEG)")
+ .hasSizeGreaterThanOrEqualTo(2);
}
@Test
public void testCreateOutputFormats() {
List formats = ImageFormatRegistry.createOutputFormats();
- assertNotNull(formats, "Output formats should not be null");
- assertTrue(formats.size() >= 2,
- "Should have at least 2 output formats (PNG, JPEG)");
+ assertThat(formats)
+ .as("Output formats should not be null")
+ .isNotNull();
+ assertThat(formats)
+ .as("Should have at least 2 output formats (PNG, JPEG)")
+ .hasSizeGreaterThanOrEqualTo(2);
}
@Test
public void testProviderCount() {
List providers = ImageFormatRegistry.getProviders();
- assertNotNull(providers, "Providers should not be null");
- assertTrue(providers.size() >= 2, "Should have at least 2 providers");
+ assertThat(providers)
+ .as("Providers should not be null")
+ .isNotNull();
+ assertThat(providers)
+ .as("Should have at least 2 providers")
+ .hasSizeGreaterThanOrEqualTo(2);
}
@Test
public void testPngProvider() {
PngFormatProvider provider = new PngFormatProvider();
- assertEquals(provider.getFormatName(), "PNG");
- assertEquals(provider.getFileExtensions(), new String[]{"png"});
- assertEquals(provider.getMimeTypes(), new String[]{"image/png"});
+ assertThat(provider.getFormatName()).isEqualTo("PNG");
+ assertThat(provider.getFileExtensions()).isEqualTo(new String[]{"png"});
+ assertThat(provider.getMimeTypes()).isEqualTo(new String[]{"image/png"});
}
@Test
public void testJpegProvider() {
JpegFormatProvider provider = new JpegFormatProvider();
- assertEquals(provider.getFormatName(), "JPEG");
- assertEquals(provider.getFileExtensions(), new String[]{"jpg", "jpeg"});
+ assertThat(provider.getFormatName()).isEqualTo("JPEG");
+ assertThat(provider.getFileExtensions()).isEqualTo(new String[]{"jpg", "jpeg"});
}
}
diff --git a/pom.xml b/pom.xml
index 5f6c7eef5..b7ce44319 100644
--- a/pom.xml
+++ b/pom.xml
@@ -59,6 +59,18 @@
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+ --add-opens java.base/java.lang=ALL-UNNAMED
+ --add-opens java.base/java.lang.reflect=ALL-UNNAMED
+ --add-opens java.base/java.util=ALL-UNNAMED
+
+
+