1 Commits

Author SHA1 Message Date
d744588f25 test: add HTMLDraftman direct visitor tests
Add 32 tests covering each visit*() method in isolation:
- visitRectangle: div+id, position, dimensions, filled/stroked/both/neither
- visitCircle: div+class, border-radius prefixes, position, id consistency
- visitTriangle: div+class, CSS border trick, position, fill color,
  regression test for hashCode mismatch (this.hashCode vs shape.hashCode)
- visitText: content, id consistency, position, font attrs, color, nowrap,
  null color fallback
- visitCollection: empty, multiple children, nested
- generateHTML: DOCTYPE, structure, stylesheet ref, shape content

Closes #10
2026-03-27 16:29:34 +01:00
19 changed files with 6 additions and 2153 deletions

View File

@@ -1,30 +0,0 @@
name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 16
uses: actions/setup-java@v4
with:
java-version: '16'
distribution: 'temurin'
- name: Build and test with coverage
run: ./mvnw verify --batch-mode
- name: Upload coverage report
if: always()
uses: https://gitea.com/actions/gitea-upload-artifact@v4
with:
name: jacoco-report
path: target/site/jacoco/

View File

@@ -1,3 +0,0 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip

View File

@@ -1,6 +1,6 @@
# TODO # TODO
- [X] Box selection (drag to select multiple shapes) - [ ] Box selection (drag to select multiple shapes)
- [ ] Undo/redo stack - [ ] Undo/redo stack
- [ ] Copy/paste functionality - [ ] Copy/paste functionality
- [ ] Group/Ungroup shapes - [ ] Group/Ungroup shapes

295
mvnw vendored
View File

