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 + + +