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());
+ }
+}