@@ -1,295 +0,0 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.4
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
scriptDir="$(dirname "$0")"
scriptName="$(basename "$0")"
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
# Find the actual extracted directory name (handles snapshots where filename != directory name)
actualDistributionDir=""
# First try the expected directory name (for regular distributions)
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
actualDistributionDir="$distributionUrlNameMain"
fi
fi
# If not found, search for any directory with the Maven executable (for snapshots)
if [ -z "$actualDistributionDir" ]; then
# enable globbing to iterate over items
set +f
for dir in "$TMP_DOWNLOAD_DIR"/*; do
if [ -d "$dir" ]; then
if [ -f "$dir/bin/$MVN_CMD" ]; then
actualDistributionDir="$(basename "$dir")"
break
fi
fi
done
set -f
fi
if [ -z "$actualDistributionDir" ]; then
verbose "Contents of $TMP_DOWNLOAD_DIR:"
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
die "Could not find Maven distribution directory in extracted archive"
fi
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"

46
pom.xml
View File

@@ -15,7 +15,6 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration> <configuration>
<source>16</source> <source>16</source>
<target>16</target> <target>16</target>
@@ -26,40 +25,6 @@
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version> <version>3.2.5</version>
</plugin> </plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.13</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
<goal>report</goal>
<goal>check</goal>
</goals>
</execution>
</executions>
<configuration>
<excludes>
<!-- UI classes require a display and cannot be unit-tested -->
<exclude>ovh/gasser/newshapes/App.class</exclude>
<exclude>ovh/gasser/newshapes/ui/**</exclude>
<exclude>ovh/gasser/newshapes/Selection.class</exclude>
</excludes>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.50</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</plugin>
</plugins> </plugins>
</build> </build>
@@ -71,13 +36,13 @@
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>
<version>2.0.16</version> <version>RELEASE</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>ch.qos.logback</groupId> <groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId> <artifactId>logback-classic</artifactId>
<version>1.5.12</version> <version>RELEASE</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -93,12 +58,5 @@
<version>5.10.0</version> <version>5.10.0</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -57,7 +57,7 @@ public class SText extends AbstractShape {
} }
public void updateMeasuredBounds(int width, int height) { public void updateMeasuredBounds(int width, int height) {
bounds.setSize(Math.max(width, 0), Math.max(height, 0)); getBounds().setSize(Math.max(width, 0), Math.max(height, 0));
} }
@Override @Override

View File

@@ -66,11 +66,11 @@ public class HTMLDraftman implements ShapeVisitor {
@Override @Override
public void visitTriangle(STriangle sTriangle) { public void visitTriangle(STriangle sTriangle) {
htmlOutput.printf("<div class=\"triangle%d\"></div>\n", sTriangle.hashCode()); htmlOutput.printf("<div class=\"triangle%d\"></div>\n", this.hashCode());
var bounds = sTriangle.getBounds(); var bounds = sTriangle.getBounds();
ColorAttributes colAttrs = (ColorAttributes) sTriangle.getAttributes(ColorAttributes.ID); ColorAttributes colAttrs = (ColorAttributes) sTriangle.getAttributes(ColorAttributes.ID);
String colorString = formatCSSColor(colAttrs.filledColor); String colorString = formatCSSColor(colAttrs.filledColor);
StringJoiner joiner = new StringJoiner("\n", ".triangle%d{\n".formatted(sTriangle.hashCode()), "\n}\n"); StringJoiner joiner = new StringJoiner("\n", ".triangle%d{\n".formatted(this.hashCode()), "\n}\n");
joiner.add(" position: absolute;"); joiner.add(" position: absolute;");
joiner.add(" top: %dpx;".formatted(bounds.y)); joiner.add(" top: %dpx;".formatted(bounds.y));
joiner.add(" left: %dpx;".formatted(bounds.x)); joiner.add(" left: %dpx;".formatted(bounds.x));

View File

@@ -1,144 +0,0 @@
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<ovh.gasser.newshapes.shapes.Shape> 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<ovh.gasser.newshapes.shapes.Shape> 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());
}
}

View File

@@ -1,129 +0,0 @@
package ovh.gasser.newshapes.attributes;
import org.junit.jupiter.api.Test;
import java.awt.*;
import static org.junit.jupiter.api.Assertions.*;
class ColorAttributesTest {
@Test
void testConstructorStoresFilledFlag() {
ColorAttributes attrs = new ColorAttributes(true, false, Color.RED, Color.BLACK);
assertTrue(attrs.filled, "filled flag should be true when constructed with true");
}
@Test
void testConstructorStoresStrokedFlag() {
ColorAttributes attrs = new ColorAttributes(false, true, Color.RED, Color.BLACK);
assertTrue(attrs.stroked, "stroked flag should be true when constructed with true");
}
@Test
void testConstructorStoresFilledColor() {
ColorAttributes attrs = new ColorAttributes(true, false, Color.BLUE, Color.BLACK);
assertEquals(Color.BLUE, attrs.filledColor, "filledColor should match the constructor argument");
}
@Test
void testConstructorStoresStrokedColor() {
ColorAttributes attrs = new ColorAttributes(false, true, Color.RED, Color.GREEN);
assertEquals(Color.GREEN, attrs.strokedColor, "strokedColor should match the constructor argument");
}
@Test
void testFilledAndStrokedBothTrue() {
ColorAttributes attrs = new ColorAttributes(true, true, Color.RED, Color.BLUE);
assertTrue(attrs.filled);
assertTrue(attrs.stroked);
}
@Test
void testFilledAndStrokedBothFalse() {
ColorAttributes attrs = new ColorAttributes(false, false, Color.RED, Color.BLUE);
assertFalse(attrs.filled);
assertFalse(attrs.stroked);
}
@Test
void testNullFilledColor() {
ColorAttributes attrs = new ColorAttributes(true, false, null, Color.BLACK);
assertNull(attrs.filledColor, "filledColor should accept null");
}
@Test
void testNullStrokedColor() {
ColorAttributes attrs = new ColorAttributes(false, true, Color.RED, null);
assertNull(attrs.strokedColor, "strokedColor should accept null");
}
@Test
void testBothColorsNull() {
ColorAttributes attrs = new ColorAttributes(false, false, null, null);
assertNull(attrs.filledColor);
assertNull(attrs.strokedColor);
}
@Test
void testGetIDReturnsCorrectValue() {
ColorAttributes attrs = new ColorAttributes(false, false, Color.RED, Color.BLACK);
assertEquals(ColorAttributes.ID, attrs.getID());
}
@Test
void testIDConstant() {
assertEquals("COLOR_ATTRS", ColorAttributes.ID);
}
@Test
void testImplementsAttributes() {
ColorAttributes attrs = new ColorAttributes(false, false, Color.RED, Color.BLACK);
assertInstanceOf(Attributes.class, attrs);
}
@Test
void testToStringContainsAllFields() {
ColorAttributes attrs = new ColorAttributes(true, false, Color.RED, Color.BLUE);
String str = attrs.toString();
assertTrue(str.contains("filled=true"), "toString should contain filled value");
assertTrue(str.contains("stroked=false"), "toString should contain stroked value");
assertTrue(str.contains("filledColor"), "toString should contain filledColor");
assertTrue(str.contains("strokedColor"), "toString should contain strokedColor");
}
@Test
void testToStringWithNullColors() {
ColorAttributes attrs = new ColorAttributes(false, false, null, null);
String str = attrs.toString();
assertNotNull(str, "toString should not throw with null colors");
assertTrue(str.contains("filledColor=null"), "toString should show null filledColor");
assertTrue(str.contains("strokedColor=null"), "toString should show null strokedColor");
}
@Test
void testTwoInstancesAreIndependent() {
ColorAttributes attrs1 = new ColorAttributes(true, false, Color.RED, Color.BLACK);
ColorAttributes attrs2 = new ColorAttributes(false, true, Color.BLUE, Color.GREEN);
assertTrue(attrs1.filled);
assertFalse(attrs2.filled);
assertFalse(attrs1.stroked);
assertTrue(attrs2.stroked);
assertEquals(Color.RED, attrs1.filledColor);
assertEquals(Color.BLUE, attrs2.filledColor);
}
@Test
void testFieldsAreImmutable() {
Color fillColor = Color.RED;
Color strokeColor = Color.BLACK;
ColorAttributes attrs = new ColorAttributes(true, true, fillColor, strokeColor);
// Since fields are final, verify they retain their values
assertEquals(Color.RED, attrs.filledColor);
assertEquals(Color.BLACK, attrs.strokedColor);
assertTrue(attrs.filled);
assertTrue(attrs.stroked);
}
}

View File

@@ -1,72 +0,0 @@
package ovh.gasser.newshapes.attributes;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class SelectionAttributesTest {
@Test
void testDefaultConstructorIsUnselected() {
SelectionAttributes attrs = new SelectionAttributes();
assertFalse(attrs.selected, "Default constructor should create unselected state");
}
@Test
void testConstructorWithTrue() {
SelectionAttributes attrs = new SelectionAttributes(true);
assertTrue(attrs.selected, "Constructor with true should create selected state");
}
@Test
void testConstructorWithFalse() {
SelectionAttributes attrs = new SelectionAttributes(false);
assertFalse(attrs.selected, "Constructor with false should create unselected state");
}
@Test
void testGetIDReturnsCorrectValue() {
SelectionAttributes attrs = new SelectionAttributes();
assertEquals(SelectionAttributes.ID, attrs.getID());
}
@Test
void testIDConstant() {
assertEquals("SELECTION_ATTRS", SelectionAttributes.ID);
}
@Test
void testImplementsAttributes() {
SelectionAttributes attrs = new SelectionAttributes();
assertInstanceOf(Attributes.class, attrs);
}
@Test
void testSelectedFieldCanBeToggled() {
SelectionAttributes attrs = new SelectionAttributes(false);
assertFalse(attrs.selected);
attrs.selected = true;
assertTrue(attrs.selected);
attrs.selected = false;
assertFalse(attrs.selected);
}
@Test
void testTwoInstancesAreIndependent() {
SelectionAttributes attrs1 = new SelectionAttributes(true);
SelectionAttributes attrs2 = new SelectionAttributes(false);
assertNotEquals(attrs1.selected, attrs2.selected);
attrs2.selected = true;
assertTrue(attrs1.selected);
assertTrue(attrs2.selected);
// Mutating one doesn't affect the other
attrs1.selected = false;
assertFalse(attrs1.selected);
assertTrue(attrs2.selected);
}
}

View File

@@ -1,387 +0,0 @@
package ovh.gasser.newshapes.shapes;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.awt.Color;
import java.awt.Point;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests bounds-based hit-testing for all shape types.
* Hit-testing is performed via {@code shape.getBounds().contains(Point)},
* delegating to {@link java.awt.Rectangle#contains(Point)}.
*
* <p>Note on {@link java.awt.Rectangle#contains} boundary semantics:
* A rectangle {@code (x, y, w, h)} contains a point {@code (px, py)} when:
* {@code x <= px < x+w} and {@code y <= py < y+h}.
* The top-left corner is <em>inclusive</em>; the bottom-right corner is <em>exclusive</em>.
*/
class ContainsPointTest {
// -----------------------------------------------------------------------
// SRectangle — bounds = (x, y, width, height)
// -----------------------------------------------------------------------
@Nested
class SRectangleContainsPoint {
// Shape: origin (10, 20), size 80×60 → right-exclusive: x=90, y=80
private final SRectangle rect = SRectangle.create(10, 20, 80, 60);
@Test
void pointInsideShouldBeContained() {
Point inside = new Point(50, 50);
assertTrue(rect.getBounds().contains(inside),
"A point clearly inside the rectangle should be contained");
}
@Test
void pointOutsideShouldNotBeContained() {
Point outside = new Point(5, 5);
assertFalse(rect.getBounds().contains(outside),
"A point outside the rectangle should not be contained");
}
@Test
void pointFarOutsideShouldNotBeContained() {
Point farOutside = new Point(200, 300);
assertFalse(rect.getBounds().contains(farOutside),
"A point far outside the rectangle should not be contained");
}
@Test
void topLeftCornerShouldBeContained() {
// Rectangle.contains is inclusive of the top-left corner
Point topLeft = new Point(10, 20);
assertTrue(rect.getBounds().contains(topLeft),
"The top-left corner (inclusive boundary) should be contained");
}
@Test
void bottomRightCornerShouldNotBeContained() {
// Rectangle.contains is exclusive of the bottom-right corner
Point bottomRight = new Point(90, 80); // x + width, y + height
assertFalse(rect.getBounds().contains(bottomRight),
"The bottom-right corner (exclusive boundary) should not be contained");
}
@Test
void pointJustInsideBottomRightShouldBeContained() {
Point justInside = new Point(89, 79); // one pixel before exclusive boundary
assertTrue(rect.getBounds().contains(justInside),
"A point one pixel before the bottom-right boundary should be contained");
}
@Test
void pointAfterTranslateShouldBeAtNewPosition() {
SRectangle moved = SRectangle.create(10, 20, 80, 60);
moved.translate(30, 40); // new bounds: (40, 60, 80, 60)
Point oldCenter = new Point(50, 50);
assertFalse(moved.getBounds().contains(oldCenter),
"After translate, old center should no longer be contained");
Point newCenter = new Point(80, 90);
assertTrue(moved.getBounds().contains(newCenter),
"After translate, a point at the new center should be contained");
}
@Test
void pointOnTopEdgeShouldBeContained() {
Point topEdge = new Point(50, 20); // y == rect.y, inside x range
assertTrue(rect.getBounds().contains(topEdge),
"A point on the top edge should be contained");
}
@Test
void pointOnLeftEdgeShouldBeContained() {
Point leftEdge = new Point(10, 50); // x == rect.x, inside y range
assertTrue(rect.getBounds().contains(leftEdge),
"A point on the left edge should be contained");
}
@Test
void pointOnRightEdgeShouldNotBeContained() {
Point rightEdge = new Point(90, 50); // x == rect.x + rect.width (exclusive)
assertFalse(rect.getBounds().contains(rightEdge),
"A point exactly on the right edge (exclusive) should not be contained");
}
@Test
void pointOnBottomEdgeShouldNotBeContained() {
Point bottomEdge = new Point(50, 80); // y == rect.y + rect.height (exclusive)
assertFalse(rect.getBounds().contains(bottomEdge),
"A point exactly on the bottom edge (exclusive) should not be contained");
}
}
// -----------------------------------------------------------------------
// SCircle — bounds = (x, y, radius*2, radius*2)
// -----------------------------------------------------------------------
@Nested
class SCircleContainsPoint {
// Shape: origin (20, 30), radius=40 → bounds (20, 30, 80, 80)
// right-exclusive corner: (100, 110)
private final SCircle circle = SCircle.create(20, 30, 40);
@Test
void pointInsideBoundsShouldBeContained() {
Point inside = new Point(60, 70);
assertTrue(circle.getBounds().contains(inside),
"A point inside the bounding box should be contained");
}
@Test
void pointOutsideBoundsShouldNotBeContained() {
Point outside = new Point(10, 10);
assertFalse(circle.getBounds().contains(outside),
"A point outside the bounding box should not be contained");
}
@Test
void topLeftCornerShouldBeContained() {
Point topLeft = new Point(20, 30);
assertTrue(circle.getBounds().contains(topLeft),
"The top-left corner of the bounding box (inclusive) should be contained");
}
@Test
void bottomRightCornerShouldNotBeContained() {
Point bottomRight = new Point(100, 110); // x + 2r, y + 2r
assertFalse(circle.getBounds().contains(bottomRight),
"The bottom-right corner of the bounding box (exclusive) should not be contained");
}
@Test
void pointAfterTranslateShouldBeAtNewPosition() {
SCircle moved = SCircle.create(20, 30, 40);
moved.translate(50, 50); // new bounds: (70, 80, 80, 80)
Point oldCenter = new Point(60, 70);
assertFalse(moved.getBounds().contains(oldCenter),
"After translate, old center should no longer be contained");
Point newCenter = new Point(110, 120);
assertTrue(moved.getBounds().contains(newCenter),
"After translate, a point at the new center should be contained");
}
}
// -----------------------------------------------------------------------
// STriangle — bounds = (x, y, size, size)
// -----------------------------------------------------------------------
@Nested
class STriangleContainsPoint {
// Shape: origin (5, 15), size=60 → bounds (5, 15, 60, 60)
// right-exclusive corner: (65, 75)
private final STriangle triangle = STriangle.create(5, 15, 60, Color.RED, Color.BLACK);
@Test
void pointInsideBoundsShouldBeContained() {
Point inside = new Point(35, 45);
assertTrue(triangle.getBounds().contains(inside),
"A point inside the bounding box should be contained");
}
@Test
void pointOutsideBoundsShouldNotBeContained() {
Point outside = new Point(0, 0);
assertFalse(triangle.getBounds().contains(outside),
"A point outside the bounding box should not be contained");
}
@Test
void topLeftCornerShouldBeContained() {
Point topLeft = new Point(5, 15);
assertTrue(triangle.getBounds().contains(topLeft),
"The top-left corner of the bounding box (inclusive) should be contained");
}
@Test
void bottomRightCornerShouldNotBeContained() {
Point bottomRight = new Point(65, 75); // x + size, y + size
assertFalse(triangle.getBounds().contains(bottomRight),
"The bottom-right corner of the bounding box (exclusive) should not be contained");
}
@Test
void pointAfterTranslateShouldBeAtNewPosition() {
STriangle moved = STriangle.create(5, 15, 60, Color.RED, Color.BLACK);
moved.translate(100, 100); // new bounds: (105, 115, 60, 60)
Point oldCenter = new Point(35, 45);
assertFalse(moved.getBounds().contains(oldCenter),
"After translate, old center should no longer be contained");
Point newCenter = new Point(135, 145);
assertTrue(moved.getBounds().contains(newCenter),
"After translate, a point at the new center should be contained");
}
}
// -----------------------------------------------------------------------
// SText — bounds = (x, y, 100, 20)
// -----------------------------------------------------------------------
@Nested
class STextContainsPoint {
// Shape: origin (50, 100) → bounds (50, 100, 100, 20)
// right-exclusive corner: (150, 120)
private final SText text = SText.create(50, 100, "Hello");
@Test
void pointInsideBoundsShouldBeContained() {
Point inside = new Point(100, 110);
assertTrue(text.getBounds().contains(inside),
"A point inside the default text bounding box should be contained");
}
@Test
void pointOutsideBoundsShouldNotBeContained() {
Point outside = new Point(10, 10);
assertFalse(text.getBounds().contains(outside),
"A point outside the text bounding box should not be contained");
}
@Test
void topLeftCornerShouldBeContained() {
Point topLeft = new Point(50, 100);
assertTrue(text.getBounds().contains(topLeft),
"The top-left corner of the text bounding box (inclusive) should be contained");
}
@Test
void bottomRightCornerShouldNotBeContained() {
// Default bounds width=100, height=20 → exclusive corner at (150, 120)
Point bottomRight = new Point(150, 120);
assertFalse(text.getBounds().contains(bottomRight),
"The bottom-right corner of the text bounding box (exclusive) should not be contained");
}
@Test
void pointJustBeforeBottomRightShouldBeContained() {
Point justInside = new Point(149, 119);
assertTrue(text.getBounds().contains(justInside),
"A point one pixel before the exclusive bottom-right boundary should be contained");
}
@Test
void pointAfterTranslateShouldBeAtNewPosition() {
// Original bounds: (50, 100, 100, 20) — covers x∈[50,150), y∈[100,120)
SText moved = SText.create(50, 100, "Hello");
moved.translate(-40, 10); // new bounds: (10, 110, 100, 20) — covers x∈[10,110), y∈[110,130)
// (130, 105) was inside original bounds but is now outside (x=130 >= 110, y=105 < 110)
Point oldPosition = new Point(130, 105);
assertFalse(moved.getBounds().contains(oldPosition),
"After translate, a point that was in the old bounds should no longer be contained");
// (50, 115) is inside new bounds: x∈[10,110), y∈[110,130) ✓
Point newPosition = new Point(50, 115);
assertTrue(moved.getBounds().contains(newPosition),
"After translate, a point in the new bounds should be contained");
}
@Test
void blankTextUsesDefaultPlaceholderAndSameBounds() {
// Even blank/null text falls back to the placeholder — bounds remain (x, y, 100, 20)
SText blankText = SText.create(0, 0, "");
Point inside = new Point(50, 10);
assertTrue(blankText.getBounds().contains(inside),
"Blank text still uses default bounds; interior point should be contained");
}
}
// -----------------------------------------------------------------------
// SCollection — bounds = union of children bounds
// -----------------------------------------------------------------------
@Nested
class SCollectionContainsPoint {
// Two non-overlapping children:
// rect1: (10, 10, 50, 50) → covers x∈[10,60), y∈[10,60)
// rect2: (100, 100, 40, 40) → covers x∈[100,140), y∈[100,140)
// Union bounds: (10, 10, 130, 130) → exclusive corner: (140, 140)
private final SRectangle child1 = SRectangle.create(10, 10, 50, 50);
private final SRectangle child2 = SRectangle.create(100, 100, 40, 40);
private final SCollection collection = SCollection.of(child1, child2);
@Test
void pointInsideFirstChildBoundsShouldBeContained() {
Point insideChild1 = new Point(30, 30);
assertTrue(collection.getBounds().contains(insideChild1),
"A point inside the first child's bounds is within the union and should be contained");
}
@Test
void pointInsideSecondChildBoundsShouldBeContained() {
Point insideChild2 = new Point(120, 120);
assertTrue(collection.getBounds().contains(insideChild2),
"A point inside the second child's bounds is within the union and should be contained");
}
@Test
void pointInGapBetweenChildrenShouldBeContained() {
// The union rectangle spans the gap between the two children
Point inGap = new Point(70, 70);
assertTrue(collection.getBounds().contains(inGap),
"A point in the gap between children is still inside the union bounds and should be contained");
}
@Test
void pointOutsideUnionShouldNotBeContained() {
Point outside = new Point(0, 0);
assertFalse(collection.getBounds().contains(outside),
"A point outside the union bounds should not be contained");
}
@Test
void topLeftCornerOfUnionShouldBeContained() {
// Union top-left = (10, 10) — inclusive
Point topLeft = new Point(10, 10);
assertTrue(collection.getBounds().contains(topLeft),
"The top-left corner of the union bounds (inclusive) should be contained");
}
@Test
void bottomRightCornerOfUnionShouldNotBeContained() {
// Union: x=10, y=10, w=130, h=130 → exclusive corner (140, 140)
Point bottomRight = new Point(140, 140);
assertFalse(collection.getBounds().contains(bottomRight),
"The bottom-right corner of the union bounds (exclusive) should not be contained");
}
@Test
void pointAfterTranslatingChildrenShouldBeAtNewPosition() {
SRectangle movableChild = SRectangle.create(0, 0, 30, 30);
SCollection movable = SCollection.of(movableChild);
movable.translate(50, 50); // child now at (50, 50, 30, 30)
Point oldPoint = new Point(15, 15);
assertFalse(movable.getBounds().contains(oldPoint),
"After translate, a point at the old location should not be contained");
Point newPoint = new Point(65, 65);
assertTrue(movable.getBounds().contains(newPoint),
"After translate, a point at the new location should be contained");
}
@Test
void singleChildCollectionBoundsMatchChildBounds() {
SRectangle only = SRectangle.create(5, 5, 20, 20);
SCollection single = SCollection.of(only);
assertEquals(only.getBounds(), single.getBounds(),
"A single-child collection's bounds should equal the child's own bounds");
}
}
}

