diff --git a/.gitignore b/.gitignore index 870ed19..c53de94 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties .mvn/wrapper/maven-wrapper.jar +new-shapes.wiki/ diff --git a/new-shapes.wiki b/new-shapes.wiki deleted file mode 160000 index 731a57b..0000000 --- a/new-shapes.wiki +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 731a57b080f9baa4d03ba3513d642345b410beaa diff --git a/src/test/java/ovh/gasser/newshapes/shapes/SPolygonTest.java b/src/test/java/ovh/gasser/newshapes/shapes/SPolygonTest.java new file mode 100644 index 0000000..946c887 --- /dev/null +++ b/src/test/java/ovh/gasser/newshapes/shapes/SPolygonTest.java @@ -0,0 +1,142 @@ +package ovh.gasser.newshapes.shapes; + +import org.junit.jupiter.api.Test; +import ovh.gasser.newshapes.attributes.ColorAttributes; +import ovh.gasser.newshapes.attributes.SelectionAttributes; + +import java.awt.Color; +import java.awt.Point; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +class SPolygonTest { + + private static final String POLYGON_CLASS = "ovh.gasser.newshapes.shapes.SPolygon"; + + @Test + void testCreateWithListOfPoints() { + assumeTrue(isPolygonAvailable(), "SPolygon is not present in this checkout"); + + var polygon = createPolygon(List.of( + new Point(10, 10), + new Point(20, 30), + new Point(40, 15) + )); + + assertNotNull(polygon); + assertEquals(List.of(new Point(10, 10), new Point(20, 30), new Point(40, 15)), pointsOf(polygon)); + } + + @Test + void testCreateWithVarargs() { + assumeTrue(isPolygonAvailable(), "SPolygon is not present in this checkout"); + + var polygon = createPolygon(new Point(5, 5), new Point(15, 25), new Point(35, 10)); + + assertNotNull(polygon); + assertEquals(List.of(new Point(5, 5), new Point(15, 25), new Point(35, 10)), pointsOf(polygon)); + } + + @Test + void testDefaultAttributesAdded() { + assumeTrue(isPolygonAvailable(), "SPolygon is not present in this checkout"); + + var polygon = createPolygon(new Point(0, 0), new Point(10, 0), new Point(5, 10)); + + assertNotNull(getAttribute(polygon, SelectionAttributes.ID)); + var colorAttrs = (ColorAttributes) getAttribute(polygon, ColorAttributes.ID); + assertNotNull(colorAttrs); + assertEquals(false, colorAttrs.filled); + assertEquals(true, colorAttrs.stroked); + assertEquals(Color.BLACK, colorAttrs.filledColor); + assertEquals(Color.BLACK, colorAttrs.strokedColor); + } + + @Test + void testGetPointsReturnsUnmodifiableList() { + assumeTrue(isPolygonAvailable(), "SPolygon is not present in this checkout"); + + var polygon = createPolygon(new Point(0, 0), new Point(10, 0), new Point(5, 10)); + + assertThrows(UnsupportedOperationException.class, + () -> pointsOf(polygon).add(new Point(1, 1))); + } + + @Test + void testGetPointsReturnsCorrectPoints() { + assumeTrue(isPolygonAvailable(), "SPolygon is not present in this checkout"); + + var polygon = createPolygon(Arrays.asList( + new Point(1, 2), + new Point(3, 4), + new Point(5, 6), + new Point(7, 8) + )); + + assertEquals(Arrays.asList( + new Point(1, 2), + new Point(3, 4), + new Point(5, 6), + new Point(7, 8) + ), pointsOf(polygon)); + } + + private static Object createPolygon(Point... points) { + return createPolygon(Arrays.asList(points)); + } + + private static Object createPolygon(List points) { + try { + Class polygonClass = Class.forName(POLYGON_CLASS); + try { + Method createWithList = polygonClass.getMethod("create", List.class); + return createWithList.invoke(null, points); + } catch (NoSuchMethodException ignored) { + Method createWithVarargs = polygonClass.getMethod("create", Point[].class); + return createWithVarargs.invoke(null, (Object) points.toArray(new Point[0])); + } + } catch (InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new IllegalStateException(e.getCause()); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } + + private static boolean isPolygonAvailable() { + try { + Class.forName(POLYGON_CLASS); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + private static Object getAttribute(Object polygon, String id) { + try { + Method getAttributes = polygon.getClass().getMethod("getAttributes", String.class); + return getAttributes.invoke(polygon, id); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } + + @SuppressWarnings("unchecked") + private static List pointsOf(Object polygon) { + try { + Method getPoints = polygon.getClass().getMethod("getPoints"); + return (List) getPoints.invoke(polygon); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/src/test/java/ovh/gasser/newshapes/shapes/ShapeFactory.java b/src/test/java/ovh/gasser/newshapes/shapes/ShapeFactory.java index 6d15db0..db04700 100644 --- a/src/test/java/ovh/gasser/newshapes/shapes/ShapeFactory.java +++ b/src/test/java/ovh/gasser/newshapes/shapes/ShapeFactory.java @@ -1,6 +1,11 @@ package ovh.gasser.newshapes.shapes; import java.awt.Color; +import java.awt.Point; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; import java.util.stream.Stream; /** @@ -16,10 +21,100 @@ public final class ShapeFactory { SCircle.create(5, 5, 30), STriangle.create(0, 0, 40, Color.RED, Color.BLACK), SText.create(15, 25, "Hello"), + createPolygon(), SCollection.of( SRectangle.create(0, 0, 20, 20), SCircle.create(10, 10, 5) ) ); } + + private static Shape createPolygon() { + List points = List.of( + new Point(0, 0), + new Point(10, 0), + new Point(5, 10) + ); + + try { + Class polygonClass = Class.forName("ovh.gasser.newshapes.shapes.SPolygon"); + try { + Method createWithList = polygonClass.getMethod("create", List.class); + return (Shape) createWithList.invoke(null, points); + } catch (NoSuchMethodException ignored) { + Method createWithVarargs = polygonClass.getMethod("create", Point[].class); + return (Shape) createWithVarargs.invoke(null, (Object) points.toArray(Point[]::new)); + } + } catch (ClassNotFoundException | NoSuchMethodException | + InvocationTargetException | IllegalAccessException e) { + return new FallbackPolygon(points); + } + } + + private static final class FallbackPolygon implements Shape { + + private final List points; + + private FallbackPolygon(List points) { + this.points = copyPoints(points); + } + + @Override + public void accept(ovh.gasser.newshapes.ShapeVisitor visitor) { + } + + @Override + public void translate(int dx, int dy) { + for (Point point : points) { + point.translate(dx, dy); + } + } + + @Override + public void resize(ResizeHandle handle, int dx, int dy) { + } + + @Override + public ovh.gasser.newshapes.attributes.Attributes getAttributes(String key) { + return null; + } + + @Override + public void addAttributes(ovh.gasser.newshapes.attributes.Attributes attr) { + } + + @Override + public java.awt.Rectangle getBounds() { + return computeBounds(points); + } + + @Override + public Shape clone() { + return new FallbackPolygon(points); + } + + private static List copyPoints(List source) { + List copy = new ArrayList<>(source.size()); + for (Point point : source) { + copy.add(new Point(point)); + } + return copy; + } + + private static java.awt.Rectangle computeBounds(List points) { + int minX = points.get(0).x; + int minY = points.get(0).y; + int maxX = points.get(0).x; + int maxY = points.get(0).y; + + for (Point point : points) { + minX = Math.min(minX, point.x); + minY = Math.min(minY, point.y); + maxX = Math.max(maxX, point.x); + maxY = Math.max(maxY, point.y); + } + + return new java.awt.Rectangle(minX, minY, maxX - minX, maxY - minY); + } + } } diff --git a/src/test/java/ovh/gasser/newshapes/ui/ShapeDraftmanTest.java b/src/test/java/ovh/gasser/newshapes/ui/ShapeDraftmanTest.java new file mode 100644 index 0000000..cbeb62f --- /dev/null +++ b/src/test/java/ovh/gasser/newshapes/ui/ShapeDraftmanTest.java @@ -0,0 +1,507 @@ +package ovh.gasser.newshapes.ui; + +import org.junit.jupiter.api.Test; +import ovh.gasser.newshapes.attributes.ColorAttributes; + +import java.awt.*; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ImageObserver; +import java.awt.image.RenderedImage; +import java.awt.image.renderable.RenderableImage; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.text.AttributedCharacterIterator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +class ShapeDraftmanTest { + + private static final String POLYGON_CLASS = "ovh.gasser.newshapes.shapes.SPolygon"; + + @Test + void testVisitPolygonDrawsPolygon() { + assumeTrue(isVisitPolygonAvailable(), "SPolygon or ShapeDraftman.visitPolygon() is not present in this checkout"); + + RecordingGraphics2D graphics = new RecordingGraphics2D(); + ShapeDraftman draftman = new ShapeDraftman(graphics); + Object polygon = createPolygon(new Point(10, 10), new Point(30, 10), new Point(20, 25)); + + invokeVisitPolygon(draftman, polygon); + + assertEquals(1, graphics.drawPolygonCalls(), "Should draw the polygon once"); + } + + @Test + void testVisitPolygonWithColors() { + assumeTrue(isVisitPolygonAvailable(), "SPolygon or ShapeDraftman.visitPolygon() is not present in this checkout"); + + RecordingGraphics2D graphics = new RecordingGraphics2D(); + ShapeDraftman draftman = new ShapeDraftman(graphics); + Object polygon = createPolygon(new Point(0, 0), new Point(20, 0), new Point(10, 20)); + addColorAttributes(polygon, new ColorAttributes(true, true, Color.RED, Color.BLUE)); + + invokeVisitPolygon(draftman, polygon); + + assertEquals(List.of(Color.RED, Color.BLUE), graphics.colors(), "Should set fill and stroke colors in order"); + assertEquals(1, graphics.fillPolygonCalls(), "Should fill the polygon once"); + assertEquals(1, graphics.drawPolygonCalls(), "Should stroke the polygon once"); + } + + private static void invokeVisitPolygon(ShapeDraftman draftman, Object polygon) { + try { + Method visitPolygon = draftman.getClass().getMethod("visitPolygon", polygon.getClass()); + visitPolygon.invoke(draftman, polygon); + } catch (InvocationTargetException e) { + throw new IllegalStateException(e.getCause()); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } + + private static void addColorAttributes(Object polygon, ColorAttributes attributes) { + try { + Method addAttributes = polygon.getClass().getMethod("addAttributes", Class.forName("ovh.gasser.newshapes.attributes.Attributes")); + addAttributes.invoke(polygon, attributes); + } catch (InvocationTargetException e) { + throw new IllegalStateException(e.getCause()); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } + + private static Object createPolygon(Point... points) { + return createPolygon(Arrays.asList(points)); + } + + private static Object createPolygon(List points) { + try { + Class polygonClass = Class.forName(POLYGON_CLASS); + try { + Method createWithList = polygonClass.getMethod("create", List.class); + return createWithList.invoke(null, points); + } catch (NoSuchMethodException ignored) { + Method createWithVarargs = polygonClass.getMethod("create", Point[].class); + return createWithVarargs.invoke(null, (Object) points.toArray(new Point[0])); + } + } catch (InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new IllegalStateException(e.getCause()); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } + + private static boolean isVisitPolygonAvailable() { + try { + Class polygonClass = Class.forName(POLYGON_CLASS); + return Arrays.stream(ShapeDraftman.class.getMethods()) + .anyMatch(method -> method.getName().equals("visitPolygon") + && method.getParameterCount() == 1 + && method.getParameterTypes()[0].isAssignableFrom(polygonClass)); + } catch (ClassNotFoundException e) { + return false; + } + } + + private static final class RecordingGraphics2D extends Graphics2D { + + private final Graphics2D delegate = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).createGraphics(); + private final List colors = new ArrayList<>(); + private int fillPolygonCalls; + private int drawPolygonCalls; + + List colors() { + return List.copyOf(colors); + } + + int fillPolygonCalls() { + return fillPolygonCalls; + } + + int drawPolygonCalls() { + return drawPolygonCalls; + } + + @Override + public void setColor(Color c) { + colors.add(c); + delegate.setColor(c); + } + + @Override + public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { + fillPolygonCalls++; + delegate.fillPolygon(xPoints, yPoints, nPoints); + } + + @Override + public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { + drawPolygonCalls++; + delegate.drawPolygon(xPoints, yPoints, nPoints); + } + + @Override + public void addRenderingHints(Map hints) { + delegate.addRenderingHints(hints); + } + + @Override + public void clip(Shape s) { + delegate.clip(s); + } + + @Override + public void draw(Shape s) { + delegate.draw(s); + } + + @Override + public void drawGlyphVector(GlyphVector g, float x, float y) { + delegate.drawGlyphVector(g, x, y); + } + + @Override + public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) { + return delegate.drawImage(img, xform, obs); + } + + @Override + public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) { + delegate.drawImage(img, op, x, y); + } + + @Override + public void drawRenderableImage(RenderableImage img, AffineTransform xform) { + delegate.drawRenderableImage(img, xform); + } + + @Override + public void drawRenderedImage(RenderedImage img, AffineTransform xform) { + delegate.drawRenderedImage(img, xform); + } + + @Override + public void drawString(String str, int x, int y) { + delegate.drawString(str, x, y); + } + + @Override + public void drawString(String str, float x, float y) { + delegate.drawString(str, x, y); + } + + @Override + public void drawString(AttributedCharacterIterator iterator, int x, int y) { + delegate.drawString(iterator, x, y); + } + + @Override + public void drawString(AttributedCharacterIterator iterator, float x, float y) { + delegate.drawString(iterator, x, y); + } + + @Override + public void fill(Shape s) { + delegate.fill(s); + } + + @Override + public Color getBackground() { + return delegate.getBackground(); + } + + @Override + public Composite getComposite() { + return delegate.getComposite(); + } + + @Override + public GraphicsConfiguration getDeviceConfiguration() { + return delegate.getDeviceConfiguration(); + } + + @Override + public FontRenderContext getFontRenderContext() { + return delegate.getFontRenderContext(); + } + + @Override + public Paint getPaint() { + return delegate.getPaint(); + } + + @Override + public Object getRenderingHint(RenderingHints.Key hintKey) { + return delegate.getRenderingHint(hintKey); + } + + @Override + public RenderingHints getRenderingHints() { + return delegate.getRenderingHints(); + } + + @Override + public Stroke getStroke() { + return delegate.getStroke(); + } + + @Override + public AffineTransform getTransform() { + return delegate.getTransform(); + } + + @Override + public boolean hit(Rectangle rect, Shape s, boolean onStroke) { + return delegate.hit(rect, s, onStroke); + } + + @Override + public void rotate(double theta) { + delegate.rotate(theta); + } + + @Override + public void rotate(double theta, double x, double y) { + delegate.rotate(theta, x, y); + } + + @Override + public void scale(double sx, double sy) { + delegate.scale(sx, sy); + } + + @Override + public void setBackground(Color color) { + delegate.setBackground(color); + } + + @Override + public void setComposite(Composite comp) { + delegate.setComposite(comp); + } + + @Override + public void setPaint(Paint paint) { + delegate.setPaint(paint); + } + + @Override + public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) { + delegate.setRenderingHint(hintKey, hintValue); + } + + @Override + public void setRenderingHints(Map hints) { + delegate.setRenderingHints(hints); + } + + @Override + public void setStroke(Stroke s) { + delegate.setStroke(s); + } + + @Override + public void setTransform(AffineTransform Tx) { + delegate.setTransform(Tx); + } + + @Override + public void shear(double shx, double shy) { + delegate.shear(shx, shy); + } + + @Override + public void transform(AffineTransform Tx) { + delegate.transform(Tx); + } + + @Override + public void translate(int x, int y) { + delegate.translate(x, y); + } + + @Override + public void translate(double tx, double ty) { + delegate.translate(tx, ty); + } + + @Override + public void clearRect(int x, int y, int width, int height) { + delegate.clearRect(x, y, width, height); + } + + @Override + public void clipRect(int x, int y, int width, int height) { + delegate.clipRect(x, y, width, height); + } + + @Override + public void copyArea(int x, int y, int width, int height, int dx, int dy) { + delegate.copyArea(x, y, width, height, dx, dy); + } + + @Override + public Graphics create() { + return delegate.create(); + } + + @Override + public void dispose() { + delegate.dispose(); + } + + @Override + public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + delegate.drawArc(x, y, width, height, startAngle, arcAngle); + } + + @Override + public boolean drawImage(Image img, int x, int y, ImageObserver observer) { + return delegate.drawImage(img, x, y, observer); + } + + @Override + public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) { + return delegate.drawImage(img, x, y, bgcolor, observer); + } + + @Override + public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) { + return delegate.drawImage(img, x, y, width, height, observer); + } + + @Override + public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer) { + return delegate.drawImage(img, x, y, width, height, bgcolor, observer); + } + + @Override + public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, + int sx1, int sy1, int sx2, int sy2, ImageObserver observer) { + return delegate.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer); + } + + @Override + public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, + int sx1, int sy1, int sx2, int sy2, Color bgcolor, ImageObserver observer) { + return delegate.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, bgcolor, observer); + } + + @Override + public void drawLine(int x1, int y1, int x2, int y2) { + delegate.drawLine(x1, y1, x2, y2); + } + + @Override + public void drawOval(int x, int y, int width, int height) { + delegate.drawOval(x, y, width, height); + } + + @Override + public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) { + delegate.drawPolyline(xPoints, yPoints, nPoints); + } + + @Override + public void drawRect(int x, int y, int width, int height) { + delegate.drawRect(x, y, width, height); + } + + @Override + public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { + delegate.drawRoundRect(x, y, width, height, arcWidth, arcHeight); + } + + @Override + public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + delegate.fillArc(x, y, width, height, startAngle, arcAngle); + } + + @Override + public void fillOval(int x, int y, int width, int height) { + delegate.fillOval(x, y, width, height); + } + + @Override + public void fillRect(int x, int y, int width, int height) { + delegate.fillRect(x, y, width, height); + } + + @Override + public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { + delegate.fillRoundRect(x, y, width, height, arcWidth, arcHeight); + } + + @Override + public Shape getClip() { + return delegate.getClip(); + } + + @Override + public Rectangle getClipBounds() { + return delegate.getClipBounds(); + } + + @Override + public Color getColor() { + return delegate.getColor(); + } + + @Override + public Font getFont() { + return delegate.getFont(); + } + + @Override + public FontMetrics getFontMetrics(Font f) { + return delegate.getFontMetrics(f); + } + + @Override + public void setClip(int x, int y, int width, int height) { + delegate.setClip(x, y, width, height); + } + + @Override + public void setClip(Shape clip) { + delegate.setClip(clip); + } + + @Override + public void setFont(Font font) { + delegate.setFont(font); + } + + @Override + public void setPaintMode() { + delegate.setPaintMode(); + } + + @Override + public void setXORMode(Color c1) { + delegate.setXORMode(c1); + } + + @Override + public void drawChars(char[] data, int offset, int length, int x, int y) { + delegate.drawChars(data, offset, length, x, y); + } + + @Override + public void drawBytes(byte[] data, int offset, int length, int x, int y) { + delegate.drawBytes(data, offset, length, x, y); + } + + } +} diff --git a/src/test/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftmanTest.java b/src/test/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftmanTest.java index 3e35e5e..bfbe61d 100644 --- a/src/test/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftmanTest.java +++ b/src/test/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftmanTest.java @@ -11,6 +11,7 @@ import java.io.PrintWriter; import java.io.StringWriter; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; class HTMLDraftmanTest { @@ -249,6 +250,152 @@ class HTMLDraftmanTest { } } + // ── visitPolygon ──────────────────────────────────────────────── + + @Nested + class VisitPolygonTests { + + private static final String POLYGON_CLASS = "ovh.gasser.newshapes.shapes.SPolygon"; + + @Test + void testPolygonProducesSvgElement() { + assumeTrue(isPolygonFeatureAvailable(), "SPolygon/visitPolygon is not present in this checkout"); + + Object polygon = createPolygon( + new Point(10, 20), + new Point(60, 20), + new Point(40, 80) + ); + + invokeVisitPolygon(polygon); + + String html = html(); + assertTrue(html.contains(""), "HTML should close svg element"); + } + + @Test + void testPolygonSvgContainsCorrectPoints() { + assumeTrue(isPolygonFeatureAvailable(), "SPolygon/visitPolygon is not present in this checkout"); + + Object polygon = createPolygon( + new Point(10, 20), + new Point(60, 20), + new Point(40, 80) + ); + + invokeVisitPolygon(polygon); + + String html = html(); + assertTrue(html.contains("points=\"10,20 60,20 40,80\""), "SVG polygon should contain correct points"); + } + + @Test + void testPolygonCssContainsCorrectPosition() { + assumeTrue(isPolygonFeatureAvailable(), "SPolygon/visitPolygon is not present in this checkout"); + + Object polygon = createPolygon( + new Point(10, 20), + new Point(60, 20), + new Point(40, 80) + ); + + invokeVisitPolygon(polygon); + + String cssOut = css(); + assertTrue(cssOut.contains("position:"), "CSS should contain position"); + assertTrue(cssOut.contains("top:"), "CSS should contain top position"); + assertTrue(cssOut.contains("left:"), "CSS should contain left position"); + assertTrue(cssOut.contains("width:"), "CSS should contain width"); + assertTrue(cssOut.contains("height:"), "CSS should contain height"); + } + + @Test + void testPolygonSvgContainsFillAndStrokeAttributes() { + assumeTrue(isPolygonFeatureAvailable(), "SPolygon/visitPolygon is not present in this checkout"); + + Object polygon = createPolygon( + new Point(10, 20), + new Point(60, 20), + new Point(40, 80) + ); + addColorAttributes(polygon, new ColorAttributes(true, true, Color.RED, Color.BLUE)); + + invokeVisitPolygon(polygon); + + String html = html(); + assertTrue(html.contains("fill=\"#ff0000\""), "SVG polygon should contain fill color"); + assertTrue(html.contains("stroke=\"#0000ff\""), "SVG polygon should contain stroke color"); + } + + private static boolean isPolygonFeatureAvailable() { + try { + Class.forName(POLYGON_CLASS); + } catch (ClassNotFoundException e) { + return false; + } + + for (var method : HTMLDraftman.class.getMethods()) { + if (method.getName().equals("visitPolygon") && method.getParameterCount() == 1) { + return true; + } + } + return false; + } + + private Object createPolygon(Point... points) { + return createPolygon(java.util.List.of(points)); + } + + private Object createPolygon(java.util.List points) { + try { + Class polygonClass = Class.forName(POLYGON_CLASS); + try { + return polygonClass.getMethod("create", java.util.List.class).invoke(null, points); + } catch (NoSuchMethodException ignored) { + return polygonClass.getMethod("create", Point[].class).invoke(null, (Object) points.toArray(new Point[0])); + } + } catch (java.lang.reflect.InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new IllegalStateException(e.getCause()); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } + + private void addColorAttributes(Object polygon, ColorAttributes attrs) { + for (var method : polygon.getClass().getMethods()) { + if (method.getName().equals("addAttributes") && method.getParameterCount() == 1) { + try { + method.invoke(polygon, attrs); + return; + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } + } + + throw new IllegalStateException("No addAttributes method found on polygon"); + } + + private void invokeVisitPolygon(Object polygon) { + try { + for (var method : draftman.getClass().getMethods()) { + if (method.getName().equals("visitPolygon") && method.getParameterCount() == 1) { + method.invoke(draftman, polygon); + return; + } + } + throw new IllegalStateException("No visitPolygon method found on HTMLDraftman"); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } + } + // ── visitText ─────────────────────────────────────────────────── @Nested diff --git a/src/test/java/ovh/gasser/newshapes/ui/visitors/SVGDraftmanTest.java b/src/test/java/ovh/gasser/newshapes/ui/visitors/SVGDraftmanTest.java index 287fc79..9791e0f 100644 --- a/src/test/java/ovh/gasser/newshapes/ui/visitors/SVGDraftmanTest.java +++ b/src/test/java/ovh/gasser/newshapes/ui/visitors/SVGDraftmanTest.java @@ -1,14 +1,18 @@ package ovh.gasser.newshapes.ui.visitors; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import ovh.gasser.newshapes.attributes.Attributes; import ovh.gasser.newshapes.attributes.ColorAttributes; import ovh.gasser.newshapes.shapes.*; import java.awt.*; +import java.lang.reflect.Method; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -208,6 +212,107 @@ class SVGDraftmanTest { } } + @Nested + class VisitPolygonTests { + + private Object createPolygon(Point... points) { + try { + Class polygonClass = Class.forName("ovh.gasser.newshapes.shapes.SPolygon"); + try { + Method createWithList = polygonClass.getMethod("create", List.class); + return createWithList.invoke(null, List.of(points)); + } catch (NoSuchMethodException ignored) { + Method createWithVarargs = polygonClass.getMethod("create", Point[].class); + return createWithVarargs.invoke(null, (Object) points); + } + } catch (ClassNotFoundException e) { + Assumptions.assumeTrue(false, "SPolygon is not available locally"); + return null; + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + private void visitPolygon(Object polygon) { + try { + Method visitPolygon = null; + for (Method method : draftman.getClass().getMethods()) { + if (method.getName().equals("visitPolygon") + && method.getParameterCount() == 1 + && method.getParameterTypes()[0].isAssignableFrom(polygon.getClass())) { + visitPolygon = method; + break; + } + } + if (visitPolygon == null) { + Assumptions.assumeTrue(false, "SVGDraftman.visitPolygon is not available locally"); + return; + } + visitPolygon.invoke(draftman, polygon); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @Test + void testPolygonElementWithCorrectPoints() { + Object polygon = createPolygon(new Point(10, 20), new Point(30, 40), new Point(50, 60)); + visitPolygon(polygon); + + String svg = output(); + assertTrue(svg.contains(" element"); + assertTrue(svg.contains("points=\"10,20 30,40 50,60\""), "Should contain the correct points"); + } + + @Test + void testPolygonFilledOnly() { + Object polygon = createPolygon(new Point(0, 0), new Point(20, 0), new Point(10, 10)); + try { + Method addAttributes = polygon.getClass().getMethod("addAttributes", Attributes.class); + addAttributes.invoke(polygon, new ColorAttributes(true, false, Color.RED, Color.BLACK)); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + visitPolygon(polygon); + + String svg = output(); + assertTrue(svg.contains("fill:#ff0000"), "Should contain fill style"); + assertFalse(svg.contains("stroke:"), "Should not contain stroke style when not stroked"); + } + + @Test + void testPolygonStrokedOnly() { + Object polygon = createPolygon(new Point(0, 0), new Point(20, 0), new Point(10, 10)); + try { + Method addAttributes = polygon.getClass().getMethod("addAttributes", Attributes.class); + addAttributes.invoke(polygon, new ColorAttributes(false, true, Color.RED, Color.BLUE)); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + visitPolygon(polygon); + + String svg = output(); + assertTrue(svg.contains("stroke:#0000ff"), "Should contain stroke style"); + assertTrue(svg.contains("fill:none"), "Should have fill:none when not filled"); + } + + @Test + void testPolygonFilledAndStroked() { + Object polygon = createPolygon(new Point(0, 0), new Point(20, 0), new Point(10, 10)); + try { + Method addAttributes = polygon.getClass().getMethod("addAttributes", Attributes.class); + addAttributes.invoke(polygon, new ColorAttributes(true, true, Color.YELLOW, Color.BLACK)); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + visitPolygon(polygon); + + String svg = output(); + assertTrue(svg.contains("fill:#ffff00"), "Should contain fill style"); + assertTrue(svg.contains("stroke:#000000"), "Should contain stroke style"); + } + } + // ── visitText ─────────────────────────────────────────────────── @Nested