From ba37b199ded1c1d10ae468d2c5aed174ea9a4c2c Mon Sep 17 00:00:00 2001 From: Thibaud Date: Fri, 27 Mar 2026 15:06:46 +0100 Subject: [PATCH] fix(html): fix hashCode for triangle The bug caused HTML class and CSS selector to have different IDs, breaking triangle rendering. --- .../newshapes/ui/visitors/HTMLDraftman.java | 4 +- .../ovh/gasser/newshapes/SelectionTest.java | 144 ++++++++++++++ .../newshapes/shapes/SCollectionTest.java | 176 ++++++++++++++++++ .../gasser/newshapes/util/StreamableTest.java | 48 +++++ 4 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 src/test/java/ovh/gasser/newshapes/SelectionTest.java create mode 100644 src/test/java/ovh/gasser/newshapes/shapes/SCollectionTest.java create mode 100644 src/test/java/ovh/gasser/newshapes/util/StreamableTest.java diff --git a/src/main/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftman.java b/src/main/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftman.java index 0a1419d..ea376e9 100644 --- a/src/main/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftman.java +++ b/src/main/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftman.java @@ -66,11 +66,11 @@ public class HTMLDraftman implements ShapeVisitor { @Override public void visitTriangle(STriangle sTriangle) { - htmlOutput.printf("
\n", this.hashCode()); + htmlOutput.printf("
\n", sTriangle.hashCode()); var bounds = sTriangle.getBounds(); ColorAttributes colAttrs = (ColorAttributes) sTriangle.getAttributes(ColorAttributes.ID); String colorString = formatCSSColor(colAttrs.filledColor); - StringJoiner joiner = new StringJoiner("\n", ".triangle%d{\n".formatted(this.hashCode()), "\n}\n"); + StringJoiner joiner = new StringJoiner("\n", ".triangle%d{\n".formatted(sTriangle.hashCode()), "\n}\n"); joiner.add(" position: absolute;"); joiner.add(" top: %dpx;".formatted(bounds.y)); joiner.add(" left: %dpx;".formatted(bounds.x)); diff --git a/src/test/java/ovh/gasser/newshapes/SelectionTest.java b/src/test/java/ovh/gasser/newshapes/SelectionTest.java new file mode 100644 index 0000000..6e49d6d --- /dev/null +++ b/src/test/java/ovh/gasser/newshapes/SelectionTest.java @@ -0,0 +1,144 @@ +package ovh.gasser.newshapes; + +import org.junit.jupiter.api.Test; +import ovh.gasser.newshapes.attributes.SelectionAttributes; +import ovh.gasser.newshapes.shapes.SCircle; +import ovh.gasser.newshapes.shapes.SRectangle; +import ovh.gasser.newshapes.shapes.STriangle; +import ovh.gasser.newshapes.ui.listeners.SelectionListener; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +class SelectionTest { + + @Test + void testIsEmptyInitially() { + Selection selection = new Selection(); + assertTrue(selection.isEmpty()); + } + + @Test + void testAdd() { + Selection selection = new Selection(); + SCircle circle = SCircle.create(0, 0, 50); + + selection.add(circle); + + assertFalse(selection.isEmpty()); + assertEquals(1, selection.getSelectedShapes().size()); + assertTrue(selection.getSelectedShapes().contains(circle)); + + // Verify shape is marked as selected + SelectionAttributes attrs = (SelectionAttributes) circle.getAttributes(SelectionAttributes.ID); + assertNotNull(attrs); + assertTrue(attrs.selected); + } + + @Test + void testAddAll() { + Selection selection = new Selection(); + SCircle circle1 = SCircle.create(0, 0, 50); + SRectangle rect = SRectangle.create(10, 10, 100, 50); + STriangle triangle = STriangle.create(50, 50, 30, java.awt.Color.BLACK, java.awt.Color.BLACK); + + List shapes = Arrays.asList(circle1, rect, triangle); + selection.addAll(shapes); + + assertEquals(3, selection.getSelectedShapes().size()); + assertTrue(selection.getSelectedShapes().contains(circle1)); + assertTrue(selection.getSelectedShapes().contains(rect)); + assertTrue(selection.getSelectedShapes().contains(triangle)); + } + + @Test + void testClear() { + Selection selection = new Selection(); + SCircle circle = SCircle.create(0, 0, 50); + SRectangle rect = SRectangle.create(10, 10, 100, 50); + + selection.add(circle); + selection.add(rect); + + assertFalse(selection.isEmpty()); + assertEquals(2, selection.getSelectedShapes().size()); + + selection.clear(); + + assertTrue(selection.isEmpty()); + assertEquals(0, selection.getSelectedShapes().size()); + + // Verify shapes are marked as unselected + SelectionAttributes circleAttrs = (SelectionAttributes) circle.getAttributes(SelectionAttributes.ID); + SelectionAttributes rectAttrs = (SelectionAttributes) rect.getAttributes(SelectionAttributes.ID); + + assertNotNull(circleAttrs); + assertNotNull(rectAttrs); + assertFalse(circleAttrs.selected); + assertFalse(rectAttrs.selected); + } + + @Test + void testGetSelectedShapesReturnsCopy() { + Selection selection = new Selection(); + SCircle circle = SCircle.create(0, 0, 50); + selection.add(circle); + + List copy = selection.getSelectedShapes(); + + // Try to modify the copy + assertThrows(UnsupportedOperationException.class, () -> copy.add(SCircle.create(10, 10, 20))); + + // Original should be unchanged + assertEquals(1, selection.getSelectedShapes().size()); + } + + @Test + void testAddListener() { + Selection selection = new Selection(); + AtomicBoolean listenerNotified = new AtomicBoolean(false); + AtomicInteger notificationCount = new AtomicInteger(0); + + SelectionListener listener = (selectedShapes) -> { + listenerNotified.set(true); + notificationCount.incrementAndGet(); + }; + + selection.addListener(listener); + + // Adding a shape should notify listener + SCircle circle = SCircle.create(0, 0, 50); + selection.add(circle); + + assertTrue(listenerNotified.get()); + assertEquals(1, notificationCount.get()); + + // Adding another shape should notify listener again + SRectangle rect = SRectangle.create(10, 10, 100, 50); + selection.add(rect); + + assertEquals(2, notificationCount.get()); + + // Clearing should notify listener + selection.clear(); + + assertEquals(3, notificationCount.get()); + } + + @Test + void testNullAddAllDoesNothing() { + Selection selection = new Selection(); + + // Should not throw + assertDoesNotThrow(() -> selection.addAll(null)); + + // Selection should still be empty + assertTrue(selection.isEmpty()); + } +} diff --git a/src/test/java/ovh/gasser/newshapes/shapes/SCollectionTest.java b/src/test/java/ovh/gasser/newshapes/shapes/SCollectionTest.java new file mode 100644 index 0000000..fa69168 --- /dev/null +++ b/src/test/java/ovh/gasser/newshapes/shapes/SCollectionTest.java @@ -0,0 +1,176 @@ +package ovh.gasser.newshapes.shapes; + +import org.junit.jupiter.api.Test; +import ovh.gasser.newshapes.App; +import ovh.gasser.newshapes.attributes.ColorAttributes; +import ovh.gasser.newshapes.attributes.SelectionAttributes; + +import java.awt.Color; +import java.awt.Rectangle; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +class SCollectionTest { + + @Test + void testCreateWithShapes() { + SRectangle rect = SRectangle.create(10, 20, 100, 50); + SCircle circle = SCircle.create(50, 50, 25); + + SCollection collection = SCollection.of(rect, circle); + + assertNotNull(collection); + assertEquals(2, collection.stream().count()); + } + + @Test + void testAdd() { + SCollection collection = SCollection.of(); + SRectangle rect = SRectangle.create(10, 20, 100, 50); + + collection.add(rect); + + assertEquals(1, collection.stream().count()); + assertSame(rect, collection.iterator().next()); + } + + @Test + void testRemove() { + SRectangle rect = SRectangle.create(10, 20, 100, 50); + SCollection collection = SCollection.of(rect); + + collection.remove(rect); + + assertEquals(0, collection.stream().count()); + } + + @Test + void testIterator() { + SRectangle rect1 = SRectangle.create(10, 20, 100, 50); + SRectangle rect2 = SRectangle.create(30, 40, 60, 70); + SCollection collection = SCollection.of(rect1, rect2); + + Iterator iterator = collection.iterator(); + + assertTrue(iterator.hasNext()); + assertSame(rect1, iterator.next()); + assertTrue(iterator.hasNext()); + assertSame(rect2, iterator.next()); + assertFalse(iterator.hasNext()); + } + + @Test + void testStream() { + SRectangle rect1 = SRectangle.create(10, 20, 100, 50); + SRectangle rect2 = SRectangle.create(30, 40, 60, 70); + SCircle circle = SCircle.create(50, 50, 25); + SCollection collection = SCollection.of(rect1, rect2, circle); + + List shapes = collection.stream().collect(Collectors.toList()); + + assertEquals(3, shapes.size()); + assertTrue(shapes.contains(rect1)); + assertTrue(shapes.contains(rect2)); + assertTrue(shapes.contains(circle)); + } + + @Test + void testGetBoundsEmptyCollection() { + SCollection collection = SCollection.of(); + + Rectangle bounds = collection.getBounds(); + + assertEquals(App.WIN_SIZE.width, bounds.width); + assertEquals(App.WIN_SIZE.height, bounds.height); + } + + @Test + void testGetBoundsWithChildren() { + SRectangle rect1 = SRectangle.create(10, 10, 50, 50); + SRectangle rect2 = SRectangle.create(100, 100, 80, 40); + SCollection collection = SCollection.of(rect1, rect2); + + Rectangle bounds = collection.getBounds(); + + // Union should cover from (10,10) to (180,140) + assertEquals(10, bounds.x); + assertEquals(10, bounds.y); + assertEquals(170, bounds.width); // 100+80-10 = 170 + assertEquals(130, bounds.height); // 100+40-10 = 130 + } + + @Test + void testTranslate() { + SRectangle rect = SRectangle.create(10, 20, 100, 50); + SCollection collection = SCollection.of(rect); + + collection.translate(5, 10); + + Rectangle bounds = rect.getBounds(); + assertEquals(15, bounds.x); + assertEquals(30, bounds.y); + } + + @Test + void testClone() { + SRectangle rect = SRectangle.create(10, 20, 100, 50); + SCollection original = SCollection.of(rect); + + SCollection cloned = (SCollection) original.clone(); + + assertNotSame(original, cloned); + assertEquals(original.stream().count(), cloned.stream().count()); + + // Verify SelectionAttributes is added to clone + assertNotNull(cloned.getAttributes(SelectionAttributes.ID)); + + // Verify deep copy - modifying clone doesn't affect original + Iterator clonedIterator = cloned.iterator(); + Shape clonedChild = clonedIterator.next(); + clonedChild.translate(100, 100); + + Rectangle originalBounds = rect.getBounds(); + assertEquals(10, originalBounds.x); + assertEquals(20, originalBounds.y); + } + + @Test + void testToString() { + SCollection collection = SCollection.of(); + + String str = collection.toString(); + + assertTrue(str.contains("SCollection")); + } + + @Test + void testAddAttributesPropagatesToChildren() { + SRectangle rect = SRectangle.create(10, 20, 100, 50); + SCollection collection = SCollection.of(rect); + + ColorAttributes attrs = new ColorAttributes(true, true, Color.RED, Color.BLUE); + collection.addAttributes(attrs); + + ColorAttributes childAttrs = (ColorAttributes) rect.getAttributes(ColorAttributes.ID); + + assertNotNull(childAttrs); + assertEquals(Color.RED, childAttrs.filledColor); + assertEquals(Color.BLUE, childAttrs.strokedColor); + } + + @Test + void testGetAttributesReturnsChildColor() { + SRectangle rect = SRectangle.create(10, 20, 100, 50, Color.GREEN); + SCircle circle = SCircle.create(50, 50, 25); + SCollection collection = SCollection.of(rect, circle); + + ColorAttributes attrs = (ColorAttributes) collection.getAttributes(ColorAttributes.ID); + + assertNotNull(attrs); + // First child's color should be returned (strokedColor for SRectangle) + assertEquals(Color.GREEN, attrs.strokedColor); + } +} diff --git a/src/test/java/ovh/gasser/newshapes/util/StreamableTest.java b/src/test/java/ovh/gasser/newshapes/util/StreamableTest.java new file mode 100644 index 0000000..1b01a05 --- /dev/null +++ b/src/test/java/ovh/gasser/newshapes/util/StreamableTest.java @@ -0,0 +1,48 @@ +package ovh.gasser.newshapes.util; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class StreamableTest { + + private static class TestStreamable implements Streamable { + private final List elements; + + TestStreamable(List elements) { + this.elements = new ArrayList<>(elements); + } + + @Override + public java.util.Iterator iterator() { + return elements.iterator(); + } + } + + @Test + void testStreamReturnsStreamOfElements() { + List testData = Arrays.asList("a", "b", "c"); + Streamable streamable = new TestStreamable(testData); + + Stream result = streamable.stream(); + + assertNotNull(result); + assertEquals(testData, result.toList()); + } + + @Test + void testStreamEmptyCollection() { + List emptyData = new ArrayList<>(); + Streamable streamable = new TestStreamable(emptyData); + + Stream result = streamable.stream(); + + assertNotNull(result); + assertTrue(result.toList().isEmpty()); + } +} -- 2.49.1