View File

@@ -1,223 +0,0 @@
package ovh.gasser.newshapes.shapes;
import ovh.gasser.newshapes.App;
import ovh.gasser.newshapes.attributes.SelectionAttributes;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.awt.*;
import static org.junit.jupiter.api.Assertions.*;
class EdgeCaseTest {
// -------------------------------------------------------------------------
// NegativeCoordinates
// -------------------------------------------------------------------------
@Nested
class NegativeCoordinates {
@Test
void rectangleAtNegativeCoordsShouldPreserveBounds() {
SRectangle rect = SRectangle.create(-10, -20, 50, 50);
Rectangle bounds = rect.getBounds();
assertEquals(-10, bounds.x, "x should be -10");
assertEquals(-20, bounds.y, "y should be -20");
assertEquals(50, bounds.width, "width should be 50");
assertEquals(50, bounds.height, "height should be 50");
}
@Test
void circleAtNegativeCoordsShouldPreserveBounds() {
SCircle circle = SCircle.create(-30, -40, 25);
Rectangle bounds = circle.getBounds();
assertEquals(-30, bounds.x, "x should be -30");
assertEquals(-40, bounds.y, "y should be -40");
assertEquals(50, bounds.width, "width should be radius*2 = 50");
assertEquals(50, bounds.height, "height should be radius*2 = 50");
}
@Test
void triangleAtNegativeCoordsShouldPreserveBounds() {
STriangle tri = STriangle.create(-5, -15, 60, Color.RED, Color.BLACK);
Rectangle bounds = tri.getBounds();
assertEquals(-5, bounds.x, "x should be -5");
assertEquals(-15, bounds.y, "y should be -15");
assertEquals(60, bounds.width, "width should be 60");
assertEquals(60, bounds.height, "height should be 60");
}
@Test
void translateToNegativePositionShouldUpdateBoundsCorrectly() {
SRectangle rect = SRectangle.create(10, 20, 80, 40);
rect.translate(-50, -70);
Rectangle bounds = rect.getBounds();
assertEquals(-40, bounds.x, "x after translate should be -40");
assertEquals(-50, bounds.y, "y after translate should be -50");
// dimensions must be unchanged
assertEquals(80, bounds.width, "width must remain 80 after translate");
assertEquals(40, bounds.height, "height must remain 40 after translate");
}
}
// -------------------------------------------------------------------------
// ZeroDimensions
// -------------------------------------------------------------------------
@Nested
class ZeroDimensions {
@Test
void rectangleWithZeroWidthShouldReturnZeroWidthFromFactory() {
SRectangle rect = SRectangle.create(0, 0, 0, 50);
assertEquals(0, rect.getBounds().width,
"factory should allow zero width without clamping");
}
@Test
void rectangleWithZeroHeightShouldReturnZeroHeightFromFactory() {
SRectangle rect = SRectangle.create(0, 0, 50, 0);
assertEquals(0, rect.getBounds().height,
"factory should allow zero height without clamping");
}
@Test
void circleWithZeroRadiusShouldHaveZeroBoundsDimensions() {
SCircle circle = SCircle.create(5, 10, 0);
assertEquals(0, circle.getRadius(), "radius should be 0");
assertEquals(0, circle.getBounds().width, "bounds width should be 0 (radius*2)");
assertEquals(0, circle.getBounds().height, "bounds height should be 0 (radius*2)");
}
}
// -------------------------------------------------------------------------
// ResizeClamping
// -------------------------------------------------------------------------
@Nested
class ResizeClamping {
@Test
void rectangleResizedToNegativeShouldClampWidthToOne() {
// Start with a 10x10 rect and drag the E handle far to the left
SRectangle rect = SRectangle.create(0, 0, 10, 10);
rect.resize(ResizeHandle.E, -200, 0);
assertTrue(rect.getBounds().width >= 1,
"width must be clamped to a minimum of 1 after negative resize");
assertEquals(1, rect.getBounds().width,
"width should be exactly 1 after extreme negative resize on E handle");
}
@Test
void rectangleResizedToNegativeShouldClampHeightToOne() {
SRectangle rect = SRectangle.create(0, 0, 10, 10);
rect.resize(ResizeHandle.S, 0, -200);
assertTrue(rect.getBounds().height >= 1,
"height must be clamped to a minimum of 1 after negative resize");
assertEquals(1, rect.getBounds().height,
"height should be exactly 1 after extreme negative resize on S handle");
}
@Test
void circleResizedToNegativeShouldClampToTwo() {
SCircle circle = SCircle.create(0, 0, 50);
// Drag E handle far to the left → shrink
circle.resize(ResizeHandle.E, -500, 0);
assertTrue(circle.getBounds().width >= 2,
"circle width must be clamped to a minimum of 2");
assertTrue(circle.getBounds().height >= 2,
"circle height must be clamped to a minimum of 2");
assertEquals(2, circle.getBounds().width,
"circle width should be exactly 2 after extreme negative resize");
assertEquals(2, circle.getBounds().height,
"circle height should be exactly 2 after extreme negative resize");
}
}
// -------------------------------------------------------------------------
// NullHandling
// -------------------------------------------------------------------------
@Nested
class NullHandling {
@Test
void textCreatedWithNullShouldReturnPlaceholder() {
SText text = SText.create(0, 0, null);
assertEquals(SText.PLACEHOLDER_TEXT, text.getText(),
"null input should be normalized to the placeholder text");
}
@Test
void textCreatedWithBlankStringShouldReturnPlaceholder() {
SText text = SText.create(0, 0, " ");
assertEquals(SText.PLACEHOLDER_TEXT, text.getText(),
"blank string input should be normalized to the placeholder text");
}
@Test
void getAttributesWithUnknownKeyShouldReturnNull() {
SRectangle rect = SRectangle.create(0, 0, 50, 50);
assertNull(rect.getAttributes("NONEXISTENT"),
"getAttributes() with an unknown key must return null");
}
@Test
void emptyCollectionCreationShouldNotThrow() {
assertDoesNotThrow(() -> SCollection.of(),
"SCollection.of() with no arguments must not throw");
}
@Test
void emptyCollectionGetBoundsShouldReturnWindowSize() {
SCollection collection = SCollection.of();
Rectangle bounds = collection.getBounds();
assertNotNull(bounds, "getBounds() on empty collection must not return null");
assertEquals(App.WIN_SIZE.width, bounds.width,
"empty collection bounds width should equal WIN_SIZE.width");
assertEquals(App.WIN_SIZE.height, bounds.height,
"empty collection bounds height should equal WIN_SIZE.height");
}
}
// -------------------------------------------------------------------------
// LargeValues
// -------------------------------------------------------------------------
@Nested
class LargeValues {
@Test
void rectangleAtMaxIntHalfCoordsShouldNotThrow() {
int halfMax = Integer.MAX_VALUE / 2;
assertDoesNotThrow(() -> SRectangle.create(halfMax, halfMax, 100, 100),
"SRectangle.create() with very large coordinates must not throw");
}
@Test
void rectangleAtMaxIntHalfCoordsPreservesBounds() {
int halfMax = Integer.MAX_VALUE / 2;
SRectangle rect = SRectangle.create(halfMax, halfMax, 100, 100);
assertEquals(halfMax, rect.getBounds().x, "x should be Integer.MAX_VALUE/2");
assertEquals(halfMax, rect.getBounds().y, "y should be Integer.MAX_VALUE/2");
assertEquals(100, rect.getBounds().width, "width should be 100");
assertEquals(100, rect.getBounds().height, "height should be 100");
}
@Test
void translateWithLargeValuesShouldNotThrow() {
SRectangle rect = SRectangle.create(0, 0, 50, 50);
// Integer overflow is a known platform behaviour; we only assert no exception
assertDoesNotThrow(() -> rect.translate(Integer.MAX_VALUE, Integer.MAX_VALUE),
"translate() with Integer.MAX_VALUE must not throw (overflow is acceptable)");
}
@Test
void translateWithLargeNegativeValuesShouldNotThrow() {
SRectangle rect = SRectangle.create(0, 0, 50, 50);
assertDoesNotThrow(() -> rect.translate(Integer.MIN_VALUE, Integer.MIN_VALUE),
"translate() with Integer.MIN_VALUE must not throw (overflow is acceptable)");
}
}
}

