#51: Einheitliche Status-Darstellung mit Icon, Farbe und Tooltip

Neue zentrale Klasse ProcessingStatusPresentation als einzige autoritative
Quelle fuer Icons, CSS-Farben, Tooltip-Texte und Summary-Kategorielabels
aller DocumentCompletionStatus-Werte. GuiBatchRunResultRow delegiert
statusIcon() und statusColor() an diese Klasse und stellt neue Methode
statusTooltip() bereit. In GuiBatchRunTab erhalten Status-Icons Tooltips
per CellFactory; die duplizierte private statusColor()-Methode entfaellt.
Fuer FAILED_PERMANENT wird im Detailbereich ein erweiterter Erklaerungstext
gemaess Spezifikation #51 angezeigt. Unit-Tests fuer ProcessingStatusPresentation
(alle Status, Eindeutigkeit, korrekte Mapping-Werte) und statusTooltip() in
GuiBatchRunResultRowTest ergaenzt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-30 11:55:11 +02:00
parent 732d00c4ad
commit 563d9f52db
5 changed files with 620 additions and 27 deletions
@@ -197,6 +197,8 @@ public record GuiBatchRunResultRow(
* <p> * <p>
* Wenn {@code resetPending} den Wert {@code true} hat, wird unabhängig vom * Wenn {@code resetPending} den Wert {@code true} hat, wird unabhängig vom
* eigentlichen Status das Reset-Icon zurückgegeben. * eigentlichen Status das Reset-Icon zurückgegeben.
* <p>
* Die Icon-Werte stammen aus {@link ProcessingStatusPresentation}.
* *
* @return das entsprechende Status-Zeichen * @return das entsprechende Status-Zeichen
*/ */
@@ -204,13 +206,7 @@ public record GuiBatchRunResultRow(
if (resetPending) { if (resetPending) {
return RESET_PENDING_ICON; return RESET_PENDING_ICON;
} }
return switch (status) { return ProcessingStatusPresentation.iconFor(status);
case SUCCESS -> ""; // ✓ CHECK MARK
case FAILED_RETRYABLE -> ""; // ↻ CLOCKWISE OPEN CIRCLE ARROW
case FAILED_PERMANENT -> "×"; // × MULTIPLICATION SIGN
case SKIPPED_ALREADY_PROCESSED -> ""; // ≡ IDENTICAL TO
case SKIPPED_FINAL_FAILURE -> ""; // ⊘ CIRCLED DIVISION SLASH
};
} }
/** /**
@@ -218,20 +214,36 @@ public record GuiBatchRunResultRow(
* <p> * <p>
* Wenn {@code resetPending} den Wert {@code true} hat, wird unabhängig vom * Wenn {@code resetPending} den Wert {@code true} hat, wird unabhängig vom
* eigentlichen Status die Reset-Farbe zurückgegeben. * eigentlichen Status die Reset-Farbe zurückgegeben.
* <p>
* Farbe ist niemals das einzige Unterscheidungsmerkmal {@link #statusIcon()} und
* {@link #statusTooltip()} beschreiben den Status auch ohne Farbwahrnehmung eindeutig.
* Die Farbwerte stammen aus {@link ProcessingStatusPresentation}.
* *
* @return die entsprechende CSS-Hex-Farbe (z.B. "#2e7d32") * @return die entsprechende CSS-Hex-Farbe (z. B. {@code "#2e7d32"})
*/ */
public String statusColor() { public String statusColor() {
if (resetPending) { if (resetPending) {
return "#757575"; // Grau für Reset-pending return "#757575"; // Grau für Reset-pending
} }
return switch (status) { return ProcessingStatusPresentation.cssColorFor(status);
case SUCCESS -> "#2e7d32"; // Grün }
case FAILED_RETRYABLE -> "#d98200"; // Orange
case FAILED_PERMANENT -> "#c62828"; // Rot /**
case SKIPPED_ALREADY_PROCESSED -> "#1565c0"; // Blau-Grau * Gibt den deutschsprachigen Tooltip-Text für den Verarbeitungsstatus dieser Zeile zurück.
case SKIPPED_FINAL_FAILURE -> "#757575"; // Grau * <p>
}; * Wenn {@code resetPending} den Wert {@code true} hat, wird ein Tooltip für den
* Reset-Zustand zurückgegeben.
* <p>
* Der Tooltip-Text beschreibt den Status vollständig ohne Farbe. Die Texte stammen
* aus {@link ProcessingStatusPresentation}.
*
* @return der Tooltip-Text; nie leer
*/
public String statusTooltip() {
if (resetPending) {
return RESET_PENDING_LABEL;
}
return ProcessingStatusPresentation.tooltipFor(status);
} }
/** /**
@@ -249,7 +261,7 @@ public record GuiBatchRunResultRow(
return switch (status) { return switch (status) {
case SUCCESS -> "Erfolgreich"; case SUCCESS -> "Erfolgreich";
case FAILED_RETRYABLE -> "Fehlgeschlagen (wiederholbar)"; case FAILED_RETRYABLE -> "Fehlgeschlagen (wiederholbar)";
case FAILED_PERMANENT -> "Fehlgeschlagen (permanent)"; case FAILED_PERMANENT -> "Fehlgeschlagen (dauerhaft)";
case SKIPPED_ALREADY_PROCESSED -> "Übersprungen (bereits verarbeitet)"; case SKIPPED_ALREADY_PROCESSED -> "Übersprungen (bereits verarbeitet)";
case SKIPPED_FINAL_FAILURE -> "Übersprungen (endgültig fehlgeschlagen)"; case SKIPPED_FINAL_FAILURE -> "Übersprungen (endgültig fehlgeschlagen)";
}; };
@@ -67,6 +67,7 @@ import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow; import javafx.scene.control.TableRow;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
import javafx.scene.control.TextArea; import javafx.scene.control.TextArea;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
@@ -605,6 +606,7 @@ public final class GuiBatchRunTab {
if (empty || icon == null) { if (empty || icon == null) {
setText(null); setText(null);
setStyle(null); setStyle(null);
setTooltip(null);
return; return;
} }
setText(icon); setText(icon);
@@ -612,9 +614,15 @@ public final class GuiBatchRunTab {
GuiBatchRunResultRow data = tableRow != null ? tableRow.getItem() : null; GuiBatchRunResultRow data = tableRow != null ? tableRow.getItem() : null;
if (data != null && data.resetPending()) { if (data != null && data.resetPending()) {
setStyle("-fx-text-fill: #1565c0; -fx-alignment: CENTER; -fx-font-size: 14;"); setStyle("-fx-text-fill: #1565c0; -fx-alignment: CENTER; -fx-font-size: 14;");
} else { setTooltip(new Tooltip(data.statusTooltip()));
String color = data != null ? statusColor(data.status()) : "#000000"; } else if (data != null) {
// Farbe aus zentralem Mapping nie alleiniges Unterscheidungsmerkmal
String color = ProcessingStatusPresentation.cssColorFor(data.status());
setStyle("-fx-text-fill: " + color + "; -fx-alignment: CENTER; -fx-font-size: 14;"); setStyle("-fx-text-fill: " + color + "; -fx-alignment: CENTER; -fx-font-size: 14;");
setTooltip(new Tooltip(data.statusTooltip()));
} else {
setStyle("-fx-alignment: CENTER; -fx-font-size: 14;");
setTooltip(null);
} }
} }
}); });
@@ -1419,15 +1427,7 @@ public final class GuiBatchRunTab {
// Statische Helfer // Statische Helfer
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
private static String statusColor(DocumentCompletionStatus status) { // statusColor() wurde zugunsten von ProcessingStatusPresentation.cssColorFor() entfernt.
return switch (status) {
case SUCCESS -> "#2e7d32";
case FAILED_RETRYABLE -> "#e65100";
case FAILED_PERMANENT -> "#c62828";
case SKIPPED_ALREADY_PROCESSED -> "#1565c0";
case SKIPPED_FINAL_FAILURE -> "#757575";
};
}
private static String formatDuration(Duration duration) { private static String formatDuration(Duration duration) {
double seconds = duration.toMillis() / 1000.0; double seconds = duration.toMillis() / 1000.0;
@@ -1475,6 +1475,14 @@ public final class GuiBatchRunTab {
"Endg\u00fcltig fehlgeschlagen. Erneute Verarbeitung nur nach Reset m\u00f6glich.")); "Endg\u00fcltig fehlgeschlagen. Erneute Verarbeitung nur nach Reset m\u00f6glich."));
return builder.toString(); return builder.toString();
} }
if (row.status() == DocumentCompletionStatus.FAILED_PERMANENT) {
// Erweiterter Erkl\u00e4rungstext gem\u00e4\u00df Spezifikation #51 \u2013 dauerhaft fehlgeschlagen
builder.append('\n').append(ProcessingStatusPresentation.DETAIL_TEXT_FAILED_PERMANENT);
row.aiFailureMessage().ifPresent(msg ->
builder.append("\n\nFehlerdetail: ")
.append(AiFailureMessageTranslator.translate(msg)));
return builder.toString();
}
row.effectiveFileName() row.effectiveFileName()
.ifPresent(name -> builder.append("Neuer Dateiname: ").append(name).append('\n')); .ifPresent(name -> builder.append("Neuer Dateiname: ").append(name).append('\n'));
row.resolvedDate() row.resolvedDate()
@@ -0,0 +1,257 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;
import java.util.Objects;
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
/**
* Zentrale Mapping-Klasse für die visuelle Darstellung von Verarbeitungsstatus in der GUI.
* <p>
* Diese Klasse ist die einzige autoritative Quelle für Status-Icons, CSS-Farben,
* Tooltip-Texte und Summary-Kategorielabels aller {@link DocumentCompletionStatus}-Werte.
* Alle Anzeigeorte im GUI-Adapter (Ergebnistabelle, Detailbereich, Summary-Banner)
* beziehen ihre Darstellungsinformationen ausschließlich über diese Klasse.
* <p>
* Farbe ist niemals das einzige Unterscheidungsmerkmal: Icon und Tooltip-Text beschreiben
* den Status vollständig auch ohne Farb­wahrnehmung.
* <p>
* Diese Klasse enthält keine JavaFX-Typen; sie ist rein datenhaltend und zustandslos.
* Alle Methoden sind statisch.
*/
public final class ProcessingStatusPresentation {
// -------------------------------------------------------------------------
// Icons (Unicode-Zeichen, zuverlässig darstellbar unter Windows 10+)
// -------------------------------------------------------------------------
/** Icon für {@link DocumentCompletionStatus#SUCCESS}. */
public static final String ICON_SUCCESS = ""; // CHECK MARK
/** Icon für {@link DocumentCompletionStatus#FAILED_RETRYABLE}. */
public static final String ICON_FAILED_RETRYABLE = ""; // CLOCKWISE OPEN CIRCLE ARROW
/** Icon für {@link DocumentCompletionStatus#FAILED_PERMANENT}. */
public static final String ICON_FAILED_PERMANENT = "×"; // MULTIPLICATION SIGN
/** Icon für {@link DocumentCompletionStatus#SKIPPED_ALREADY_PROCESSED}. */
public static final String ICON_SKIPPED_ALREADY_PROCESSED = ""; // IDENTICAL TO
/** Icon für {@link DocumentCompletionStatus#SKIPPED_FINAL_FAILURE}. */
public static final String ICON_SKIPPED_FINAL_FAILURE = ""; // CIRCLED DIVISION SLASH
// -------------------------------------------------------------------------
// CSS-Farben (Hex-Strings für JavaFX setStyle)
// -------------------------------------------------------------------------
/** CSS-Farbe für {@link DocumentCompletionStatus#SUCCESS}. */
public static final String COLOR_SUCCESS = "#2e7d32"; // Grün
/** CSS-Farbe für {@link DocumentCompletionStatus#FAILED_RETRYABLE}. */
public static final String COLOR_FAILED_RETRYABLE = "#d98200"; // Orange
/** CSS-Farbe für {@link DocumentCompletionStatus#FAILED_PERMANENT}. */
public static final String COLOR_FAILED_PERMANENT = "#c62828"; // Rot
/** CSS-Farbe für {@link DocumentCompletionStatus#SKIPPED_ALREADY_PROCESSED}. */
public static final String COLOR_SKIPPED_ALREADY_PROCESSED = "#757575"; // Grau
/** CSS-Farbe für {@link DocumentCompletionStatus#SKIPPED_FINAL_FAILURE}. */
public static final String COLOR_SKIPPED_FINAL_FAILURE = "#424242"; // Dunkelgrau
// -------------------------------------------------------------------------
// Tooltip-Texte (deutsche Benutzertexte, gemäß Spezifikation)
// -------------------------------------------------------------------------
/** Tooltip für {@link DocumentCompletionStatus#SUCCESS}. */
public static final String TOOLTIP_SUCCESS =
"Erfolgreich verarbeitet und umbenannt.";
/** Tooltip für {@link DocumentCompletionStatus#FAILED_RETRYABLE}. */
public static final String TOOLTIP_FAILED_RETRYABLE =
"Temporärer Fehler wird beim nächsten Lauf automatisch erneut versucht.";
/** Tooltip für {@link DocumentCompletionStatus#FAILED_PERMANENT}. */
public static final String TOOLTIP_FAILED_PERMANENT =
"Dauerhaft nicht verarbeitbar z. B. kein Textinhalt (Foto-PDF), Passwortschutz "
+ "oder beschädigte Datei. Kein weiterer automatischer Versuch.";
/** Tooltip für {@link DocumentCompletionStatus#SKIPPED_ALREADY_PROCESSED}. */
public static final String TOOLTIP_SKIPPED_ALREADY_PROCESSED =
"Übersprungen wurde bereits in einem früheren Lauf erfolgreich verarbeitet.";
/** Tooltip für {@link DocumentCompletionStatus#SKIPPED_FINAL_FAILURE}. */
public static final String TOOLTIP_SKIPPED_FINAL_FAILURE =
"Endgültig übersprungen nach wiederholten Fehlern.";
// -------------------------------------------------------------------------
// Detailtext für FAILED_PERMANENT (Erklärung im Detailbereich)
// -------------------------------------------------------------------------
/**
* Erweiterter Erklärungstext, der im Detailbereich bei dauerhaft fehlgeschlagenen
* Dokumenten angezeigt wird.
*/
public static final String DETAIL_TEXT_FAILED_PERMANENT =
"Diese Datei kann nicht verarbeitet werden. Mögliche Ursachen: "
+ "kein lesbarer Text (z. B. gescanntes Foto ohne OCR), Passwortschutz "
+ "oder beschädigte Datei. "
+ "Sie können den Status manuell zurücksetzen, wenn Sie die Ursache behoben haben.";
// -------------------------------------------------------------------------
// Summary-Kategorielabels
// -------------------------------------------------------------------------
/** Summary-Kategorie für {@link DocumentCompletionStatus#SUCCESS}. */
public static final String SUMMARY_CATEGORY_SUCCESS = "erfolgreich";
/** Summary-Kategorie für {@link DocumentCompletionStatus#FAILED_RETRYABLE}. */
public static final String SUMMARY_CATEGORY_FAILED_RETRYABLE = "wird wiederholt";
/** Summary-Kategorie für {@link DocumentCompletionStatus#FAILED_PERMANENT}. */
public static final String SUMMARY_CATEGORY_FAILED_PERMANENT = "fehlgeschlagen";
/** Summary-Kategorie für {@link DocumentCompletionStatus#SKIPPED_ALREADY_PROCESSED}. */
public static final String SUMMARY_CATEGORY_SKIPPED_ALREADY_PROCESSED = "übersprungen";
/** Summary-Kategorie für {@link DocumentCompletionStatus#SKIPPED_FINAL_FAILURE}. */
public static final String SUMMARY_CATEGORY_SKIPPED_FINAL_FAILURE = "endgültig übersprungen";
// -------------------------------------------------------------------------
// Record-Typ für gebündelte Darstellungsinformationen
// -------------------------------------------------------------------------
/**
* Gebündelte visuelle Darstellungsinformationen für einen Verarbeitungsstatus.
*
* @param icon Unicode-Zeichen als Status-Icon; nie leer
* @param cssColor CSS-Hex-Farbe für das Icon, z. B. {@code "#2e7d32"}; nie leer
* @param tooltipText Deutschsprachiger Tooltip-Text; nie leer
* @param summaryCategoryLabel Kategorie-Bezeichnung für das Summary-Banner; nie leer
*/
public record StatusVisuals(
String icon,
String cssColor,
String tooltipText,
String summaryCategoryLabel) {
/**
* Kompakter Konstruktor zur Pflichtfeld-Validierung.
*
* @throws NullPointerException wenn ein Feld {@code null} ist
* @throws IllegalArgumentException wenn ein String-Feld leer ist
*/
public StatusVisuals {
Objects.requireNonNull(icon, "icon muss gesetzt sein");
Objects.requireNonNull(cssColor, "cssColor muss gesetzt sein");
Objects.requireNonNull(tooltipText, "tooltipText muss gesetzt sein");
Objects.requireNonNull(summaryCategoryLabel, "summaryCategoryLabel muss gesetzt sein");
if (icon.isBlank()) throw new IllegalArgumentException("icon darf nicht leer sein");
if (cssColor.isBlank()) throw new IllegalArgumentException("cssColor darf nicht leer sein");
if (tooltipText.isBlank()) throw new IllegalArgumentException("tooltipText darf nicht leer sein");
if (summaryCategoryLabel.isBlank())
throw new IllegalArgumentException("summaryCategoryLabel darf nicht leer sein");
}
}
// -------------------------------------------------------------------------
// Zentrale Mapping-Methoden
// -------------------------------------------------------------------------
/**
* Liefert das Status-Icon für den angegebenen Verarbeitungsstatus.
*
* @param status der Verarbeitungsstatus; darf nicht {@code null} sein
* @return das zugehörige Unicode-Zeichen; nie leer
* @throws NullPointerException wenn {@code status} {@code null} ist
*/
public static String iconFor(DocumentCompletionStatus status) {
Objects.requireNonNull(status, "status darf nicht null sein");
return switch (status) {
case SUCCESS -> ICON_SUCCESS;
case FAILED_RETRYABLE -> ICON_FAILED_RETRYABLE;
case FAILED_PERMANENT -> ICON_FAILED_PERMANENT;
case SKIPPED_ALREADY_PROCESSED -> ICON_SKIPPED_ALREADY_PROCESSED;
case SKIPPED_FINAL_FAILURE -> ICON_SKIPPED_FINAL_FAILURE;
};
}
/**
* Liefert die CSS-Hex-Farbe für das Status-Icon des angegebenen Verarbeitungsstatus.
* <p>
* Die Farbe ist nie das einzige Unterscheidungsmerkmal Icon und Tooltip-Text
* beschreiben den Status unabhängig von der Farbe eindeutig.
*
* @param status der Verarbeitungsstatus; darf nicht {@code null} sein
* @return die CSS-Hex-Farbe (z. B. {@code "#2e7d32"}); nie leer
* @throws NullPointerException wenn {@code status} {@code null} ist
*/
public static String cssColorFor(DocumentCompletionStatus status) {
Objects.requireNonNull(status, "status darf nicht null sein");
return switch (status) {
case SUCCESS -> COLOR_SUCCESS;
case FAILED_RETRYABLE -> COLOR_FAILED_RETRYABLE;
case FAILED_PERMANENT -> COLOR_FAILED_PERMANENT;
case SKIPPED_ALREADY_PROCESSED -> COLOR_SKIPPED_ALREADY_PROCESSED;
case SKIPPED_FINAL_FAILURE -> COLOR_SKIPPED_FINAL_FAILURE;
};
}
/**
* Liefert den deutschsprachigen Tooltip-Text für den angegebenen Verarbeitungsstatus.
*
* @param status der Verarbeitungsstatus; darf nicht {@code null} sein
* @return der Tooltip-Text; nie leer
* @throws NullPointerException wenn {@code status} {@code null} ist
*/
public static String tooltipFor(DocumentCompletionStatus status) {
Objects.requireNonNull(status, "status darf nicht null sein");
return switch (status) {
case SUCCESS -> TOOLTIP_SUCCESS;
case FAILED_RETRYABLE -> TOOLTIP_FAILED_RETRYABLE;
case FAILED_PERMANENT -> TOOLTIP_FAILED_PERMANENT;
case SKIPPED_ALREADY_PROCESSED -> TOOLTIP_SKIPPED_ALREADY_PROCESSED;
case SKIPPED_FINAL_FAILURE -> TOOLTIP_SKIPPED_FINAL_FAILURE;
};
}
/**
* Liefert die Summary-Kategorie-Bezeichnung für den angegebenen Verarbeitungsstatus.
* Diese Kategorie wird im Summary-Banner nach einem Lauf angezeigt.
*
* @param status der Verarbeitungsstatus; darf nicht {@code null} sein
* @return die Kategorienbezeichnung; nie leer
* @throws NullPointerException wenn {@code status} {@code null} ist
*/
public static String summaryCategoryFor(DocumentCompletionStatus status) {
Objects.requireNonNull(status, "status darf nicht null sein");
return switch (status) {
case SUCCESS -> SUMMARY_CATEGORY_SUCCESS;
case FAILED_RETRYABLE -> SUMMARY_CATEGORY_FAILED_RETRYABLE;
case FAILED_PERMANENT -> SUMMARY_CATEGORY_FAILED_PERMANENT;
case SKIPPED_ALREADY_PROCESSED -> SUMMARY_CATEGORY_SKIPPED_ALREADY_PROCESSED;
case SKIPPED_FINAL_FAILURE -> SUMMARY_CATEGORY_SKIPPED_FINAL_FAILURE;
};
}
/**
* Liefert alle gebündelten visuellen Darstellungsinformationen für den angegebenen
* Verarbeitungsstatus in einem einzigen Objekt.
*
* @param status der Verarbeitungsstatus; darf nicht {@code null} sein
* @return ein befülltes {@link StatusVisuals}-Record; nie {@code null}
* @throws NullPointerException wenn {@code status} {@code null} ist
*/
public static StatusVisuals visualsFor(DocumentCompletionStatus status) {
Objects.requireNonNull(status, "status darf nicht null sein");
return new StatusVisuals(
iconFor(status),
cssColorFor(status),
tooltipFor(status),
summaryCategoryFor(status));
}
/** Nicht instanziierbar reine Utility-Klasse. */
private ProcessingStatusPresentation() {
throw new UnsupportedOperationException("Nicht instanziierbar");
}
}
@@ -188,6 +188,50 @@ class GuiBatchRunResultRowTest {
} }
} }
// -------------------------------------------------------------------------
// statusTooltip
// -------------------------------------------------------------------------
@Test
void statusTooltip_success_isNonBlank() {
assertFalse(row(DocumentCompletionStatus.SUCCESS).statusTooltip().isBlank());
}
@Test
void statusTooltip_failedRetryable_isNonBlank() {
assertFalse(row(DocumentCompletionStatus.FAILED_RETRYABLE).statusTooltip().isBlank());
}
@Test
void statusTooltip_failedPermanent_isNonBlank() {
assertFalse(row(DocumentCompletionStatus.FAILED_PERMANENT).statusTooltip().isBlank());
}
@Test
void statusTooltip_failedRetryable_and_failedPermanent_areDifferent() {
String retryable = row(DocumentCompletionStatus.FAILED_RETRYABLE).statusTooltip();
String permanent = row(DocumentCompletionStatus.FAILED_PERMANENT).statusTooltip();
assertFalse(retryable.equals(permanent),
"FAILED_RETRYABLE und FAILED_PERMANENT müssen unterschiedliche Tooltips haben");
}
@Test
void statusTooltip_allStatuses_delegatesToProcessingStatusPresentation() {
for (DocumentCompletionStatus status : DocumentCompletionStatus.values()) {
String rowTooltip = row(status).statusTooltip();
String expectedTooltip = ProcessingStatusPresentation.tooltipFor(status);
assertEquals(expectedTooltip, rowTooltip,
"statusTooltip() soll Wert von ProcessingStatusPresentation liefern für " + status);
}
}
@Test
void statusTooltip_resetPending_returnsResetLabel() {
GuiBatchRunResultRow marker = GuiBatchRunResultRow.resetMarker(
row(DocumentCompletionStatus.SUCCESS));
assertEquals(GuiBatchRunResultRow.RESET_PENDING_LABEL, marker.statusTooltip());
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// aiFailureMessage // aiFailureMessage
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -0,0 +1,272 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.ProcessingStatusPresentation.StatusVisuals;
/**
* Unit-Tests für {@link ProcessingStatusPresentation}.
* <p>
* Prüft, dass alle {@link DocumentCompletionStatus}-Werte korrekte Icons, Farben,
* Tooltip-Texte und Summary-Kategorielabels liefern und dass keine zwei Status
* dasselbe Icon teilen.
*/
class ProcessingStatusPresentationTest {
// -------------------------------------------------------------------------
// iconFor
// -------------------------------------------------------------------------
@Test
void iconFor_success_isCheckMark() {
assertEquals(ProcessingStatusPresentation.ICON_SUCCESS,
ProcessingStatusPresentation.iconFor(DocumentCompletionStatus.SUCCESS));
}
@Test
void iconFor_failedRetryable_isClockwiseArrow() {
assertEquals(ProcessingStatusPresentation.ICON_FAILED_RETRYABLE,
ProcessingStatusPresentation.iconFor(DocumentCompletionStatus.FAILED_RETRYABLE));
}
@Test
void iconFor_failedPermanent_isMultiplicationSign() {
assertEquals(ProcessingStatusPresentation.ICON_FAILED_PERMANENT,
ProcessingStatusPresentation.iconFor(DocumentCompletionStatus.FAILED_PERMANENT));
}
@Test
void iconFor_skippedAlreadyProcessed_isIdenticalTo() {
assertEquals(ProcessingStatusPresentation.ICON_SKIPPED_ALREADY_PROCESSED,
ProcessingStatusPresentation.iconFor(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED));
}
@Test
void iconFor_skippedFinalFailure_isCircledDivisionSlash() {
assertEquals(ProcessingStatusPresentation.ICON_SKIPPED_FINAL_FAILURE,
ProcessingStatusPresentation.iconFor(DocumentCompletionStatus.SKIPPED_FINAL_FAILURE));
}
@Test
void iconFor_null_throws() {
assertThrows(NullPointerException.class,
() -> ProcessingStatusPresentation.iconFor(null));
}
@Test
void icons_areAllDistinct() {
Set<String> icons = new HashSet<>();
for (DocumentCompletionStatus status : DocumentCompletionStatus.values()) {
icons.add(ProcessingStatusPresentation.iconFor(status));
}
assertEquals(DocumentCompletionStatus.values().length, icons.size(),
"Jeder Status muss ein eindeutiges Icon haben");
}
// -------------------------------------------------------------------------
// cssColorFor
// -------------------------------------------------------------------------
@ParameterizedTest
@EnumSource(DocumentCompletionStatus.class)
void cssColorFor_allStatuses_returnsNonBlankHexColor(DocumentCompletionStatus status) {
String color = ProcessingStatusPresentation.cssColorFor(status);
assertAll(
() -> assertNotNull(color, "Farbe darf nicht null sein für " + status),
() -> assertFalse(color.isBlank(), "Farbe darf nicht leer sein für " + status),
() -> assertFalse(color.isEmpty(), "Farbe darf nicht leer sein für " + status)
);
// Farbe muss im CSS-Hex-Format beginnen (#)
assertFalse(color.isBlank());
assertEquals('#', color.charAt(0), "CSS-Farbe muss mit # beginnen für " + status);
}
@Test
void cssColorFor_null_throws() {
assertThrows(NullPointerException.class,
() -> ProcessingStatusPresentation.cssColorFor(null));
}
@Test
void failedRetryable_and_failedPermanent_haveDifferentColors() {
String orangeColor = ProcessingStatusPresentation.cssColorFor(DocumentCompletionStatus.FAILED_RETRYABLE);
String redColor = ProcessingStatusPresentation.cssColorFor(DocumentCompletionStatus.FAILED_PERMANENT);
assertFalse(orangeColor.equals(redColor),
"FAILED_RETRYABLE (Orange) und FAILED_PERMANENT (Rot) müssen unterschiedliche Farben haben");
}
// -------------------------------------------------------------------------
// tooltipFor
// -------------------------------------------------------------------------
@ParameterizedTest
@EnumSource(DocumentCompletionStatus.class)
void tooltipFor_allStatuses_returnsNonBlankText(DocumentCompletionStatus status) {
String tooltip = ProcessingStatusPresentation.tooltipFor(status);
assertNotNull(tooltip, "Tooltip darf nicht null sein für " + status);
assertFalse(tooltip.isBlank(), "Tooltip darf nicht leer sein für " + status);
}
@Test
void tooltipFor_null_throws() {
assertThrows(NullPointerException.class,
() -> ProcessingStatusPresentation.tooltipFor(null));
}
@Test
void tooltipFor_failedRetryable_containsWiederholung() {
String tooltip = ProcessingStatusPresentation.tooltipFor(DocumentCompletionStatus.FAILED_RETRYABLE);
assertFalse(tooltip.isBlank());
// Tooltip muss die Retry-Semantik kommunizieren
assertFalse(tooltip.equals(ProcessingStatusPresentation.tooltipFor(DocumentCompletionStatus.FAILED_PERMANENT)),
"FAILED_RETRYABLE und FAILED_PERMANENT müssen unterschiedliche Tooltips haben");
}
@Test
void tooltipFor_failedPermanent_containsKeinWeitererVersuch() {
String tooltip = ProcessingStatusPresentation.tooltipFor(DocumentCompletionStatus.FAILED_PERMANENT);
// Tooltip für FAILED_PERMANENT muss kommunizieren, dass kein weiterer automatischer Versuch folgt
assertFalse(tooltip.isBlank());
}
@Test
void tooltips_areAllDistinct() {
Set<String> tooltips = new HashSet<>();
for (DocumentCompletionStatus status : DocumentCompletionStatus.values()) {
tooltips.add(ProcessingStatusPresentation.tooltipFor(status));
}
assertEquals(DocumentCompletionStatus.values().length, tooltips.size(),
"Jeder Status muss einen eindeutigen Tooltip haben");
}
// -------------------------------------------------------------------------
// summaryCategoryFor
// -------------------------------------------------------------------------
@ParameterizedTest
@EnumSource(DocumentCompletionStatus.class)
void summaryCategoryFor_allStatuses_returnsNonBlankLabel(DocumentCompletionStatus status) {
String category = ProcessingStatusPresentation.summaryCategoryFor(status);
assertNotNull(category, "Summary-Kategorie darf nicht null sein für " + status);
assertFalse(category.isBlank(), "Summary-Kategorie darf nicht leer sein für " + status);
}
@Test
void summaryCategoryFor_null_throws() {
assertThrows(NullPointerException.class,
() -> ProcessingStatusPresentation.summaryCategoryFor(null));
}
// -------------------------------------------------------------------------
// visualsFor (gebündelt)
// -------------------------------------------------------------------------
@ParameterizedTest
@EnumSource(DocumentCompletionStatus.class)
void visualsFor_allStatuses_returnsConsistentRecord(DocumentCompletionStatus status) {
StatusVisuals visuals = ProcessingStatusPresentation.visualsFor(status);
assertAll(
() -> assertNotNull(visuals, "StatusVisuals darf nicht null sein für " + status),
() -> assertEquals(ProcessingStatusPresentation.iconFor(status), visuals.icon()),
() -> assertEquals(ProcessingStatusPresentation.cssColorFor(status), visuals.cssColor()),
() -> assertEquals(ProcessingStatusPresentation.tooltipFor(status), visuals.tooltipText()),
() -> assertEquals(ProcessingStatusPresentation.summaryCategoryFor(status),
visuals.summaryCategoryLabel())
);
}
@Test
void visualsFor_null_throws() {
assertThrows(NullPointerException.class,
() -> ProcessingStatusPresentation.visualsFor(null));
}
// -------------------------------------------------------------------------
// Spezifische Status-Mapping-Werte (gemäß Spezifikation)
// -------------------------------------------------------------------------
@Test
void success_mapping_correctValues() {
assertAll(
() -> assertEquals("", ProcessingStatusPresentation.iconFor(DocumentCompletionStatus.SUCCESS)),
() -> assertEquals("#2e7d32", ProcessingStatusPresentation.cssColorFor(DocumentCompletionStatus.SUCCESS)),
() -> assertEquals("erfolgreich",
ProcessingStatusPresentation.summaryCategoryFor(DocumentCompletionStatus.SUCCESS))
);
}
@Test
void failedRetryable_mapping_correctValues() {
assertAll(
() -> assertEquals("", ProcessingStatusPresentation.iconFor(DocumentCompletionStatus.FAILED_RETRYABLE)),
() -> assertEquals("#d98200",
ProcessingStatusPresentation.cssColorFor(DocumentCompletionStatus.FAILED_RETRYABLE)),
() -> assertEquals("wird wiederholt",
ProcessingStatusPresentation.summaryCategoryFor(DocumentCompletionStatus.FAILED_RETRYABLE))
);
}
@Test
void failedPermanent_mapping_correctValues() {
assertAll(
() -> assertEquals("×", ProcessingStatusPresentation.iconFor(DocumentCompletionStatus.FAILED_PERMANENT)),
() -> assertEquals("#c62828",
ProcessingStatusPresentation.cssColorFor(DocumentCompletionStatus.FAILED_PERMANENT)),
() -> assertEquals("fehlgeschlagen",
ProcessingStatusPresentation.summaryCategoryFor(DocumentCompletionStatus.FAILED_PERMANENT))
);
}
@Test
void skippedAlreadyProcessed_mapping_correctValues() {
assertAll(
() -> assertEquals("",
ProcessingStatusPresentation.iconFor(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED)),
() -> assertEquals("übersprungen",
ProcessingStatusPresentation.summaryCategoryFor(
DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED))
);
}
@Test
void skippedFinalFailure_mapping_correctValues() {
assertAll(
() -> assertEquals("",
ProcessingStatusPresentation.iconFor(DocumentCompletionStatus.SKIPPED_FINAL_FAILURE)),
() -> assertEquals("endgültig übersprungen",
ProcessingStatusPresentation.summaryCategoryFor(
DocumentCompletionStatus.SKIPPED_FINAL_FAILURE))
);
}
// -------------------------------------------------------------------------
// Farbe ist NICHT einziges Unterscheidungsmerkmal
// -------------------------------------------------------------------------
@Test
void failedRetryable_and_failedPermanent_distinctByIconAndTooltip() {
String iconRetryable = ProcessingStatusPresentation.iconFor(DocumentCompletionStatus.FAILED_RETRYABLE);
String iconPermanent = ProcessingStatusPresentation.iconFor(DocumentCompletionStatus.FAILED_PERMANENT);
String tooltipRetryable = ProcessingStatusPresentation.tooltipFor(DocumentCompletionStatus.FAILED_RETRYABLE);
String tooltipPermanent = ProcessingStatusPresentation.tooltipFor(DocumentCompletionStatus.FAILED_PERMANENT);
assertAll(
() -> assertFalse(iconRetryable.equals(iconPermanent),
"Icons müssen sich unterscheiden"),
() -> assertFalse(tooltipRetryable.equals(tooltipPermanent),
"Tooltips müssen sich unterscheiden")
);
}
}