diff --git a/src/test/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftmanTest.java b/src/test/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftmanTest.java
new file mode 100644
index 0000000..3e35e5e
--- /dev/null
+++ b/src/test/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftmanTest.java
@@ -0,0 +1,409 @@
+package ovh.gasser.newshapes.ui.visitors;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import ovh.gasser.newshapes.attributes.ColorAttributes;
+import ovh.gasser.newshapes.shapes.*;
+
+import java.awt.*;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class HTMLDraftmanTest {
+
+ private StringWriter htmlBuffer;
+ private StringWriter cssBuffer;
+ private PrintWriter htmlWriter;
+ private PrintWriter cssWriter;
+ private HTMLDraftman draftman;
+
+ @BeforeEach
+ void setUp() {
+ htmlBuffer = new StringWriter();
+ cssBuffer = new StringWriter();
+ htmlWriter = new PrintWriter(htmlBuffer);
+ cssWriter = new PrintWriter(cssBuffer);
+ draftman = new HTMLDraftman(htmlWriter, cssWriter);
+ }
+
+ private String html() {
+ htmlWriter.flush();
+ return htmlBuffer.toString();
+ }
+
+ private String css() {
+ cssWriter.flush();
+ return cssBuffer.toString();
+ }
+
+ // ── visitRectangle ──────────────────────────────────────────────
+
+ @Nested
+ class VisitRectangleTests {
+
+ @Test
+ void testRectangleProducesDivWithId() {
+ SRectangle rect = SRectangle.create(10, 20, 100, 50);
+ draftman.visitRectangle(rect);
+
+ assertTrue(html().contains("
Hello
"), "HTML should contain text content in div");
+ }
+
+ @Test
+ void testTextHtmlIdMatchesCssSelector() {
+ SText text = SText.create(0, 0, "Test");
+ draftman.visitText(text);
+
+ String id = "txt" + text.hashCode();
+ assertTrue(html().contains("id=\"" + id + "\""), "HTML should contain text div with correct id");
+ assertTrue(css().contains("#" + id), "CSS should reference same text id");
+ }
+
+ @Test
+ void testTextCssContainsPosition() {
+ SText text = SText.create(15, 25, "Pos");
+ draftman.visitText(text);
+
+ String cssOut = css();
+ assertTrue(cssOut.contains("top:25px"), "CSS should contain correct top");
+ assertTrue(cssOut.contains("left:15px"), "CSS should contain correct left");
+ }
+
+ @Test
+ void testTextCssContainsDefaultFontAttributes() {
+ SText text = SText.create(0, 0, "Font");
+ draftman.visitText(text);
+
+ String cssOut = css();
+ assertTrue(cssOut.contains("font-family:"), "CSS should contain font-family");
+ assertTrue(cssOut.contains("font-size:16px"), "CSS should contain default font size");
+ assertTrue(cssOut.contains("font-style:normal"), "CSS should contain normal font style");
+ assertTrue(cssOut.contains("font-weight:normal"), "CSS should contain normal font weight");
+ }
+
+ @Test
+ void testTextCssContainsColor() {
+ SText text = SText.create(0, 0, "Colored");
+ text.addAttributes(new ColorAttributes(true, false, Color.RED, Color.BLACK));
+ draftman.visitText(text);
+
+ assertTrue(css().contains("color:#ff0000"), "CSS should contain fill color as text color");
+ }
+
+ @Test
+ void testTextCssWhiteSpaceNowrap() {
+ SText text = SText.create(0, 0, "NoWrap");
+ draftman.visitText(text);
+
+ assertTrue(css().contains("white-space: nowrap"), "CSS should prevent text wrapping");
+ }
+
+ @Test
+ void testTextWithNullColorAttributesFallsBackToBlack() {
+ SText text = SText.create(0, 0, "Fallback");
+ text.addAttributes(new ColorAttributes(false, false, null, null));
+ draftman.visitText(text);
+
+ assertTrue(css().contains("color:#000000"), "Should fall back to black with null colors");
+ }
+ }
+
+ // ── visitCollection ─────────────────────────────────────────────
+
+ @Nested
+ class VisitCollectionTests {
+
+ @Test
+ void testEmptyCollection() {
+ SCollection empty = SCollection.of();
+ draftman.visitCollection(empty);
+
+ assertEquals("", html(), "Empty collection should produce no HTML");
+ assertEquals("", css(), "Empty collection should produce no CSS");
+ }
+
+ @Test
+ void testCollectionVisitsAllChildren() {
+ SCollection coll = SCollection.of(
+ SRectangle.create(0, 0, 10, 10),
+ SCircle.create(20, 20, 5)
+ );
+ draftman.visitCollection(coll);
+
+ assertTrue(html().contains("rec"), "Should visit rectangle");
+ assertTrue(html().contains("circle"), "Should visit circle");
+ }
+
+ @Test
+ void testNestedCollectionVisitsAllDescendants() {
+ SCollection inner = SCollection.of(
+ SRectangle.create(0, 0, 5, 5)
+ );
+ SCollection outer = SCollection.of(
+ inner,
+ SCircle.create(10, 10, 3)
+ );
+ draftman.visitCollection(outer);
+
+ assertTrue(html().contains("rec"), "Should visit nested rectangle");
+ assertTrue(html().contains("circle"), "Should visit circle at outer level");
+ }
+ }
+
+ // ── generateHTML ────────────────────────────────────────────────
+
+ @Nested
+ class GenerateHTMLTests {
+
+ @Test
+ void testGenerateHTMLIncludesDoctype() {
+ SCollection model = SCollection.of();
+ draftman.generateHTML(model);
+
+ assertTrue(html().contains(""), "Should include DOCTYPE");
+ }
+
+ @Test
+ void testGenerateHTMLIncludesHtmlStructure() {
+ SCollection model = SCollection.of();
+ draftman.generateHTML(model);
+
+ String htmlOut = html();
+ assertTrue(htmlOut.contains(""), "Should include head tag");
+ assertTrue(htmlOut.contains(""), "Should include body tag");
+ assertTrue(htmlOut.contains(""), "Should close body tag");
+ }
+
+ @Test
+ void testGenerateHTMLReferencesStylesheet() {
+ SCollection model = SCollection.of();
+ draftman.generateHTML(model);
+
+ assertTrue(html().contains("style.css"), "Should reference CSS stylesheet");
+ }
+
+ @Test
+ void testGenerateHTMLIncludesShapeContent() {
+ SCollection model = SCollection.of(
+ SRectangle.create(1, 2, 3, 4)
+ );
+ draftman.generateHTML(model);
+
+ assertTrue(html().contains("