View File

@@ -1,94 +0,0 @@
package ovh.gasser.newshapes.shapes;
import org.junit.jupiter.api.Test;
import java.awt.Cursor;
import static org.junit.jupiter.api.Assertions.*;
class ResizeHandleTest {
@Test
void testEightHandlesExist() {
assertEquals(8, ResizeHandle.values().length,
"ResizeHandle enum should define exactly 8 handles");
}
@Test
void testAllHandleNamesExist() {
assertNotNull(ResizeHandle.valueOf("NW"), "Handle NW should exist");
assertNotNull(ResizeHandle.valueOf("N"), "Handle N should exist");
assertNotNull(ResizeHandle.valueOf("NE"), "Handle NE should exist");
assertNotNull(ResizeHandle.valueOf("E"), "Handle E should exist");
assertNotNull(ResizeHandle.valueOf("SE"), "Handle SE should exist");
assertNotNull(ResizeHandle.valueOf("S"), "Handle S should exist");
assertNotNull(ResizeHandle.valueOf("SW"), "Handle SW should exist");
assertNotNull(ResizeHandle.valueOf("W"), "Handle W should exist");
}
@Test
void testNwMapsToCorrectCursorType() {
assertEquals(Cursor.NW_RESIZE_CURSOR, ResizeHandle.NW.getCursorType(),
"NW handle should map to Cursor.NW_RESIZE_CURSOR");
}
@Test
void testNMapsToCorrectCursorType() {
assertEquals(Cursor.N_RESIZE_CURSOR, ResizeHandle.N.getCursorType(),
"N handle should map to Cursor.N_RESIZE_CURSOR");
}
@Test
void testNeMapsToCorrectCursorType() {
assertEquals(Cursor.NE_RESIZE_CURSOR, ResizeHandle.NE.getCursorType(),
"NE handle should map to Cursor.NE_RESIZE_CURSOR");
}
@Test
void testEMapsToCorrectCursorType() {
assertEquals(Cursor.E_RESIZE_CURSOR, ResizeHandle.E.getCursorType(),
"E handle should map to Cursor.E_RESIZE_CURSOR");
}
@Test
void testSeMapsToCorrectCursorType() {
assertEquals(Cursor.SE_RESIZE_CURSOR, ResizeHandle.SE.getCursorType(),
"SE handle should map to Cursor.SE_RESIZE_CURSOR");
}
@Test
void testSMapsToCorrectCursorType() {
assertEquals(Cursor.S_RESIZE_CURSOR, ResizeHandle.S.getCursorType(),
"S handle should map to Cursor.S_RESIZE_CURSOR");
}
@Test
void testSwMapsToCorrectCursorType() {
assertEquals(Cursor.SW_RESIZE_CURSOR, ResizeHandle.SW.getCursorType(),
"SW handle should map to Cursor.SW_RESIZE_CURSOR");
}
@Test
void testWMapsToCorrectCursorType() {
assertEquals(Cursor.W_RESIZE_CURSOR, ResizeHandle.W.getCursorType(),
"W handle should map to Cursor.W_RESIZE_CURSOR");
}
@Test
void testValueOfReturnsCorrectConstant() {
assertSame(ResizeHandle.NW, ResizeHandle.valueOf("NW"), "valueOf(\"NW\") should return ResizeHandle.NW");
assertSame(ResizeHandle.N, ResizeHandle.valueOf("N"), "valueOf(\"N\") should return ResizeHandle.N");
assertSame(ResizeHandle.NE, ResizeHandle.valueOf("NE"), "valueOf(\"NE\") should return ResizeHandle.NE");
assertSame(ResizeHandle.E, ResizeHandle.valueOf("E"), "valueOf(\"E\") should return ResizeHandle.E");
assertSame(ResizeHandle.SE, ResizeHandle.valueOf("SE"), "valueOf(\"SE\") should return ResizeHandle.SE");
assertSame(ResizeHandle.S, ResizeHandle.valueOf("S"), "valueOf(\"S\") should return ResizeHandle.S");
assertSame(ResizeHandle.SW, ResizeHandle.valueOf("SW"), "valueOf(\"SW\") should return ResizeHandle.SW");
assertSame(ResizeHandle.W, ResizeHandle.valueOf("W"), "valueOf(\"W\") should return ResizeHandle.W");
}
@Test
void testValueOfWithInvalidNameThrowsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class,
() -> ResizeHandle.valueOf("INVALID"),
"valueOf() with an unknown name should throw IllegalArgumentException");
}
}

