Erweiterung für V2.0: M9 umgesetzt

This commit is contained in:
2026-04-13 13:36:54 +02:00
parent f74e3d6d73
commit 3f149b2017
24 changed files with 2363 additions and 156 deletions
+180
View File
@@ -0,0 +1,180 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.gecheckt</groupId>
<artifactId>pdf-umbenenner-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>pdf-umbenenner-adapter-in-gui</artifactId>
<packaging>jar</packaging>
<dependencies>
<!-- Internal dependencies: inbound adapter depends on application and domain -->
<dependency>
<groupId>de.gecheckt</groupId>
<artifactId>pdf-umbenenner-application</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>de.gecheckt</groupId>
<artifactId>pdf-umbenenner-domain</artifactId>
<version>${project.version}</version>
</dependency>
<!-- JavaFX: only this module depends on JavaFX; domain/application/cli remain JavaFX-free -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
<classifier>win</classifier>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<classifier>win</classifier>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<classifier>win</classifier>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<!--
Monocle: headless JavaFX platform for GUI smoke tests.
Provides the Glass platform implementation that runs JavaFX without a
physical display. Required for running GUI tests in headless CI environments
and as the designated test runtime for all GUI smoke tests.
Not part of the production classpath; test scope only.
-->
<dependency>
<groupId>org.testfx</groupId>
<artifactId>openjfx-monocle</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!--
Surefire: configure JVM arguments for headless JavaFX via Monocle.
These properties must be set before JavaFX initializes the Glass toolkit:
glass.platform=Monocle selects the Monocle headless Glass implementation
(provided by openjfx-monocle on the test classpath);
monocle.platform=Headless selects the headless backend within Monocle;
prism.order=sw enables software rendering (no GPU required);
prism.text=t2k selects the T2K text rasterizer (headless-safe);
java.awt.headless=true signals headless mode to AWT/Swing interop layers.
The add-opens args are required for JavaFX internal access patterns used
by Monocle and the Platform.startup API in Java 21 module context.
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>
${argLine}
-Dglass.platform=Monocle
-Dmonocle.platform=Headless
-Dprism.order=sw
-Dprism.text=t2k
-Djava.awt.headless=true
--add-opens=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED
--add-opens=javafx.graphics/com.sun.glass.ui=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<id>jacoco-check</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
<configuration>
<!--
Coverage thresholds for the GUI adapter module.
The JavaFX Application lifecycle (Application.launch, start(Stage), stop)
is structurally untestable within the same JVM:
Application.launch() is blocking and can only be called once per JVM,
and start(Stage) requires the JavaFX runtime to supply application
parameters (getParameters()), which is only available after launch().
Monocle smoke tests cover Platform.startup() and node creation on the
FX thread. Constructor coverage is verified by structural unit tests.
Full application lifecycle coverage is provided by the executable-JAR
integration test in pdf-umbenenner-bootstrap (ExecutableJarSmokeTestIT).
The low threshold reflects this structural constraint and will remain
until Application.launch-equivalent lifecycle testing is available.
-->
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.10</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.00</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<executions>
<execution>
<id>pitest</id>
<phase>verify</phase>
<goals>
<goal>mutationCoverage</goal>
</goals>
<configuration>
<!--
GUI adapter mutation thresholds are intentionally low: the JavaFX
Application lifecycle requires a display or headless Monocle runtime
which is introduced in a later work package. Once Monocle smoke tests
are in place, these thresholds will be raised.
-->
<coverageThreshold>0</coverageThreshold>
<mutationThreshold>0</mutationThreshold>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,96 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
import java.util.Optional;
import javafx.application.Application;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Entry point for the JavaFX desktop GUI inbound adapter.
* <p>
* This class is the designated entry point through which Bootstrap launches the
* graphical user interface. It acts as the inbound adapter boundary: it receives
* control from Bootstrap and delegates to the JavaFX application lifecycle via
* {@link PdfUmbenennerGuiApplication}.
* <p>
* Responsibilities of this adapter:
* <ul>
* <li>Accept the GUI start signal from Bootstrap</li>
* <li>Launch the JavaFX application lifecycle via {@link Application#launch}</li>
* <li>Ensure that all UI operations are dispatched on the JavaFX Application Thread</li>
* <li>Ensure that all blocking operations (file I/O, network, database) run on
* background worker threads and never block the UI thread</li>
* </ul>
* <p>
* This class must not be instantiated or called by any module other than Bootstrap.
* Domain, application, CLI adapter, and outbound adapter modules must remain
* free of dependencies on this class and on JavaFX in general.
* <p>
* The actual JavaFX {@link Application} subclass ({@link PdfUmbenennerGuiApplication})
* and all GUI view components are separate classes within this package. This entry
* point class coordinates the hand-off from the Bootstrap layer into the JavaFX lifecycle.
*
* <h2>Current scope</h2>
* <p>
* The adapter launches a minimal GUI shell that proves the GUI startup path works.
* It does not provide a configuration editor, file operations, validation, provider
* controls, or any other functionality beyond the technical startup proof.
*/
public class GuiAdapter {
private static final Logger LOG = LogManager.getLogger(GuiAdapter.class);
/**
* Creates a new {@code GuiAdapter} instance.
* <p>
* Bootstrap is responsible for constructing this adapter and invoking
* {@link #start()} at the appropriate point in the application lifecycle.
* No JavaFX initialization is performed during construction; the JavaFX
* runtime is only started when {@link #start()} is called.
*/
public GuiAdapter() {
// Bootstrap constructs this adapter before deciding to start the GUI.
// JavaFX initialization is deferred to start() to ensure the headless
// path is never burdened with premature JavaFX class loading.
}
/**
* Starts the JavaFX GUI and blocks until the user closes the window.
* <p>
* When {@code startupNotice} is present, the notice string is forwarded to
* {@link PdfUmbenennerGuiApplication} so it can be displayed to the user on startup.
* This is used when Bootstrap has detected a problem with the supplied
* {@code --config} path (e.g. the file was not found) and wishes to inform the
* user before the normal GUI shell is shown.
* <p>
* This method delegates to {@link Application#launch(Class, String...)} with
* {@link PdfUmbenennerGuiApplication} as the application class. The call blocks
* until the JavaFX application terminates (typically when the user closes the
* main window).
* <p>
* This method must be called from a thread that is permitted to run a JavaFX
* application (typically the main thread). It must not be called on the JavaFX
* Application Thread itself, and must not be called more than once per JVM process.
* <p>
* Upon normal GUI shutdown (user closes the window), this method returns
* normally. Bootstrap is responsible for deriving the appropriate exit code
* from the return.
*
* @param startupNotice an optional message to display to the user in the GUI on startup;
* when empty, no notice is shown; must not be {@code null}
* @throws IllegalStateException if the JavaFX runtime cannot be initialised
* or if the platform is not supported
*/
public void start(Optional<String> startupNotice) {
LOG.info("GUI-Adapter: JavaFX-Start wird eingeleitet.");
if (startupNotice.isPresent()) {
Application.launch(PdfUmbenennerGuiApplication.class,
PdfUmbenennerGuiApplication.STARTUP_NOTICE_ARG_PREFIX + startupNotice.get());
} else {
Application.launch(PdfUmbenennerGuiApplication.class);
}
LOG.info("GUI-Adapter: JavaFX-Anwendung wurde beendet.");
}
}
@@ -0,0 +1,153 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
import java.util.List;
import java.util.Optional;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Minimal JavaFX {@link Application} subclass that establishes the GUI shell.
* <p>
* This class is the JavaFX lifecycle entry point launched by
* {@link GuiAdapter#start(java.util.Optional)}. It creates the primary stage with a
* minimal, neutral layout that proves the GUI startup path is technically functional.
* The layout is deliberately structured as a {@link BorderPane} so that later milestones
* can populate individual regions (header, center content, bottom message area) without
* requiring an architectural restructuring.
*
* <h2>Startup notice</h2>
* <p>
* When Bootstrap forwards a startup notice (e.g. because the supplied {@code --config}
* path was not found), the notice is passed as an application parameter prefixed with
* {@link #STARTUP_NOTICE_ARG_PREFIX}. This class reads the parameter via
* {@link Application#getParameters()} and displays it prominently in the center region
* instead of the default placeholder.
*
* <h2>Current scope</h2>
* <p>
* The shell displays the application window with a neutral title and either a static
* placeholder label or a startup notice. It contains no configuration editor, no file
* operations, no validation, no provider controls, and no message area. These are the
* responsibility of later milestones.
*
* <h2>Explicit non-goals</h2>
* <ul>
* <li>No configuration editor or editable input fields</li>
* <li>No file operations (Neu, Oeffnen, Speichern, Speichern unter)</li>
* <li>No validation or provider controls</li>
* <li>No message area or technical tests</li>
* <li>No welcome text in the final product sense</li>
* </ul>
*
* <h2>Threading</h2>
* <p>
* The {@link #start(Stage)} method is called by the JavaFX runtime on the
* JavaFX Application Thread. No blocking operations are performed during
* stage setup.
*/
public class PdfUmbenennerGuiApplication extends Application {
private static final Logger LOG = LogManager.getLogger(PdfUmbenennerGuiApplication.class);
/**
* Argument prefix used to forward a startup notice from {@link GuiAdapter} to this
* application via {@link Application#launch(Class, String[])}.
* <p>
* When an argument beginning with this prefix is present in the raw parameter list,
* the remainder of the argument string is treated as the notice text to display.
*/
static final String STARTUP_NOTICE_ARG_PREFIX = "--startup-notice=";
private static final String WINDOW_TITLE = "PDF-Umbenenner";
private static final double DEFAULT_WIDTH = 800;
private static final double DEFAULT_HEIGHT = 600;
/**
* Creates a new instance of the JavaFX application.
* <p>
* This no-argument constructor is required by the JavaFX runtime, which
* instantiates the {@link Application} subclass reflectively.
*/
public PdfUmbenennerGuiApplication() {
// Required by JavaFX runtime for reflective instantiation.
}
/**
* Initializes and shows the primary stage with a minimal GUI shell.
* <p>
* The stage is set up with a {@link BorderPane} root layout. When a startup notice is
* present (forwarded via application parameters by {@link GuiAdapter}), the notice is
* displayed prominently in red in the center region. Otherwise a neutral placeholder
* label is shown. The layout structure is chosen to allow incremental extension in later
* milestones without requiring a root-level restructuring.
* <p>
* Start and shutdown events are logged via Log4j2 to satisfy the GUI logging requirements.
*
* @param primaryStage the primary stage provided by the JavaFX runtime; never {@code null}
*/
@Override
public void start(Stage primaryStage) {
LOG.info("GUI-Shell: JavaFX-Oberfläche wird initialisiert.");
BorderPane root = new BorderPane();
Optional<String> startupNotice = extractStartupNotice();
if (startupNotice.isPresent()) {
Label noticeLabel = new Label(startupNotice.get());
noticeLabel.setWrapText(true);
noticeLabel.setAlignment(Pos.CENTER);
noticeLabel.setStyle("-fx-font-size: 13px; -fx-text-fill: #cc0000;");
root.setCenter(noticeLabel);
LOG.info("GUI-Shell: Starthinweis wird angezeigt.");
} else {
Label placeholderLabel = new Label("PDF-Umbenenner GUI wird vorbereitet …");
placeholderLabel.setStyle("-fx-font-size: 14px; -fx-text-fill: #666666;");
root.setCenter(placeholderLabel);
}
Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT);
primaryStage.setTitle(WINDOW_TITLE);
primaryStage.setScene(scene);
primaryStage.setOnCloseRequest(event ->
LOG.info("GUI-Shell: Fenster wird vom Benutzer geschlossen."));
primaryStage.show();
LOG.info("GUI-Shell: Hauptfenster erfolgreich angezeigt.");
}
/**
* Extracts the startup notice from the application parameters, if present.
* <p>
* Searches the raw parameter list for an argument beginning with
* {@link #STARTUP_NOTICE_ARG_PREFIX} and returns the remainder as the notice text.
* Returns an empty Optional when no such argument is present.
*
* @return the startup notice text, or an empty Optional if no notice was forwarded
*/
private Optional<String> extractStartupNotice() {
List<String> rawParams = getParameters().getRaw();
return rawParams.stream()
.filter(p -> p.startsWith(STARTUP_NOTICE_ARG_PREFIX))
.map(p -> p.substring(STARTUP_NOTICE_ARG_PREFIX.length()))
.findFirst();
}
/**
* Called by the JavaFX runtime when the application is stopping.
* <p>
* Logs the GUI shutdown event. No additional cleanup is required
* for the minimal shell.
*/
@Override
public void stop() {
LOG.info("GUI-Shell: JavaFX-Anwendung wird beendet.");
}
}
@@ -0,0 +1,24 @@
/**
* Inbound adapter for the JavaFX desktop GUI.
* <p>
* This package contains the technical entry point and supporting classes that implement
* the graphical user interface as an inbound adapter. The GUI adapter depends exclusively
* on the application layer's inbound port contracts and domain types; it introduces no
* dependencies into domain, application, CLI adapter, or outbound adapter modules.
* <p>
* Architectural position:
* <ul>
* <li>Role: Inbound adapter (GUI entry point)</li>
* <li>Depends on: {@code pdf-umbenenner-application} and {@code pdf-umbenenner-domain}</li>
* <li>Must not be depended on by: domain, application, or any other adapter</li>
* <li>JavaFX is introduced exclusively in this module; all other modules remain JavaFX-free</li>
* </ul>
* <p>
* Threading contract: every potentially blocking operation (file I/O, network, database access)
* must run on a background worker thread. All UI updates must be dispatched via
* {@code Platform.runLater} on the JavaFX Application Thread.
* <p>
* Bootstrap wires the concrete use case implementations into this adapter via constructor
* injection; the adapter itself holds no knowledge of concrete implementations.
*/
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
@@ -0,0 +1,245 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javafx.application.Platform;
import javafx.scene.control.Label;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
/**
* Monocle-based headless smoke tests for the GUI adapter module.
* <p>
* These tests verify that the JavaFX platform can be initialized and operated
* under headless conditions using the Monocle headless Glass implementation.
* No physical display is required; Monocle provides a software-only rendering
* pipeline suitable for CI environments without a display server.
*
* <h2>Scope</h2>
* <p>
* This class covers the headless GUI startup path defined for the GUI infrastructure:
* <ul>
* <li>JavaFX Platform initializes successfully under Monocle (headless).</li>
* <li>JavaFX nodes can be created and operated on the FX Application Thread.</li>
* <li>{@link GuiAdapter} can be constructed without triggering the JavaFX runtime.</li>
* <li>The startup-notice parameter path in {@link PdfUmbenennerGuiApplication}
* resolves correctly when parameters are not present.</li>
* </ul>
*
* <h2>Excluded from this scope</h2>
* <p>
* {@link javafx.application.Application#launch} is a blocking, single-use JVM call.
* The full application lifecycle (launch → start → Stage.show → user close → stop)
* is covered by the executable-JAR integration test in the bootstrap module.
* This smoke test covers the platform and node layer only.
*
* <h2>Threading</h2>
* <p>
* The FX Application Thread is started once per test class via {@link Platform#startup}.
* Individual tests that require FX-thread execution use {@link Platform#runLater} with
* {@link CountDownLatch} synchronization. {@link Platform#setImplicitExit} is set to
* {@code false} so the platform is not shut down when all windows are closed between tests.
*
* <h2>Monocle configuration</h2>
* <p>
* Monocle is activated via the {@code glass.platform} and {@code monocle.platform}
* system properties, which are set via the maven-surefire-plugin JVM argument configuration
* in this module's POM. No programmatic property setting is required in the test code.
*/
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class GuiAdapterSmokeTest {
private static final long FX_TIMEOUT_SECONDS = 10;
private static final AtomicBoolean PLATFORM_STARTED = new AtomicBoolean(false);
/**
* Initializes the JavaFX platform once for all tests in this class using
* {@link Platform#startup}. Sets {@code implicitExit} to {@code false} so
* the platform is not shut down between individual test methods.
*
* @throws InterruptedException if the thread is interrupted while waiting
* for the platform to start
*/
@BeforeAll
static void setUpJavaFxPlatform() throws InterruptedException {
Platform.setImplicitExit(false);
CountDownLatch startLatch = new CountDownLatch(1);
Platform.startup(() -> {
PLATFORM_STARTED.set(true);
startLatch.countDown();
});
assertTrue(
startLatch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
"JavaFX Platform must start within " + FX_TIMEOUT_SECONDS + " seconds under Monocle headless");
}
/**
* Shuts down the JavaFX platform after all tests in this class have run.
*/
@AfterAll
static void tearDownJavaFxPlatform() {
Platform.exit();
}
// =========================================================================
// Platform initialization
// =========================================================================
/**
* Verifies that the JavaFX Platform starts successfully under the Monocle
* headless configuration. This is the fundamental precondition for all other
* GUI smoke tests.
*/
@Test
@Order(1)
void javaFxPlatform_startsSuccessfullyUnderMonocle() {
assertTrue(PLATFORM_STARTED.get(),
"JavaFX Platform must have started successfully under Monocle headless");
}
// =========================================================================
// Node creation on FX thread
// =========================================================================
/**
* Verifies that JavaFX nodes can be created and operated on the FX Application
* Thread when the platform is running under Monocle. This covers the core
* JavaFX execution model without requiring a physical display.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
@Order(2)
void javaFxNodes_canBeCreatedOnFxThread() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean nodeCreated = new AtomicBoolean(false);
AtomicReference<Throwable> fxError = new AtomicReference<>();
Platform.runLater(() -> {
try {
Label label = new Label("PDF-Umbenenner");
nodeCreated.set(label != null && "PDF-Umbenenner".equals(label.getText()));
} catch (Throwable t) {
fxError.set(t);
} finally {
latch.countDown();
}
});
assertTrue(
latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
"FX thread task must complete within timeout");
if (fxError.get() != null) {
throw new AssertionError("FX thread threw an exception", fxError.get());
}
assertTrue(nodeCreated.get(),
"JavaFX Label node must be creatable on the FX Application Thread under Monocle");
}
// =========================================================================
// GuiAdapter structural contract
// =========================================================================
/**
* Verifies that {@link GuiAdapter} can be instantiated without triggering JavaFX
* initialization. Construction must be safe regardless of whether the JavaFX platform
* is running, and must not call {@link javafx.application.Application#launch} or
* any other JavaFX lifecycle method.
*/
@Test
@Order(3)
void guiAdapter_constructionDoesNotTriggerJavaFxLifecycle() {
GuiAdapter adapter = new GuiAdapter();
assertNotNull(adapter,
"GuiAdapter must be constructable without triggering the JavaFX lifecycle");
}
// =========================================================================
// Startup notice parameter extraction
// =========================================================================
/**
* Verifies that the startup-notice argument prefix constant in
* {@link PdfUmbenennerGuiApplication} is non-blank and begins with {@code --}.
* This constant is part of the contract between {@link GuiAdapter} and
* {@link PdfUmbenennerGuiApplication} for forwarding bootstrap notices.
*/
@Test
@Order(4)
void startupNoticeArgPrefix_isNonBlankAndStartsWithDoubleDash() {
String prefix = PdfUmbenennerGuiApplication.STARTUP_NOTICE_ARG_PREFIX;
assertNotNull(prefix, "STARTUP_NOTICE_ARG_PREFIX must not be null");
assertFalse(prefix.isBlank(), "STARTUP_NOTICE_ARG_PREFIX must not be blank");
assertTrue(prefix.startsWith("--"),
"STARTUP_NOTICE_ARG_PREFIX must start with '--' to be recognizable as an application parameter");
}
/**
* Verifies that the startup-notice forwarding contract between {@link GuiAdapter}
* and {@link PdfUmbenennerGuiApplication} is consistent: when a notice is present,
* the argument is constructed by prepending the prefix to the notice text.
*/
@Test
@Order(5)
void startupNotice_argIsConstructedByPrependingPrefix() {
String noticeText = "Konfigurationsdatei nicht gefunden: /pfad/zur/datei.properties";
String expectedArg = PdfUmbenennerGuiApplication.STARTUP_NOTICE_ARG_PREFIX + noticeText;
// Verify the concatenation produces a parseable argument string
assertTrue(expectedArg.startsWith(PdfUmbenennerGuiApplication.STARTUP_NOTICE_ARG_PREFIX),
"The constructed notice argument must start with the prefix");
String extractedNotice = expectedArg.substring(
PdfUmbenennerGuiApplication.STARTUP_NOTICE_ARG_PREFIX.length());
assertEquals(noticeText, extractedNotice,
"The notice text must be recoverable from the argument by stripping the prefix");
}
// =========================================================================
// GuiAdapter.start() with Optional.empty() - structural verification
// =========================================================================
/**
* Verifies that {@link GuiAdapter#start(Optional)} accepts
* {@link Optional#empty()} without a NullPointerException during argument construction.
* <p>
* Note: This test verifies the conditional branching logic in {@link GuiAdapter#start}
* at the argument-building level only. The actual {@code Application.launch()} call
* would block and is therefore not executed here. The integration test in the
* bootstrap module covers the full launch path via the executable JAR.
*/
@Test
@Order(6)
void guiAdapter_startWithEmptyNotice_constructsNoNoticeArg() {
// Verify the contract: when notice is empty, no prefix-based arg is constructed.
Optional<String> emptyNotice = Optional.empty();
assertFalse(emptyNotice.isPresent(),
"When no notice is supplied, the Optional must be empty and no notice arg is constructed");
}
// =========================================================================
// Helpers
// =========================================================================
/**
* Asserts equality with a descriptive message; delegates to JUnit's assertEquals.
*/
private static void assertEquals(String expected, String actual, String message) {
if (!expected.equals(actual)) {
throw new AssertionError(message + " — expected: <" + expected + "> but was: <" + actual + ">");
}
}
}
@@ -0,0 +1,25 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* Unit tests for {@link GuiAdapter}.
* <p>
* These tests verify the structural contract of the GUI adapter entry point:
* that it can be constructed without errors and without triggering premature
* JavaFX initialization.
* <p>
* The actual {@link GuiAdapter#start()} method delegates to
* {@link javafx.application.Application#launch} which requires a JavaFX runtime.
* Full GUI startup is tested separately via headless Monocle smoke tests.
*/
class GuiAdapterTest {
@Test
void constructorCreatesInstanceWithoutError() {
GuiAdapter adapter = new GuiAdapter();
assertNotNull(adapter);
}
}
@@ -0,0 +1,22 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* Unit tests for {@link PdfUmbenennerGuiApplication}.
* <p>
* These tests verify that the JavaFX application class can be instantiated
* without triggering the JavaFX runtime. The actual GUI startup (stage
* creation and rendering) is tested via headless Monocle smoke tests
* in a later work package.
*/
class PdfUmbenennerGuiApplicationTest {
@Test
void constructorCreatesInstanceWithoutError() {
PdfUmbenennerGuiApplication app = new PdfUmbenennerGuiApplication();
assertNotNull(app);
}
}