#73: Summary-Banner unterhalb Fortschrittsbalken nach Laufabschluss
Neue Komponente BatchRunSummaryBanner aggregiert die Ergebnisliste nach Laufende und zeigt je Kategorie Icon + Anzahl + Text an. Banner verschwindet beim Start des nächsten Laufs. READY_FOR_AI, PROPOSAL_READY und PROCESSING werden nicht gezählt (nicht im DocumentCompletionStatus-Enum enthalten); Reset-Pending-Zeilen werden explizit ausgeschlossen. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+202
@@ -0,0 +1,202 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einzeilige Zusammenfassungsleiste, die nach Abschluss eines Verarbeitungslaufs
|
||||||
|
* die aggregierten Ergebnisse anzeigt.
|
||||||
|
*
|
||||||
|
* <p>Das Banner erscheint nach Laufabschluss unterhalb des Fortschrittsbalkens und
|
||||||
|
* oberhalb der Ergebnistabelle. Es zeigt nur Kategorien, deren Zähler größer als null
|
||||||
|
* ist. Folgende Status werden nicht gezählt und tauchen nie im Banner auf:
|
||||||
|
* {@code READY_FOR_AI}, {@code PROPOSAL_READY} und {@code PROCESSING} sind im
|
||||||
|
* Enum {@link DocumentCompletionStatus} nicht enthalten – alle enthaltenen Werte
|
||||||
|
* werden gezählt, außer Einträgen mit {@code resetPending=true}, da diese keinen
|
||||||
|
* abgeschlossenen Zustand darstellen.
|
||||||
|
*
|
||||||
|
* <p>Farbe ist niemals das einzige Unterscheidungsmerkmal: Jedes Segment enthält
|
||||||
|
* ein Icon und einen Text.
|
||||||
|
*
|
||||||
|
* <p>Die öffentlichen Methoden {@link #clear()} und {@link #update(Map)} sind
|
||||||
|
* thread-agnostisch definiert, aber müssen auf dem JavaFX Application Thread aufgerufen
|
||||||
|
* werden (oder das Banner muss via {@code Platform.runLater} aktualisiert werden).
|
||||||
|
* Die Aggregations-Hilfsmethode {@link #aggregateCounts(Iterable)} ist vollständig
|
||||||
|
* unabhängig von JavaFX und kann auf jedem Thread aufgerufen werden.
|
||||||
|
*/
|
||||||
|
public final class BatchRunSummaryBanner {
|
||||||
|
|
||||||
|
/** Trennzeichen zwischen den Kategoriesegmenten. */
|
||||||
|
private static final String SEGMENT_SEPARATOR = " · ";
|
||||||
|
|
||||||
|
/** Abstand zwischen den Label-Segmenten in Pixeln. */
|
||||||
|
private static final int SPACING = 0;
|
||||||
|
|
||||||
|
/** Innerer Abstand des Containers in Pixeln (oben/unten). */
|
||||||
|
private static final double PADDING_V = 4.0;
|
||||||
|
|
||||||
|
/** Standardfarbe für den Summentext. */
|
||||||
|
private static final String STYLE_DEFAULT = "-fx-font-size: 12;";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alle {@link DocumentCompletionStatus}-Werte, die im Banner angezeigt werden,
|
||||||
|
* in der verbindlichen Anzeigereihenfolge gemäß Spezifikation.
|
||||||
|
*/
|
||||||
|
private static final List<DocumentCompletionStatus> DISPLAYED_ORDER = List.of(
|
||||||
|
DocumentCompletionStatus.SUCCESS,
|
||||||
|
DocumentCompletionStatus.FAILED_RETRYABLE,
|
||||||
|
DocumentCompletionStatus.FAILED_PERMANENT,
|
||||||
|
DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED,
|
||||||
|
DocumentCompletionStatus.SKIPPED_FINAL_FAILURE
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Wurzel-Container des Banners – wird in das Tab-Layout eingebettet. */
|
||||||
|
private final HBox container;
|
||||||
|
|
||||||
|
/** Label, das den kompletten Bannertext als Inline-Segmente trägt. */
|
||||||
|
private final Label contentLabel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt ein neues, initial unsichtbares Summary-Banner.
|
||||||
|
*/
|
||||||
|
public BatchRunSummaryBanner() {
|
||||||
|
contentLabel = new Label();
|
||||||
|
contentLabel.setStyle(STYLE_DEFAULT);
|
||||||
|
contentLabel.setWrapText(false);
|
||||||
|
|
||||||
|
container = new HBox(SPACING, contentLabel);
|
||||||
|
container.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
container.setStyle("-fx-padding: " + PADDING_V + " 0 " + PADDING_V + " 0;");
|
||||||
|
|
||||||
|
// Initial unsichtbar, nimmt keinen Platz ein
|
||||||
|
container.setVisible(false);
|
||||||
|
container.setManaged(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Öffentliche API
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Versteckt das Banner und leert seinen Inhalt.
|
||||||
|
*
|
||||||
|
* <p>Muss auf dem JavaFX Application Thread aufgerufen werden.
|
||||||
|
*/
|
||||||
|
public void clear() {
|
||||||
|
contentLabel.setText("");
|
||||||
|
container.setVisible(false);
|
||||||
|
container.setManaged(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert das Banner mit den aggregierten Zählern und macht es sichtbar.
|
||||||
|
*
|
||||||
|
* <p>Zeigt nur Kategorien mit Anzahl > 0. Wenn alle Zähler null sind (leerer Lauf),
|
||||||
|
* wird das Banner versteckt.
|
||||||
|
*
|
||||||
|
* <p>Muss auf dem JavaFX Application Thread aufgerufen werden.
|
||||||
|
*
|
||||||
|
* @param counts Zuordnung von Verarbeitungsstatus zu Anzahl;
|
||||||
|
* fehlende Status werden als 0 interpretiert; darf nicht null sein
|
||||||
|
*/
|
||||||
|
public void update(Map<DocumentCompletionStatus, Integer> counts) {
|
||||||
|
Objects.requireNonNull(counts, "counts darf nicht null sein");
|
||||||
|
|
||||||
|
String text = buildBannerText(counts);
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentLabel.setText(text);
|
||||||
|
container.setVisible(true);
|
||||||
|
container.setManaged(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert den JavaFX-Container-Knoten zum Einbetten in das Tab-Layout.
|
||||||
|
*
|
||||||
|
* @return der Container-Knoten; nie null
|
||||||
|
*/
|
||||||
|
public HBox getNode() {
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Aggregations-Hilfe (thread-agnostisch, testbar ohne JavaFX)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zählt die Anzahl jedes {@link DocumentCompletionStatus} in der übergebenen
|
||||||
|
* Iterable. Einträge mit {@code resetPending=true} werden ignoriert, da sie
|
||||||
|
* keinen abgeschlossenen Verarbeitungszustand darstellen.
|
||||||
|
*
|
||||||
|
* <p>Diese Methode ist vollständig unabhängig von JavaFX und kann auf jedem
|
||||||
|
* Thread aufgerufen werden.
|
||||||
|
*
|
||||||
|
* @param rows die Ergebniszeilen des Laufs; darf nicht null sein;
|
||||||
|
* null-Elemente werden übersprungen
|
||||||
|
* @return eine Map mit der Anzahl je Status; enthält alle anzuzeigenden
|
||||||
|
* Status (fehlende haben Wert 0); nie null
|
||||||
|
*/
|
||||||
|
public static Map<DocumentCompletionStatus, Integer> aggregateCounts(
|
||||||
|
Iterable<? extends GuiBatchRunResultRow> rows) {
|
||||||
|
Objects.requireNonNull(rows, "rows darf nicht null sein");
|
||||||
|
|
||||||
|
Map<DocumentCompletionStatus, Integer> counts = new EnumMap<>(DocumentCompletionStatus.class);
|
||||||
|
// Alle anzuzeigenden Status mit 0 vorbelegen
|
||||||
|
for (DocumentCompletionStatus status : DISPLAYED_ORDER) {
|
||||||
|
counts.put(status, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (GuiBatchRunResultRow row : rows) {
|
||||||
|
if (row == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Reset-Pending-Zeilen zählen nicht – sie haben noch keinen abgeschlossenen Status
|
||||||
|
if (row.resetPending()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DocumentCompletionStatus status = row.status();
|
||||||
|
// Nur anzuzeigende Status zählen (entspricht dem Ausschluss von
|
||||||
|
// Übergangszuständen wie READY_FOR_AI, PROPOSAL_READY, PROCESSING)
|
||||||
|
if (counts.containsKey(status)) {
|
||||||
|
counts.merge(status, 1, Integer::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Interne Hilfsmethoden
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erzeugt den angezeigten Bannertext aus den Zählern.
|
||||||
|
* Liefert einen leeren String wenn alle Zähler null sind.
|
||||||
|
*
|
||||||
|
* @param counts die Zähler je Status; darf nicht null sein
|
||||||
|
* @return der fertige Bannertext oder ein leerer String
|
||||||
|
*/
|
||||||
|
static String buildBannerText(Map<DocumentCompletionStatus, Integer> counts) {
|
||||||
|
List<String> segments = new ArrayList<>();
|
||||||
|
for (DocumentCompletionStatus status : DISPLAYED_ORDER) {
|
||||||
|
int count = counts.getOrDefault(status, 0);
|
||||||
|
if (count > 0) {
|
||||||
|
String icon = ProcessingStatusPresentation.iconFor(status);
|
||||||
|
String category = ProcessingStatusPresentation.summaryCategoryFor(status);
|
||||||
|
segments.add(icon + " " + count + " " + category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String.join(SEGMENT_SEPARATOR, segments);
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-2
@@ -199,6 +199,9 @@ public final class GuiBatchRunTab {
|
|||||||
/** PDF-Vorschau-Komponente im Detailbereich. */
|
/** PDF-Vorschau-Komponente im Detailbereich. */
|
||||||
private final PdfPreviewPane pdfPreview = new PdfPreviewPane();
|
private final PdfPreviewPane pdfPreview = new PdfPreviewPane();
|
||||||
|
|
||||||
|
/** Summary-Banner unterhalb des Fortschrittsbalkens – sichtbar nach Laufabschluss. */
|
||||||
|
private final BatchRunSummaryBanner summaryBanner = new BatchRunSummaryBanner();
|
||||||
|
|
||||||
private final Supplier<Path> configPathSupplier;
|
private final Supplier<Path> configPathSupplier;
|
||||||
private final BooleanSupplier savedConfigurationReadyCheck;
|
private final BooleanSupplier savedConfigurationReadyCheck;
|
||||||
private final Runnable onRunStateChanged;
|
private final Runnable onRunStateChanged;
|
||||||
@@ -501,8 +504,14 @@ public final class GuiBatchRunTab {
|
|||||||
HBox.setHgrow(progressBar, Priority.ALWAYS);
|
HBox.setHgrow(progressBar, Priority.ALWAYS);
|
||||||
|
|
||||||
counterLabel.setId("batch-run-counter");
|
counterLabel.setId("batch-run-counter");
|
||||||
HBox header = new HBox(SECONDARY_SPACING, progressBar, counterLabel);
|
HBox progressRow = new HBox(SECONDARY_SPACING, progressBar, counterLabel);
|
||||||
header.setAlignment(Pos.CENTER_LEFT);
|
progressRow.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
// Summary-Banner unterhalb des Fortschrittsbalkens, oberhalb der Tabelle
|
||||||
|
HBox bannerNode = summaryBanner.getNode();
|
||||||
|
bannerNode.setId("batch-run-summary-banner");
|
||||||
|
|
||||||
|
VBox header = new VBox(0, progressRow, bannerNode);
|
||||||
header.setPadding(new Insets(0, 0, SECONDARY_SPACING, 0));
|
header.setPadding(new Insets(0, 0, SECONDARY_SPACING, 0));
|
||||||
return header;
|
return header;
|
||||||
}
|
}
|
||||||
@@ -1187,6 +1196,7 @@ public final class GuiBatchRunTab {
|
|||||||
messageArea.setVisible(false);
|
messageArea.setVisible(false);
|
||||||
messageArea.setManaged(false);
|
messageArea.setManaged(false);
|
||||||
messageArea.setStyle(null);
|
messageArea.setStyle(null);
|
||||||
|
summaryBanner.clear();
|
||||||
resetMetrics();
|
resetMetrics();
|
||||||
updateCounterLabel();
|
updateCounterLabel();
|
||||||
progressBar.setProgress(0);
|
progressBar.setProgress(0);
|
||||||
@@ -1580,6 +1590,10 @@ public final class GuiBatchRunTab {
|
|||||||
miniRunCompletedFingerprints = new HashSet<>();
|
miniRunCompletedFingerprints = new HashSet<>();
|
||||||
}
|
}
|
||||||
selectedRows.clear();
|
selectedRows.clear();
|
||||||
|
// Summary-Banner aus der aktuellen Ergebnisliste aggregieren und anzeigen
|
||||||
|
Map<DocumentCompletionStatus, Integer> counts =
|
||||||
|
BatchRunSummaryBanner.aggregateCounts(resultItems);
|
||||||
|
summaryBanner.update(counts);
|
||||||
appendSummary(outcome);
|
appendSummary(outcome);
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
notifyRunStateChanged();
|
notifyRunStateChanged();
|
||||||
|
|||||||
+233
@@ -0,0 +1,233 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
|
||||||
|
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit-Tests für {@link BatchRunSummaryBanner}.
|
||||||
|
* <p>
|
||||||
|
* Geprüft werden die Aggregationslogik und die Textgenerierung unabhängig von JavaFX.
|
||||||
|
* Die GUI-Integrationsmethoden ({@code clear()}, {@code update()}, {@code getNode()})
|
||||||
|
* erfordern eine JavaFX-Runtime und werden durch Smoke-Tests abgedeckt.
|
||||||
|
*/
|
||||||
|
class BatchRunSummaryBannerTest {
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Hilfsmethoden für Testdaten
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private static GuiBatchRunResultRow row(DocumentCompletionStatus status) {
|
||||||
|
return new GuiBatchRunResultRow(
|
||||||
|
"test.pdf",
|
||||||
|
new DocumentFingerprint("a".repeat(64)),
|
||||||
|
status,
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
Duration.ZERO,
|
||||||
|
false,
|
||||||
|
Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GuiBatchRunResultRow resetPendingRow() {
|
||||||
|
GuiBatchRunResultRow base = row(DocumentCompletionStatus.SUCCESS);
|
||||||
|
return GuiBatchRunResultRow.resetMarker(base);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// aggregateCounts
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aggregateCounts_leereListe_alleZaehlerNull() {
|
||||||
|
Map<DocumentCompletionStatus, Integer> counts =
|
||||||
|
BatchRunSummaryBanner.aggregateCounts(Collections.emptyList());
|
||||||
|
|
||||||
|
assertEquals(0, counts.getOrDefault(DocumentCompletionStatus.SUCCESS, 0));
|
||||||
|
assertEquals(0, counts.getOrDefault(DocumentCompletionStatus.FAILED_RETRYABLE, 0));
|
||||||
|
assertEquals(0, counts.getOrDefault(DocumentCompletionStatus.FAILED_PERMANENT, 0));
|
||||||
|
assertEquals(0, counts.getOrDefault(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED, 0));
|
||||||
|
assertEquals(0, counts.getOrDefault(DocumentCompletionStatus.SKIPPED_FINAL_FAILURE, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aggregateCounts_nurErfolgreiche_zaehltNurSuccess() {
|
||||||
|
List<GuiBatchRunResultRow> rows = List.of(
|
||||||
|
row(DocumentCompletionStatus.SUCCESS),
|
||||||
|
row(DocumentCompletionStatus.SUCCESS),
|
||||||
|
row(DocumentCompletionStatus.SUCCESS));
|
||||||
|
|
||||||
|
Map<DocumentCompletionStatus, Integer> counts =
|
||||||
|
BatchRunSummaryBanner.aggregateCounts(rows);
|
||||||
|
|
||||||
|
assertEquals(3, counts.get(DocumentCompletionStatus.SUCCESS));
|
||||||
|
assertEquals(0, counts.get(DocumentCompletionStatus.FAILED_RETRYABLE));
|
||||||
|
assertEquals(0, counts.get(DocumentCompletionStatus.FAILED_PERMANENT));
|
||||||
|
assertEquals(0, counts.get(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED));
|
||||||
|
assertEquals(0, counts.get(DocumentCompletionStatus.SKIPPED_FINAL_FAILURE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aggregateCounts_gemischterLauf_alleKategorienKorrekt() {
|
||||||
|
List<GuiBatchRunResultRow> rows = List.of(
|
||||||
|
row(DocumentCompletionStatus.SUCCESS),
|
||||||
|
row(DocumentCompletionStatus.SUCCESS),
|
||||||
|
row(DocumentCompletionStatus.FAILED_RETRYABLE),
|
||||||
|
row(DocumentCompletionStatus.FAILED_PERMANENT),
|
||||||
|
row(DocumentCompletionStatus.FAILED_PERMANENT),
|
||||||
|
row(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED),
|
||||||
|
row(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED),
|
||||||
|
row(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED),
|
||||||
|
row(DocumentCompletionStatus.SKIPPED_FINAL_FAILURE));
|
||||||
|
|
||||||
|
Map<DocumentCompletionStatus, Integer> counts =
|
||||||
|
BatchRunSummaryBanner.aggregateCounts(rows);
|
||||||
|
|
||||||
|
assertEquals(2, counts.get(DocumentCompletionStatus.SUCCESS));
|
||||||
|
assertEquals(1, counts.get(DocumentCompletionStatus.FAILED_RETRYABLE));
|
||||||
|
assertEquals(2, counts.get(DocumentCompletionStatus.FAILED_PERMANENT));
|
||||||
|
assertEquals(3, counts.get(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED));
|
||||||
|
assertEquals(1, counts.get(DocumentCompletionStatus.SKIPPED_FINAL_FAILURE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aggregateCounts_resetPendingZeilenWerdenNichtGezaehlt() {
|
||||||
|
// Reset-Pending-Zeilen haben noch keinen abgeschlossenen Status und
|
||||||
|
// dürfen nicht ins Summary einfließen
|
||||||
|
List<GuiBatchRunResultRow> rows = List.of(
|
||||||
|
row(DocumentCompletionStatus.SUCCESS),
|
||||||
|
resetPendingRow(),
|
||||||
|
resetPendingRow());
|
||||||
|
|
||||||
|
Map<DocumentCompletionStatus, Integer> counts =
|
||||||
|
BatchRunSummaryBanner.aggregateCounts(rows);
|
||||||
|
|
||||||
|
assertEquals(1, counts.get(DocumentCompletionStatus.SUCCESS));
|
||||||
|
assertEquals(0, counts.get(DocumentCompletionStatus.FAILED_RETRYABLE));
|
||||||
|
assertEquals(0, counts.get(DocumentCompletionStatus.FAILED_PERMANENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// buildBannerText
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildBannerText_alleZaehlerNull_leerString() {
|
||||||
|
Map<DocumentCompletionStatus, Integer> counts = Map.of(
|
||||||
|
DocumentCompletionStatus.SUCCESS, 0,
|
||||||
|
DocumentCompletionStatus.FAILED_RETRYABLE, 0,
|
||||||
|
DocumentCompletionStatus.FAILED_PERMANENT, 0,
|
||||||
|
DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED, 0,
|
||||||
|
DocumentCompletionStatus.SKIPPED_FINAL_FAILURE, 0);
|
||||||
|
|
||||||
|
String text = BatchRunSummaryBanner.buildBannerText(counts);
|
||||||
|
|
||||||
|
assertTrue(text.isEmpty(), "Leere Zähler ergeben leeren Text: '" + text + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildBannerText_nurErfolgreiche_nurSuccessSegment() {
|
||||||
|
Map<DocumentCompletionStatus, Integer> counts = Map.of(
|
||||||
|
DocumentCompletionStatus.SUCCESS, 17,
|
||||||
|
DocumentCompletionStatus.FAILED_RETRYABLE, 0,
|
||||||
|
DocumentCompletionStatus.FAILED_PERMANENT, 0,
|
||||||
|
DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED, 0,
|
||||||
|
DocumentCompletionStatus.SKIPPED_FINAL_FAILURE, 0);
|
||||||
|
|
||||||
|
String text = BatchRunSummaryBanner.buildBannerText(counts);
|
||||||
|
|
||||||
|
assertTrue(text.contains("17"), "Anzahl 17 muss im Text erscheinen: " + text);
|
||||||
|
assertTrue(text.contains("erfolgreich"), "Kategorie 'erfolgreich' muss erscheinen: " + text);
|
||||||
|
assertTrue(text.contains("✓"), "Icon ✓ muss erscheinen: " + text);
|
||||||
|
assertFalse(text.contains("↻"), "Kein ↻ wenn FAILED_RETRYABLE = 0: " + text);
|
||||||
|
assertFalse(text.contains("×"), "Kein × wenn FAILED_PERMANENT = 0: " + text);
|
||||||
|
assertFalse(text.contains("≡"), "Kein ≡ wenn SKIPPED_ALREADY_PROCESSED = 0: " + text);
|
||||||
|
assertFalse(text.contains("⊘"), "Kein ⊘ wenn SKIPPED_FINAL_FAILURE = 0: " + text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildBannerText_vollerLauf_alleSegmenteEnthalten() {
|
||||||
|
Map<DocumentCompletionStatus, Integer> counts = Map.of(
|
||||||
|
DocumentCompletionStatus.SUCCESS, 14,
|
||||||
|
DocumentCompletionStatus.FAILED_RETRYABLE, 1,
|
||||||
|
DocumentCompletionStatus.FAILED_PERMANENT, 2,
|
||||||
|
DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED, 3,
|
||||||
|
DocumentCompletionStatus.SKIPPED_FINAL_FAILURE, 1);
|
||||||
|
|
||||||
|
String text = BatchRunSummaryBanner.buildBannerText(counts);
|
||||||
|
|
||||||
|
// Jedes Segment enthält Icon + Anzahl + Kategorie
|
||||||
|
assertTrue(text.contains("✓ 14 erfolgreich"), "SUCCESS-Segment: " + text);
|
||||||
|
assertTrue(text.contains("↻ 1 wird wiederholt"), "FAILED_RETRYABLE-Segment: " + text);
|
||||||
|
assertTrue(text.contains("× 2 fehlgeschlagen"), "FAILED_PERMANENT-Segment: " + text);
|
||||||
|
assertTrue(text.contains("≡ 3 übersprungen"), "SKIPPED_ALREADY_PROCESSED-Segment: " + text);
|
||||||
|
assertTrue(text.contains("⊘ 1 endgültig übersprungen"), "SKIPPED_FINAL_FAILURE-Segment: " + text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildBannerText_nurSkippedFinalFailure_erscheintImBanner() {
|
||||||
|
// Sicherstellung: ⊘ erscheint auch wenn > 0, obwohl es die seltenste Kategorie ist
|
||||||
|
Map<DocumentCompletionStatus, Integer> counts = Map.of(
|
||||||
|
DocumentCompletionStatus.SUCCESS, 0,
|
||||||
|
DocumentCompletionStatus.FAILED_RETRYABLE, 0,
|
||||||
|
DocumentCompletionStatus.FAILED_PERMANENT, 0,
|
||||||
|
DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED, 0,
|
||||||
|
DocumentCompletionStatus.SKIPPED_FINAL_FAILURE, 2);
|
||||||
|
|
||||||
|
String text = BatchRunSummaryBanner.buildBannerText(counts);
|
||||||
|
|
||||||
|
assertTrue(text.contains("⊘"), "Icon ⊘ muss erscheinen: " + text);
|
||||||
|
assertTrue(text.contains("2"), "Anzahl 2 muss erscheinen: " + text);
|
||||||
|
assertTrue(text.contains("endgültig übersprungen"), "Kategorie muss erscheinen: " + text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildBannerText_nurKategorienMitAnzahlGroesserNull_erscheinen() {
|
||||||
|
// Nur SUCCESS=5 ist gesetzt; alle anderen 0 → kein anderes Segment
|
||||||
|
Map<DocumentCompletionStatus, Integer> counts = Map.of(
|
||||||
|
DocumentCompletionStatus.SUCCESS, 5,
|
||||||
|
DocumentCompletionStatus.FAILED_RETRYABLE, 0,
|
||||||
|
DocumentCompletionStatus.FAILED_PERMANENT, 0,
|
||||||
|
DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED, 0,
|
||||||
|
DocumentCompletionStatus.SKIPPED_FINAL_FAILURE, 0);
|
||||||
|
|
||||||
|
String text = BatchRunSummaryBanner.buildBannerText(counts);
|
||||||
|
|
||||||
|
// Kein Trennzeichen (·) darf erscheinen, wenn nur ein Segment vorhanden ist
|
||||||
|
assertFalse(text.contains("·"), "Kein Trenner bei einzelnem Segment: " + text);
|
||||||
|
assertTrue(text.contains("✓ 5 erfolgreich"), "Nur SUCCESS-Segment: " + text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aggregateCounts_kombinationMitResetPending_nurEchtAbgeschlosseneGezaehlt() {
|
||||||
|
// 2 SUCCESS + 1 FAILED_PERMANENT + 1 resetPending(SUCCESS) → nur 2+1 gezählt
|
||||||
|
List<GuiBatchRunResultRow> rows = List.of(
|
||||||
|
row(DocumentCompletionStatus.SUCCESS),
|
||||||
|
row(DocumentCompletionStatus.SUCCESS),
|
||||||
|
row(DocumentCompletionStatus.FAILED_PERMANENT),
|
||||||
|
resetPendingRow());
|
||||||
|
|
||||||
|
Map<DocumentCompletionStatus, Integer> counts =
|
||||||
|
BatchRunSummaryBanner.aggregateCounts(rows);
|
||||||
|
|
||||||
|
assertEquals(2, counts.get(DocumentCompletionStatus.SUCCESS));
|
||||||
|
assertEquals(1, counts.get(DocumentCompletionStatus.FAILED_PERMANENT));
|
||||||
|
// Summe aller gezählten Einträge = 3, nicht 4
|
||||||
|
int total = counts.values().stream().mapToInt(Integer::intValue).sum();
|
||||||
|
assertEquals(3, total, "Reset-Pending darf nicht mitgezählt werden");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user