View File

@@ -1,176 +0,0 @@
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<Shape> 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<Shape> 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<Shape> 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);
}
}

View File

@@ -44,40 +44,4 @@ class STextTest {
SText text = SText.create(0, 0, null); SText text = SText.create(0, 0, null);
assertEquals(SText.PLACEHOLDER_TEXT, text.getText()); assertEquals(SText.PLACEHOLDER_TEXT, text.getText());
} }
@Test
void testUpdateMeasuredBoundsActuallyUpdatesBounds() {
SText text = SText.create(10, 20, "Hello");
text.updateMeasuredBounds(200, 30);
Rectangle bounds = text.getBounds();
assertEquals(200, bounds.width, "updateMeasuredBounds must update width");
assertEquals(30, bounds.height, "updateMeasuredBounds must update height");
}
@Test
void testUpdateMeasuredBoundsDoesNotChangePosition() {
SText text = SText.create(10, 20, "Hello");
text.updateMeasuredBounds(200, 30);
Rectangle bounds = text.getBounds();
assertEquals(10, bounds.x, "updateMeasuredBounds must not change x");
assertEquals(20, bounds.y, "updateMeasuredBounds must not change y");
}
@Test
void testUpdateMeasuredBoundsNegativeClampedToZero() {
SText text = SText.create(0, 0, "Hello");
text.updateMeasuredBounds(-5, -10);
Rectangle bounds = text.getBounds();
assertEquals(0, bounds.width, "negative width must be clamped to 0");
assertEquals(0, bounds.height, "negative height must be clamped to 0");
}
@Test
void testUpdateMeasuredBoundsZeroIsAllowed() {
SText text = SText.create(0, 0, "Hello");
text.updateMeasuredBounds(0, 0);
Rectangle bounds = text.getBounds();
assertEquals(0, bounds.width);
assertEquals(0, bounds.height);
}
} }

