#50: Statuszeile mit Version, Provider und Konfigurationsdateipfad
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+13
@@ -277,6 +277,15 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
*/
|
||||
Consumer<String> titleUpdateListener = title -> { };
|
||||
|
||||
/**
|
||||
* Listener der bei jedem Zustandswechsel des Editor-Zustands aufgerufen wird und
|
||||
* den neuen Zustand an die Statuszeile weiterleitet.
|
||||
* <p>
|
||||
* Package-private, damit {@link PdfUmbenennerGuiApplication} die Statuszeile verdrahten kann.
|
||||
* Standard ist ein No-Op, damit der Workspace auch ohne Statuszeile funktioniert.
|
||||
*/
|
||||
Consumer<GuiConfigurationEditorState> statusBarStateListener = state -> { };
|
||||
|
||||
/**
|
||||
* Per-provider {@link GuiModelFieldContainer} instances, one for each known provider family.
|
||||
* Populated in {@link #createProviderBlock(String, AiProviderFamily)} and registered with
|
||||
@@ -1095,6 +1104,8 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
|
||||
this.editorState = completion.newState();
|
||||
refreshHeader();
|
||||
// Statuszeile nach erfolgreichem Speichern aktualisieren (Konfigurationspfad kann neu sein)
|
||||
statusBarStateListener.accept(this.editorState);
|
||||
|
||||
if (result.hasApiKeyPreservationNote()) {
|
||||
LOG.info("GUI-Editor: API-Key fuer Provider '{}' wurde beibehalten (Feld war leer, "
|
||||
@@ -1190,6 +1201,8 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
pendingMessages.clear();
|
||||
refreshView();
|
||||
runEditorValidation();
|
||||
// Statuszeile über den neuen Zustand informieren
|
||||
statusBarStateListener.accept(newState);
|
||||
}
|
||||
|
||||
private void configureRoot() {
|
||||
|
||||
+13
-6
@@ -44,7 +44,8 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||
* the {@link GuiManualFileCopyPort} used to manually copy a source file to the target
|
||||
* folder for documents that have not yet been successfully processed, and
|
||||
* the {@link GuiHistoricalDocumentContextPort} used to retrieve the historical processing
|
||||
* context for documents that were skipped in the current run.
|
||||
* context for documents that were skipped in the current run, and the resolved application
|
||||
* version string that the status bar displays at the bottom of the main window.
|
||||
* <p>
|
||||
* All ports and services are supplied by Bootstrap so that the GUI adapter does not need to
|
||||
* know about provider-specific HTTP details or adapter wiring.
|
||||
@@ -65,7 +66,8 @@ public record GuiStartupContext(
|
||||
GuiResetDocumentStatusPort resetDocumentStatusPort,
|
||||
GuiManualFileRenamePort manualFileRenamePort,
|
||||
GuiManualFileCopyPort manualFileCopyPort,
|
||||
GuiHistoricalDocumentContextPort historicalDocumentContextPort) {
|
||||
GuiHistoricalDocumentContextPort historicalDocumentContextPort,
|
||||
String applicationVersion) {
|
||||
|
||||
/**
|
||||
* Creates a fully wired startup context.
|
||||
@@ -92,6 +94,8 @@ public record GuiStartupContext(
|
||||
* must not be {@code null}
|
||||
* @param historicalDocumentContextPort bridge that resolves the historical processing context
|
||||
* for skipped documents; must not be {@code null}
|
||||
* @param applicationVersion resolved application version string shown in the status
|
||||
* bar; {@code null} defaults to {@code "dev"}
|
||||
*/
|
||||
public GuiStartupContext {
|
||||
initialState = Objects.requireNonNull(initialState, "initialState must not be null");
|
||||
@@ -124,6 +128,8 @@ public record GuiStartupContext(
|
||||
"manualFileCopyPort must not be null");
|
||||
historicalDocumentContextPort = Objects.requireNonNull(historicalDocumentContextPort,
|
||||
"historicalDocumentContextPort must not be null");
|
||||
// Null-Fallback für Testumgebungen ohne gepacktes JAR
|
||||
applicationVersion = applicationVersion == null ? "dev" : applicationVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,7 +171,7 @@ public record GuiStartupContext(
|
||||
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
|
||||
miniRunLauncher, resetDocumentStatusPort, rejectingManualFileRenamePort(),
|
||||
rejectingManualFileCopyPort(),
|
||||
noOpHistoricalDocumentContextPort());
|
||||
noOpHistoricalDocumentContextPort(), "dev");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,7 +207,7 @@ public record GuiStartupContext(
|
||||
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
|
||||
rejectingMiniRunLauncher(), rejectingResetPort(), rejectingManualFileRenamePort(),
|
||||
rejectingManualFileCopyPort(),
|
||||
noOpHistoricalDocumentContextPort());
|
||||
noOpHistoricalDocumentContextPort(), "dev");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -237,7 +243,7 @@ public record GuiStartupContext(
|
||||
technicalTestOrchestrator, correctionExecutionService,
|
||||
rejectingBatchRunLauncher(), rejectingMiniRunLauncher(), rejectingResetPort(),
|
||||
rejectingManualFileRenamePort(), rejectingManualFileCopyPort(),
|
||||
noOpHistoricalDocumentContextPort());
|
||||
noOpHistoricalDocumentContextPort(), "dev");
|
||||
}
|
||||
|
||||
private static GuiBatchRunLauncher rejectingBatchRunLauncher() {
|
||||
@@ -351,6 +357,7 @@ public record GuiStartupContext(
|
||||
rejectingResetPort(),
|
||||
rejectingManualFileRenamePort(),
|
||||
rejectingManualFileCopyPort(),
|
||||
noOpHistoricalDocumentContextPort());
|
||||
noOpHistoricalDocumentContextPort(),
|
||||
"dev");
|
||||
}
|
||||
}
|
||||
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.AiProviderFamilyStringConverter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiProviderConfigurationState;
|
||||
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
/**
|
||||
* Permanente Statuszeile am unteren Rand des Hauptfensters.
|
||||
* <p>
|
||||
* Die Statuszeile zeigt immer drei Segmente:
|
||||
* <ul>
|
||||
* <li><b>Links:</b> Anwendungsversion im Format {@code V<version>}, z. B. {@code Vdev}.</li>
|
||||
* <li><b>Mitte:</b> Aktiver Provider und Modellname aus der geladenen Konfiguration.</li>
|
||||
* <li><b>Rechts:</b> Pfad der geladenen Konfigurationsdatei.</li>
|
||||
* </ul>
|
||||
* Wenn keine Konfiguration geladen ist, zeigen Mitte und Rechts den Text
|
||||
* {@value #KEIN_PROFIL_TEXT}. Die Versionsanzeige ist stets sichtbar.
|
||||
* <p>
|
||||
* Alle Aktualisierungen dieser Komponente müssen auf dem JavaFX Application Thread erfolgen.
|
||||
* Die Klasse selbst erzwingt dies nicht; der Aufrufer trägt die Verantwortung.
|
||||
*/
|
||||
public final class GuiStatusBar {
|
||||
|
||||
/** Anzeigetext wenn keine Konfiguration geladen ist. */
|
||||
static final String KEIN_PROFIL_TEXT = "Kein Profil geladen";
|
||||
|
||||
/** Präfix vor der Versionsnummer in der linken Statuszeilen-Zelle. */
|
||||
private static final String VERSION_PREFIX = "V";
|
||||
|
||||
private static final AiProviderFamilyStringConverter PROVIDER_CONVERTER =
|
||||
new AiProviderFamilyStringConverter();
|
||||
|
||||
private final String applicationVersion;
|
||||
private final BorderPane root;
|
||||
private final Label versionLabel;
|
||||
private final Label providerLabel;
|
||||
private final Label configPathLabel;
|
||||
|
||||
/**
|
||||
* Erstellt eine neue Statuszeile mit der angegebenen Anwendungsversion.
|
||||
*
|
||||
* @param applicationVersion die aufgelöste Versionsnummer; {@code null} oder leer führt zum
|
||||
* Fallback {@code "dev"}
|
||||
*/
|
||||
public GuiStatusBar(String applicationVersion) {
|
||||
this.applicationVersion = (applicationVersion == null || applicationVersion.isBlank())
|
||||
? "dev"
|
||||
: applicationVersion;
|
||||
|
||||
// Linkes Segment: Versionsanzeige
|
||||
this.versionLabel = new Label(VERSION_PREFIX + this.applicationVersion);
|
||||
this.versionLabel.setStyle("-fx-font-size: 11px; -fx-text-fill: #555555;");
|
||||
|
||||
// Mittleres Segment: Provider und Modell
|
||||
this.providerLabel = new Label(KEIN_PROFIL_TEXT);
|
||||
this.providerLabel.setStyle("-fx-font-size: 11px; -fx-text-fill: #555555;");
|
||||
this.providerLabel.setAlignment(Pos.CENTER);
|
||||
|
||||
// Rechtes Segment: Konfigurationspfad
|
||||
this.configPathLabel = new Label(KEIN_PROFIL_TEXT);
|
||||
this.configPathLabel.setStyle("-fx-font-size: 11px; -fx-text-fill: #555555;");
|
||||
this.configPathLabel.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
// Abstandhalter zwischen den Segmenten
|
||||
Region leftSpacer = new Region();
|
||||
Region rightSpacer = new Region();
|
||||
HBox.setHgrow(leftSpacer, Priority.ALWAYS);
|
||||
HBox.setHgrow(rightSpacer, Priority.ALWAYS);
|
||||
|
||||
HBox content = new HBox(16,
|
||||
versionLabel, leftSpacer,
|
||||
providerLabel, rightSpacer,
|
||||
configPathLabel);
|
||||
content.setAlignment(Pos.CENTER_LEFT);
|
||||
content.setPadding(new Insets(4, 12, 4, 12));
|
||||
content.setStyle("-fx-background-color: #f5f5f5;");
|
||||
|
||||
Separator topSeparator = new Separator();
|
||||
|
||||
this.root = new BorderPane();
|
||||
this.root.setTop(topSeparator);
|
||||
this.root.setCenter(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Wurzelknoten der Statuszeile zurück, der in das Hauptfenster eingebettet wird.
|
||||
*
|
||||
* @return der Wurzelknoten; nie {@code null}
|
||||
*/
|
||||
public BorderPane root() {
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die Statuszeile anhand des aktuellen Editor-Zustands.
|
||||
* <p>
|
||||
* Ist kein Dateisnapshot vorhanden, wird {@link #clearConfiguration()} ausgeführt.
|
||||
* Andernfalls werden Provider, Modell und Konfigurationspfad aus dem Zustand ermittelt
|
||||
* und angezeigt.
|
||||
* <p>
|
||||
* Muss auf dem JavaFX Application Thread aufgerufen werden.
|
||||
*
|
||||
* @param state der aktuelle Editor-Zustand; darf nicht {@code null} sein
|
||||
*/
|
||||
public void applyEditorState(GuiConfigurationEditorState state) {
|
||||
if (state == null || !state.hasLoadedFileSnapshot()) {
|
||||
clearConfiguration();
|
||||
return;
|
||||
}
|
||||
String configPath = state.configurationPathText();
|
||||
String providerText = resolveProviderText(state);
|
||||
providerLabel.setText(providerText);
|
||||
configPathLabel.setText(configPath.isBlank() ? KEIN_PROFIL_TEXT : configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt Mitte und Rechts der Statuszeile auf den Text {@link #KEIN_PROFIL_TEXT} zurück.
|
||||
* <p>
|
||||
* Die Versionsanzeige bleibt unverändert.
|
||||
* <p>
|
||||
* Muss auf dem JavaFX Application Thread aufgerufen werden.
|
||||
*/
|
||||
public void clearConfiguration() {
|
||||
providerLabel.setText(KEIN_PROFIL_TEXT);
|
||||
configPathLabel.setText(KEIN_PROFIL_TEXT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den aktuell angezeigten Versionstext zurück (inkl. Präfix {@code V}).
|
||||
* <p>
|
||||
* Für Tests zugänglich.
|
||||
*
|
||||
* @return der angezeigte Versionstext; nie {@code null}
|
||||
*/
|
||||
String versionText() {
|
||||
return versionLabel.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den aktuell angezeigten Provider-Text zurück.
|
||||
* <p>
|
||||
* Für Tests zugänglich.
|
||||
*
|
||||
* @return der angezeigte Provider-Text; nie {@code null}
|
||||
*/
|
||||
String providerText() {
|
||||
return providerLabel.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den aktuell angezeigten Konfigurationspfad-Text zurück.
|
||||
* <p>
|
||||
* Für Tests zugänglich.
|
||||
*
|
||||
* @return der angezeigte Konfigurationspfad-Text; nie {@code null}
|
||||
*/
|
||||
String configPathText() {
|
||||
return configPathLabel.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt den anzuzeigenden Provider-Text aus dem Editor-Zustand.
|
||||
* <p>
|
||||
* Das Format ist: {@code Provider: <AnzeigeName> · <Modellname>}, wobei der Modellname
|
||||
* weggelassen wird, wenn er leer ist.
|
||||
*
|
||||
* @param state der Editor-Zustand; darf nicht {@code null} sein
|
||||
* @return der formatierte Provider-Text; nie {@code null}
|
||||
*/
|
||||
private static String resolveProviderText(GuiConfigurationEditorState state) {
|
||||
String activeIdentifier = state.values().activeProviderFamily();
|
||||
if (activeIdentifier == null || activeIdentifier.isBlank()) {
|
||||
return KEIN_PROFIL_TEXT;
|
||||
}
|
||||
AiProviderFamily family = AiProviderFamily.fromIdentifier(activeIdentifier).orElse(null);
|
||||
if (family == null) {
|
||||
return KEIN_PROFIL_TEXT;
|
||||
}
|
||||
String displayName = PROVIDER_CONVERTER.toString(family);
|
||||
GuiProviderConfigurationState providerState = state.values().providerConfiguration(family);
|
||||
String model = providerState != null ? providerState.model() : "";
|
||||
if (model == null || model.isBlank()) {
|
||||
return "Provider: " + displayName;
|
||||
}
|
||||
return "Provider: " + displayName + " · " + model;
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -8,6 +8,7 @@ import javafx.application.Platform;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
|
||||
@@ -69,7 +70,16 @@ public class PdfUmbenennerGuiApplication extends Application {
|
||||
// Wire the title-update listener so the stage title stays in sync with the dirty state.
|
||||
workspace.titleUpdateListener = primaryStage::setTitle;
|
||||
|
||||
Scene scene = new Scene(workspace.root(), DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
// Statuszeile anlegen und mit dem Workspace verdrahten
|
||||
GuiStatusBar statusBar = new GuiStatusBar(startupContext.applicationVersion());
|
||||
workspace.statusBarStateListener = statusBar::applyEditorState;
|
||||
|
||||
// Statuszeile unterhalb des Workspace-Inhalts einbetten
|
||||
BorderPane outerLayout = new BorderPane();
|
||||
outerLayout.setCenter(workspace.root());
|
||||
outerLayout.setBottom(statusBar.root());
|
||||
|
||||
Scene scene = new Scene(outerLayout, DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
primaryStage.setTitle(GuiWindowTitleFormatter.format(workspace.editorState()));
|
||||
primaryStage.setScene(scene);
|
||||
|
||||
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationFileSnapshot;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationTemplateFactory;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationValues;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiProviderConfigurationState;
|
||||
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
||||
import javafx.application.Platform;
|
||||
|
||||
/**
|
||||
* Tests für die Statuszeilen-Komponente {@link GuiStatusBar}.
|
||||
* <p>
|
||||
* Überprüft die Versionsanzeige, den Provider-Text und den Konfigurationspfad-Text
|
||||
* in den verschiedenen Zuständen (ohne und mit geladener Konfiguration).
|
||||
* <p>
|
||||
* Die Tests laufen unter Monocle (Headless-JavaFX), da {@link GuiStatusBar} JavaFX-Controls erzeugt.
|
||||
*/
|
||||
class GuiStatusBarTest {
|
||||
|
||||
private static final long FX_TIMEOUT_SECONDS = 10;
|
||||
private static final AtomicBoolean PLATFORM_STARTED = new AtomicBoolean(false);
|
||||
|
||||
/**
|
||||
* Initialisiert die JavaFX-Plattform einmalig für alle Tests dieser Klasse.
|
||||
*
|
||||
* @throws InterruptedException falls der Thread beim Warten unterbrochen wird
|
||||
*/
|
||||
@BeforeAll
|
||||
static void setUpJavaFxPlatform() throws InterruptedException {
|
||||
Platform.setImplicitExit(false);
|
||||
CountDownLatch startLatch = new CountDownLatch(1);
|
||||
try {
|
||||
Platform.startup(() -> {
|
||||
PLATFORM_STARTED.set(true);
|
||||
startLatch.countDown();
|
||||
});
|
||||
} catch (IllegalStateException alreadyInitialized) {
|
||||
// JavaFX wurde bereits durch einen anderen Test gestartet
|
||||
PLATFORM_STARTED.set(true);
|
||||
startLatch.countDown();
|
||||
}
|
||||
assertTrue(
|
||||
startLatch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
|
||||
"JavaFX-Plattform muss innerhalb des Timeouts starten");
|
||||
}
|
||||
|
||||
/** Plattform bleibt für nachfolgende Tests am Leben. */
|
||||
@AfterAll
|
||||
static void tearDownJavaFxPlatform() {
|
||||
// Absichtlich kein Platform.exit() – damit andere Smoke-Tests weiterhin die Plattform nutzen können.
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Versionsanzeige
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Prüft, dass die Versionsanzeige das korrekte Präfix und die übergebene Version enthält.
|
||||
*
|
||||
* @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
|
||||
*/
|
||||
@Test
|
||||
void versionLabel_zeigtVersionMitPraefix() throws Exception {
|
||||
AtomicReference<String> versionText = new AtomicReference<>();
|
||||
runOnFxThread(() -> {
|
||||
GuiStatusBar bar = new GuiStatusBar("3.0.42");
|
||||
versionText.set(bar.versionText());
|
||||
});
|
||||
assertEquals("V3.0.42", versionText.get(),
|
||||
"Die Versionsanzeige muss das Präfix 'V' gefolgt von der Versionsnummer enthalten");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, dass ein {@code null}-Wert für die Version als {@code "dev"} angezeigt wird.
|
||||
*
|
||||
* @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
|
||||
*/
|
||||
@Test
|
||||
void versionLabel_mitNullFaellzurueckAufDev() throws Exception {
|
||||
AtomicReference<String> versionText = new AtomicReference<>();
|
||||
runOnFxThread(() -> {
|
||||
GuiStatusBar bar = new GuiStatusBar(null);
|
||||
versionText.set(bar.versionText());
|
||||
});
|
||||
assertEquals("Vdev", versionText.get(),
|
||||
"Ein null-Wert muss als Fallback 'dev' angezeigt werden");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, dass ein leerer String für die Version als {@code "dev"} angezeigt wird.
|
||||
*
|
||||
* @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
|
||||
*/
|
||||
@Test
|
||||
void versionLabel_mitLeeremStringFaellzurueckAufDev() throws Exception {
|
||||
AtomicReference<String> versionText = new AtomicReference<>();
|
||||
runOnFxThread(() -> {
|
||||
GuiStatusBar bar = new GuiStatusBar(" ");
|
||||
versionText.set(bar.versionText());
|
||||
});
|
||||
assertEquals("Vdev", versionText.get(),
|
||||
"Ein leerer String muss als Fallback 'dev' angezeigt werden");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Standardzustand ohne geladene Konfiguration
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Prüft, dass Mitte und Rechts den Text „Kein Profil geladen" zeigen, wenn keine
|
||||
* Konfiguration geladen ist.
|
||||
*
|
||||
* @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
|
||||
*/
|
||||
@Test
|
||||
void ohneKonfiguration_zeigtKeinProfilGeladen() throws Exception {
|
||||
AtomicReference<String> providerText = new AtomicReference<>();
|
||||
AtomicReference<String> configPathText = new AtomicReference<>();
|
||||
runOnFxThread(() -> {
|
||||
GuiStatusBar bar = new GuiStatusBar("1.0.0");
|
||||
providerText.set(bar.providerText());
|
||||
configPathText.set(bar.configPathText());
|
||||
});
|
||||
assertEquals(GuiStatusBar.KEIN_PROFIL_TEXT, providerText.get(),
|
||||
"Ohne geladene Konfiguration muss 'Kein Profil geladen' als Provider-Text erscheinen");
|
||||
assertEquals(GuiStatusBar.KEIN_PROFIL_TEXT, configPathText.get(),
|
||||
"Ohne geladene Konfiguration muss 'Kein Profil geladen' als Konfigurationspfad erscheinen");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, dass {@link GuiStatusBar#clearConfiguration()} Mitte und Rechts zurücksetzt.
|
||||
*
|
||||
* @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
|
||||
*/
|
||||
@Test
|
||||
void clearConfiguration_setztMitteUndRechtsZurueck() throws Exception {
|
||||
AtomicReference<String> providerText = new AtomicReference<>();
|
||||
AtomicReference<String> configPathText = new AtomicReference<>();
|
||||
runOnFxThread(() -> {
|
||||
GuiStatusBar bar = new GuiStatusBar("1.0.0");
|
||||
// Zustand mit Konfiguration setzen, dann löschen
|
||||
GuiConfigurationEditorState state = buildStateWithConfiguration(
|
||||
"config/application.properties", AiProviderFamily.CLAUDE, "claude-opus-4-7");
|
||||
bar.applyEditorState(state);
|
||||
bar.clearConfiguration();
|
||||
providerText.set(bar.providerText());
|
||||
configPathText.set(bar.configPathText());
|
||||
});
|
||||
assertEquals(GuiStatusBar.KEIN_PROFIL_TEXT, providerText.get(),
|
||||
"Nach clearConfiguration() muss 'Kein Profil geladen' als Provider-Text erscheinen");
|
||||
assertEquals(GuiStatusBar.KEIN_PROFIL_TEXT, configPathText.get(),
|
||||
"Nach clearConfiguration() muss 'Kein Profil geladen' als Konfigurationspfad erscheinen");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Zustand nach Laden einer Konfiguration
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Prüft, dass nach {@link GuiStatusBar#applyEditorState} der korrekte Provider-Text
|
||||
* mit Modell angezeigt wird.
|
||||
*
|
||||
* @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
|
||||
*/
|
||||
@Test
|
||||
void applyEditorState_mitClaudeUndModell_zeigtKorrektesFormat() throws Exception {
|
||||
AtomicReference<String> providerText = new AtomicReference<>();
|
||||
runOnFxThread(() -> {
|
||||
GuiStatusBar bar = new GuiStatusBar("1.0.0");
|
||||
GuiConfigurationEditorState state = buildStateWithConfiguration(
|
||||
"config/application.properties", AiProviderFamily.CLAUDE, "claude-opus-4-7");
|
||||
bar.applyEditorState(state);
|
||||
providerText.set(bar.providerText());
|
||||
});
|
||||
assertEquals("Provider: Claude · claude-opus-4-7", providerText.get(),
|
||||
"Der Provider-Text muss das Format 'Provider: <Name> · <Modell>' haben");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, dass nach {@link GuiStatusBar#applyEditorState} der korrekte Konfigurationspfad
|
||||
* angezeigt wird.
|
||||
*
|
||||
* @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
|
||||
*/
|
||||
@Test
|
||||
void applyEditorState_mitKonfigurationspfad_zeigtKonfiguationspfad() throws Exception {
|
||||
AtomicReference<String> configPathText = new AtomicReference<>();
|
||||
runOnFxThread(() -> {
|
||||
GuiStatusBar bar = new GuiStatusBar("1.0.0");
|
||||
GuiConfigurationEditorState state = buildStateWithConfiguration(
|
||||
"config/application.properties", AiProviderFamily.CLAUDE, "claude-opus-4-7");
|
||||
bar.applyEditorState(state);
|
||||
configPathText.set(bar.configPathText());
|
||||
});
|
||||
assertTrue(configPathText.get().contains("application.properties"),
|
||||
"Der Konfigurationspfad muss den Dateinamen enthalten");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, dass ein OpenAI-kompatibler Provider korrekt angezeigt wird.
|
||||
*
|
||||
* @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
|
||||
*/
|
||||
@Test
|
||||
void applyEditorState_mitOpenAiUndModell_zeigtKorrektesFormat() throws Exception {
|
||||
AtomicReference<String> providerText = new AtomicReference<>();
|
||||
runOnFxThread(() -> {
|
||||
GuiStatusBar bar = new GuiStatusBar("1.0.0");
|
||||
GuiConfigurationEditorState state = buildStateWithConfiguration(
|
||||
"config/application.properties", AiProviderFamily.OPENAI_COMPATIBLE, "gpt-4o");
|
||||
bar.applyEditorState(state);
|
||||
providerText.set(bar.providerText());
|
||||
});
|
||||
assertEquals("Provider: OpenAI-kompatibel · gpt-4o", providerText.get(),
|
||||
"Der Provider-Text muss für OpenAI-kompatibel den deutschen Anzeigenamen verwenden");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, dass beim Übergeben eines {@code null}-Zustands kein Absturz erfolgt und der
|
||||
* Text „Kein Profil geladen" erscheint.
|
||||
*
|
||||
* @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
|
||||
*/
|
||||
@Test
|
||||
void applyEditorState_mitNull_keinAbsturz() throws Exception {
|
||||
AtomicReference<String> providerText = new AtomicReference<>();
|
||||
runOnFxThread(() -> {
|
||||
GuiStatusBar bar = new GuiStatusBar("1.0.0");
|
||||
bar.applyEditorState(null);
|
||||
providerText.set(bar.providerText());
|
||||
});
|
||||
assertEquals(GuiStatusBar.KEIN_PROFIL_TEXT, providerText.get(),
|
||||
"Ein null-Zustand darf keinen Absturz verursachen");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, dass ohne geladenen Dateisnapshot „Kein Profil geladen" angezeigt wird,
|
||||
* auch wenn Konfigurationswerte vorhanden sind.
|
||||
*
|
||||
* @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
|
||||
*/
|
||||
@Test
|
||||
void applyEditorState_ohneSnapshot_zeigtKeinProfilGeladen() throws Exception {
|
||||
AtomicReference<String> providerText = new AtomicReference<>();
|
||||
runOnFxThread(() -> {
|
||||
GuiStatusBar bar = new GuiStatusBar("1.0.0");
|
||||
// Standard-Template hat keinen Snapshot
|
||||
GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createStandardTemplate();
|
||||
bar.applyEditorState(state);
|
||||
providerText.set(bar.providerText());
|
||||
});
|
||||
assertEquals(GuiStatusBar.KEIN_PROFIL_TEXT, providerText.get(),
|
||||
"Ohne geladenen Dateisnapshot muss 'Kein Profil geladen' erscheinen");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, dass der Wurzelknoten der Statuszeile nicht null ist.
|
||||
*
|
||||
* @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
|
||||
*/
|
||||
@Test
|
||||
void root_istNichtNull() throws Exception {
|
||||
AtomicBoolean rootNotNull = new AtomicBoolean(false);
|
||||
runOnFxThread(() -> {
|
||||
GuiStatusBar bar = new GuiStatusBar("1.0.0");
|
||||
rootNotNull.set(bar.root() != null);
|
||||
});
|
||||
assertTrue(rootNotNull.get(), "Der Wurzelknoten der Statuszeile darf nicht null sein");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Hilfsmethoden
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Führt eine Aktion synchron auf dem JavaFX Application Thread aus und wartet auf Abschluss.
|
||||
*
|
||||
* @param action die auszuführende Aktion
|
||||
* @throws Exception falls die Aktion einen Fehler wirft oder das Timeout überschritten wird
|
||||
*/
|
||||
private static void runOnFxThread(Runnable action) throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<Throwable> error = new AtomicReference<>();
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
action.run();
|
||||
} catch (Throwable t) {
|
||||
error.set(t);
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
|
||||
"FX-Thread-Task muss innerhalb des Timeouts abgeschlossen werden");
|
||||
if (error.get() != null) {
|
||||
throw new AssertionError("FX-Thread hat eine Ausnahme geworfen", error.get());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen Editor-Zustand mit geladenem Dateisnapshot für den angegebenen
|
||||
* Konfigurationspfad, Provider und Modell.
|
||||
*
|
||||
* @param configPath relativer Konfigurationsdateipfad
|
||||
* @param family aktive Provider-Familie
|
||||
* @param model Modellbezeichner
|
||||
* @return ein Editor-Zustand mit Snapshot
|
||||
*/
|
||||
private static GuiConfigurationEditorState buildStateWithConfiguration(
|
||||
String configPath, AiProviderFamily family, String model) {
|
||||
GuiConfigurationEditorState template = GuiConfigurationTemplateFactory.createStandardTemplate();
|
||||
// Provider und Modell setzen
|
||||
GuiProviderConfigurationState providerState = new GuiProviderConfigurationState(
|
||||
"https://api.example.com", model, "30",
|
||||
de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiProviderApiKeyState.unresolved());
|
||||
GuiConfigurationValues values = template.values()
|
||||
.withActiveProviderFamily(family.getIdentifier())
|
||||
.withProviderConfiguration(family, providerState);
|
||||
// Snapshot anlegen
|
||||
GuiConfigurationFileSnapshot snapshot = new GuiConfigurationFileSnapshot(
|
||||
Path.of(configPath), new Properties());
|
||||
return new GuiConfigurationEditorState(
|
||||
Optional.of(snapshot), values, values, Optional.empty());
|
||||
}
|
||||
}
|
||||
+9
-4
@@ -806,6 +806,8 @@ public class BootstrapRunner {
|
||||
GuiManualFileRenamePort manualRenamePort = this::performGuiManualFileRename;
|
||||
GuiManualFileCopyPort manualCopyPort = this::performGuiManualFileCopy;
|
||||
GuiHistoricalDocumentContextPort historicalDocumentContextPort = this::resolveHistoricalDocumentContextForGui;
|
||||
// Versionsnummer aus dem MANIFEST.MF des gepackten JARs lesen; Fallback "dev" bei IDE-Start
|
||||
String applicationVersion = ApplicationVersionProvider.resolveVersion();
|
||||
|
||||
if (configPathOverride.isEmpty()) {
|
||||
return new GuiStartupContext(
|
||||
@@ -824,7 +826,8 @@ public class BootstrapRunner {
|
||||
resetPort,
|
||||
manualRenamePort,
|
||||
manualCopyPort,
|
||||
historicalDocumentContextPort);
|
||||
historicalDocumentContextPort,
|
||||
applicationVersion);
|
||||
}
|
||||
|
||||
Path configPath = Paths.get(configPathOverride.get());
|
||||
@@ -848,7 +851,8 @@ public class BootstrapRunner {
|
||||
resetPort,
|
||||
manualRenamePort,
|
||||
manualCopyPort,
|
||||
historicalDocumentContextPort);
|
||||
historicalDocumentContextPort,
|
||||
applicationVersion);
|
||||
}
|
||||
|
||||
LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath());
|
||||
@@ -858,7 +862,7 @@ public class BootstrapRunner {
|
||||
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
|
||||
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
|
||||
miniRunLauncher, resetPort, manualRenamePort, manualCopyPort,
|
||||
historicalDocumentContextPort);
|
||||
historicalDocumentContextPort, applicationVersion);
|
||||
} catch (GuiConfigurationLoadException e) {
|
||||
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
|
||||
e.getMessage(), e);
|
||||
@@ -878,7 +882,8 @@ public class BootstrapRunner {
|
||||
resetPort,
|
||||
manualRenamePort,
|
||||
manualCopyPort,
|
||||
historicalDocumentContextPort);
|
||||
historicalDocumentContextPort,
|
||||
applicationVersion);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user