View File

@@ -1,73 +0,0 @@
package ovh.gasser.newshapes.shapes;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.awt.Rectangle;
import static org.junit.jupiter.api.Assertions.*;
/**
* Parameterized contract tests verifying Shape interface invariants
* across all implementations (SRectangle, SCircle, STriangle, SText, SCollection).
*/
class ShapeContractTest {
@ParameterizedTest(name = "{0}")
@MethodSource("ovh.gasser.newshapes.shapes.ShapeFactory#allShapes")
void cloneReturnsIndependentCopy(Shape shape) {
Shape cloned = shape.clone();
// clone must not be the same instance
assertNotSame(shape, cloned, "clone() must return a new instance");
// clone must have equal bounds
assertEquals(shape.getBounds(), cloned.getBounds(),
"clone() must preserve bounds");
// mutating the clone must not affect the original
Rectangle originalBounds = shape.getBounds();
cloned.translate(999, 999);
assertEquals(originalBounds, shape.getBounds(),
"Translating the clone must not affect the original's bounds");
}
@ParameterizedTest(name = "{0}")
@MethodSource("ovh.gasser.newshapes.shapes.ShapeFactory#allShapes")
void getBoundsReturnsCopy(Shape shape) {
Rectangle bounds1 = shape.getBounds();
Rectangle bounds2 = shape.getBounds();
// successive calls must return equal bounds
assertEquals(bounds1, bounds2,
"getBounds() must return consistent values");
// but not the same object (defensive copy)
assertNotSame(bounds1, bounds2,
"getBounds() must return a copy, not internal state");
// mutating the returned Rectangle must not affect the shape
bounds1.translate(500, 500);
assertEquals(bounds2, shape.getBounds(),
"Mutating the returned Rectangle must not affect the shape");
}
@ParameterizedTest(name = "{0}")
@MethodSource("ovh.gasser.newshapes.shapes.ShapeFactory#allShapes")
void translateMutatesInPlace(Shape shape) {
Rectangle before = shape.getBounds();
int dx = 7, dy = -3;
shape.translate(dx, dy);
Rectangle after = shape.getBounds();
assertEquals(before.x + dx, after.x,
"translate() must shift x by dx");
assertEquals(before.y + dy, after.y,
"translate() must shift y by dy");
assertEquals(before.width, after.width,
"translate() must not change width");
assertEquals(before.height, after.height,
"translate() must not change height");
}
}

View File

@@ -1,25 +0,0 @@
package ovh.gasser.newshapes.shapes;
import java.awt.Color;
import java.util.stream.Stream;
/**
* Provides Shape instances for parameterized contract tests.
*/
public final class ShapeFactory {
private ShapeFactory() {}
static Stream<Shape> allShapes() {
return Stream.of(
SRectangle.create(10, 20, 100, 50),
SCircle.create(5, 5, 30),
STriangle.create(0, 0, 40, Color.RED, Color.BLACK),
SText.create(15, 25, "Hello"),
SCollection.of(
SRectangle.create(0, 0, 20, 20),
SCircle.create(10, 10, 5)
)
);
}
}

View File

@@ -1,370 +0,0 @@
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 SVGDraftmanTest {
private StringWriter buffer;
private PrintWriter writer;
private SVGDraftman draftman;
@BeforeEach
void setUp() {
buffer = new StringWriter();
writer = new PrintWriter(buffer);
draftman = new SVGDraftman(writer);
}
private String output() {
writer.flush();
return buffer.toString();
}
// ── visitRectangle ──────────────────────────────────────────────
@Nested
class VisitRectangleTests {
@Test
void testRectangleElementWithCorrectDimensions() {
SRectangle rect = SRectangle.create(10, 20, 100, 50);
draftman.visitRectangle(rect);
String svg = output();
assertTrue(svg.contains("<rect"), "Should produce a <rect> element");
assertTrue(svg.contains("width=\"100\""), "Should have correct width");
assertTrue(svg.contains("height=\"50\""), "Should have correct height");
assertTrue(svg.contains("x=\"10\""), "Should have correct x");
assertTrue(svg.contains("y=\"20\""), "Should have correct y");
}
@Test
void testRectangleFilledOnly() {
SRectangle rect = SRectangle.create(0, 0, 30, 30);
rect.addAttributes(new ColorAttributes(true, false, Color.RED, Color.BLACK));
draftman.visitRectangle(rect);
String svg = output();
assertTrue(svg.contains("fill=\"#ff0000\""), "Should contain fill color");
assertFalse(svg.contains("stroke="), "Should not contain stroke when stroked=false");
}
@Test
void testRectangleStrokedOnly() {
SRectangle rect = SRectangle.create(0, 0, 30, 30);
rect.addAttributes(new ColorAttributes(false, true, Color.RED, Color.BLUE));
draftman.visitRectangle(rect);
String svg = output();
assertTrue(svg.contains("stroke=\"#0000ff\""), "Should contain stroke color");
assertTrue(svg.contains("stroke-width=\"1\""), "Should contain stroke-width");
assertTrue(svg.contains("fill=\"none\""), "Should have fill=none when not filled");
}
@Test
void testRectangleFilledAndStroked() {
SRectangle rect = SRectangle.create(0, 0, 30, 30);
rect.addAttributes(new ColorAttributes(true, true, Color.RED, Color.BLUE));
draftman.visitRectangle(rect);
String svg = output();
assertTrue(svg.contains("fill=\"#ff0000\""), "Should contain fill color");
assertTrue(svg.contains("stroke=\"#0000ff\""), "Should contain stroke color");
}
@Test
void testRectangleNeitherFilledNorStroked() {
SRectangle rect = SRectangle.create(0, 0, 30, 30);
rect.addAttributes(new ColorAttributes(false, false, Color.RED, Color.BLUE));
draftman.visitRectangle(rect);
String svg = output();
assertTrue(svg.contains("fill=\"none\""), "Should have fill=none");
assertFalse(svg.contains("stroke="), "Should not have stroke attribute");
}
}
// ── visitCircle ─────────────────────────────────────────────────
@Nested
class VisitCircleTests {
@Test
void testCircleElementWithCorrectAttributes() {
SCircle circle = SCircle.create(50, 60, 30);
draftman.visitCircle(circle);
String svg = output();
assertTrue(svg.contains("<circle"), "Should produce a <circle> element");
// cx = x + r = 50 + 30 = 80, cy = y + r = 60 + 30 = 90
assertTrue(svg.contains("cx=\"80\""), "cx should be x + radius");
assertTrue(svg.contains("cy=\"90\""), "cy should be y + radius");
assertTrue(svg.contains("r=\"30\""), "Should have correct radius");
}
@Test
void testCircleFilledOnly() {
SCircle circle = SCircle.create(0, 0, 20);
circle.addAttributes(new ColorAttributes(true, false, Color.GREEN, Color.BLACK));
draftman.visitCircle(circle);
String svg = output();
assertTrue(svg.contains("fill=\"#00ff00\""), "Should contain fill color");
assertFalse(svg.contains("stroke="), "Should not contain stroke");
}
@Test
void testCircleStrokedOnly() {
SCircle circle = SCircle.create(0, 0, 20);
circle.addAttributes(new ColorAttributes(false, true, Color.RED, Color.MAGENTA));
draftman.visitCircle(circle);
String svg = output();
assertTrue(svg.contains("stroke=\"#ff00ff\""), "Should contain stroke color");
assertTrue(svg.contains("fill=\"none\""), "Should have fill=none");
}
@Test
void testCircleFilledAndStroked() {
SCircle circle = SCircle.create(0, 0, 15);
circle.addAttributes(new ColorAttributes(true, true, Color.YELLOW, Color.BLACK));
draftman.visitCircle(circle);
String svg = output();
assertTrue(svg.contains("fill=\"#ffff00\""), "Should contain fill color");
assertTrue(svg.contains("stroke=\"#000000\""), "Should contain stroke color");
}
}
// ── visitTriangle ───────────────────────────────────────────────
@Nested
class VisitTriangleTests {
@Test
void testTriangleProducesPolygonElement() {
STriangle tri = STriangle.create(10, 20, 40, Color.RED, Color.BLACK);
draftman.visitTriangle(tri);
String svg = output();
assertTrue(svg.contains("<polygon"), "Should produce a <polygon> element");
assertTrue(svg.contains("points="), "Should have points attribute");
}
@Test
void testTrianglePointsAreCorrect() {
STriangle tri = STriangle.create(10, 20, 40, Color.RED, Color.BLACK);
draftman.visitTriangle(tri);
String svg = output();
// bottom-left: (x, y+size) = (10, 60)
assertTrue(svg.contains("10,60"), "Should contain bottom-left point");
// top-center: (x + size/2, y) = (30, 20)
assertTrue(svg.contains("30,20"), "Should contain top-center point");
// bottom-right: (x + size, y + size) = (50, 60)
assertTrue(svg.contains("50,60"), "Should contain bottom-right point");
}
@Test
void testTriangleFilledOnly() {
STriangle tri = STriangle.create(0, 0, 20, Color.BLUE, Color.BLACK);
tri.addAttributes(new ColorAttributes(true, false, Color.BLUE, Color.BLACK));
draftman.visitTriangle(tri);
String svg = output();
assertTrue(svg.contains("fill:#0000ff"), "Should contain fill style");
assertFalse(svg.contains("stroke:"), "Should not contain stroke style when not stroked");
}
@Test
void testTriangleStrokedAndFilled() {
STriangle tri = STriangle.create(0, 0, 20, Color.RED, Color.GREEN);
tri.addAttributes(new ColorAttributes(true, true, Color.RED, Color.GREEN));
draftman.visitTriangle(tri);
String svg = output();
assertTrue(svg.contains("fill:#ff0000"), "Should contain fill style");
assertTrue(svg.contains("stroke:#00ff00"), "Should contain stroke style");
}
@Test
void testTriangleNotFilled() {
STriangle tri = STriangle.create(0, 0, 20, Color.RED, Color.BLACK);
tri.addAttributes(new ColorAttributes(false, true, Color.RED, Color.BLACK));
draftman.visitTriangle(tri);
String svg = output();
assertTrue(svg.contains("fill:none"), "Should have fill:none when not filled");
}
}
// ── visitText ───────────────────────────────────────────────────
@Nested
class VisitTextTests {
@Test
void testTextElementWithCorrectContent() {
SText text = SText.create(10, 20, "Hello");
draftman.visitText(text);
String svg = output();
assertTrue(svg.contains("<text"), "Should produce a <text> element");
assertTrue(svg.contains(">Hello</text>"), "Should contain the text content");
}
@Test
void testTextPositionIncludesFontSizeOffset() {
SText text = SText.create(10, 20, "Hi");
draftman.visitText(text);
String svg = output();
assertTrue(svg.contains("x=\"10\""), "x should match bounds.x");
// y = bounds.y + fontSize = 20 + 16 = 36
assertTrue(svg.contains("y=\"36\""), "y should be bounds.y + fontSize");
}
@Test
void testTextDefaultFontAttributes() {
SText text = SText.create(0, 0, "Test");
draftman.visitText(text);
String svg = output();
assertTrue(svg.contains("font-family=\"SansSerif\""), "Should use default font family");
assertTrue(svg.contains("font-size=\"16\""), "Should use default font size");
assertTrue(svg.contains("font-style=\"normal\""), "Default style should be normal");
assertTrue(svg.contains("font-weight=\"normal\""), "Default weight should be normal");
}
@Test
void testTextDefaultColorIsBlack() {
SText text = SText.create(0, 0, "Test");
// default ColorAttributes: filled=true, filledColor=BLACK
draftman.visitText(text);
String svg = output();
assertTrue(svg.contains("fill=\"#000000\""), "Default text color should be black");
}
@Test
void testTextWithCustomFillColor() {
SText text = SText.create(0, 0, "Colored");
text.addAttributes(new ColorAttributes(true, false, Color.RED, Color.BLACK));
draftman.visitText(text);
String svg = output();
assertTrue(svg.contains("fill=\"#ff0000\""), "Text should use filledColor");
}
@Test
void testTextWithNullColorAttributesFallsBackToBlack() {
SText text = SText.create(0, 0, "NoColor");
text.addAttributes(new ColorAttributes(false, false, null, null));
draftman.visitText(text);
String svg = output();
assertTrue(svg.contains("fill=\"#000000\""), "Should fall back to black with null colors");
}
}
// ── visitCollection ─────────────────────────────────────────────
@Nested
class VisitCollectionTests {
@Test
void testEmptyCollection() {
SCollection empty = SCollection.of();
draftman.visitCollection(empty);
String svg = output();
assertEquals("", svg, "Empty collection should produce no output");
}
@Test
void testCollectionVisitsAllChildren() {
SCollection coll = SCollection.of(
SRectangle.create(0, 0, 10, 10),
SCircle.create(20, 20, 5)
);
draftman.visitCollection(coll);
String svg = output();
assertTrue(svg.contains("<rect"), "Should visit rectangle");
assertTrue(svg.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);
String svg = output();
assertTrue(svg.contains("<rect"), "Should visit nested rectangle");
assertTrue(svg.contains("<circle"), "Should visit circle at outer level");
}
}
// ── generateSVG ─────────────────────────────────────────────────
@Nested
class GenerateSVGTests {
@Test
void testGenerateSVGIncludesXmlDeclaration() {
SCollection model = SCollection.of();
draftman.generateSVG(model);
String svg = output();
assertTrue(svg.contains("<?xml version=\"1.0\" encoding=\"utf-8\"?>"),
"Should include XML declaration");
}
@Test
void testGenerateSVGIncludesSvgNamespace() {
SCollection model = SCollection.of();
draftman.generateSVG(model);
String svg = output();
assertTrue(svg.contains("xmlns=\"http://www.w3.org/2000/svg\""),
"Should include SVG namespace");
}
@Test
void testGenerateSVGClosesSvgTag() {
SCollection model = SCollection.of();
draftman.generateSVG(model);
String svg = output();
assertTrue(svg.contains("</svg>"), "Should close <svg> tag");
}
@Test
void testGenerateSVGIncludesShapeContent() {
SCollection model = SCollection.of(
SRectangle.create(1, 2, 3, 4)
);
draftman.generateSVG(model);
String svg = output();
assertTrue(svg.contains("<rect"), "Should include shape elements");
assertTrue(svg.contains("</svg>"), "Should close SVG after shapes");
}
}
}

View File

@@ -1,48 +0,0 @@
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<String> {
private final List<String> elements;
TestStreamable(List<String> elements) {
this.elements = new ArrayList<>(elements);
}
@Override
public java.util.Iterator<String> iterator() {
return elements.iterator();
}
}
@Test
void testStreamReturnsStreamOfElements() {
List<String> testData = Arrays.asList("a", "b", "c");
Streamable<String> streamable = new TestStreamable(testData);
Stream<String> result = streamable.stream();
assertNotNull(result);
assertEquals(testData, result.toList());
}
@Test
void testStreamEmptyCollection() {
List<String> emptyData = new ArrayList<>();
Streamable<String> streamable = new TestStreamable(emptyData);
Stream<String> result = streamable.stream();
assertNotNull(result);
assertTrue(result.toList().isEmpty());
}
}