diff --git a/finance/tests/test_gui.py b/finance/tests/test_gui.py
index 407af09..159ea8d 100644
--- a/finance/tests/test_gui.py
+++ b/finance/tests/test_gui.py
@@ -124,3 +124,13 @@ def test_planning_page_has_all_sections(client):
assert r.status_code == 200
for heading in ("Wiederkehrende Posten", "Einmalposten", "Kredite", "Szenarien"):
assert heading in r.text
+
+
+def test_planning_page_shows_empty_suggestions_hint(client):
+ # UX-Regel: das "Vorschläge aus Buchungen"-Fieldset ist immer sichtbar,
+ # auch ohne Daten - statt komplett zu verschwinden zeigt es einen Hinweis.
+ client.post("/login", data={"username": "admin", "password": "geheim"})
+ r = client.get("/planung")
+ assert r.status_code == 200
+ assert "Vorschläge aus Buchungen" in r.text
+ assert "Keine Vorschläge" in r.text
diff --git a/finance/tests/test_import_api.py b/finance/tests/test_import_api.py
index 13c81b7..1af7c1c 100644
--- a/finance/tests/test_import_api.py
+++ b/finance/tests/test_import_api.py
@@ -1,3 +1,4 @@
+import re
from datetime import date
from decimal import Decimal
from pathlib import Path
@@ -203,6 +204,78 @@ def test_rollback_unknown_import_returns_404(client):
assert r.status_code == 404
+def _row_for(html: str, filename: str) -> str:
+ """Isoliert den
-Block eines bestimmten Import-Statements anhand seines
+ Dateinamens, damit Button-Zustaende eindeutig dem richtigen Statement
+ zugeordnet werden (statt nur global auf Disabled-Buttons zu zaehlen)."""
+ for row in html.split("
"):
+ if filename in row:
+ return row
+ raise AssertionError(f"keine Zeile fuer {filename!r} gefunden")
+
+
+def _has_active_button(row: str, label: str) -> bool:
+ for tag in re.finditer(r"", row):
+ if "disabled" not in tag.group(0):
+ return True
+ return False
+
+
+def _has_disabled_button(row: str, label: str) -> bool:
+ return bool(re.search(r"", row))
+
+
+def test_import_list_button_matrix(client, tmp_path, monkeypatch):
+ # UX-Regel: Bedienelemente sind immer sichtbar, ggf. ausgegraut (disabled),
+ # statt je nach Status komplett zu fehlen. Drei Statements mit den drei
+ # moeglichen Status erzeugen, dann pro Zeile die Button-Matrix pruefen.
+ monkeypatch.setenv("FB_INBOX_DIR", str(tmp_path / "inbox"))
+ monkeypatch.setenv("FB_UPLOADS_DIR", str(tmp_path / "uploads"))
+ from app.config import get_settings
+ get_settings.cache_clear()
+
+ # parse_pdf direkt (nicht ueber die fake_parse-Fixture) patchen/zuruecksetzen,
+ # damit der dritte Upload weiter unten wieder den echten Parser durchlaeuft
+ # und an kaputten Bytes wie in test_upload_corrupt_pdf_produces_error_statement_not_500
+ # scheitert - monkeypatch.undo() wuerde hier auch die setenv-Aenderungen
+ # oben zuruecknehmen.
+ import app.services.importer as importer_mod
+ original_parse_pdf = importer_mod.parse_pdf
+ importer_mod.parse_pdf = lambda p: FAKE
+ try:
+ sid_draft = _upload(client, "entwurf.pdf").json()["id"]
+ sid_confirmed = _upload(client, "bestaetigt.pdf").json()["id"]
+ assert client.post(f"/api/imports/{sid_confirmed}/confirm", headers=H).status_code == 200
+ finally:
+ importer_mod.parse_pdf = original_parse_pdf
+
+ r_err = client.post("/api/imports/upload", headers=H,
+ files={"file": ("fehler.pdf", b"not a pdf", "application/pdf")})
+ assert r_err.status_code == 201
+ assert r_err.json()["status"] == "error"
+
+ client.post("/login", data={"username": "admin", "password": "geheim"})
+ r = client.get("/import/list")
+ assert r.status_code == 200
+
+ draft_row = _row_for(r.text, "entwurf.pdf")
+ assert _has_active_button(draft_row, "Übernehmen")
+ assert _has_active_button(draft_row, "Verwerfen")
+ assert _has_disabled_button(draft_row, "Import zurückrollen")
+ assert not _has_active_button(draft_row, "Import zurückrollen")
+
+ confirmed_row = _row_for(r.text, "bestaetigt.pdf")
+ assert _has_disabled_button(confirmed_row, "Übernehmen")
+ assert _has_disabled_button(confirmed_row, "Verwerfen")
+ assert _has_active_button(confirmed_row, "Import zurückrollen")
+ assert not _has_disabled_button(confirmed_row, "Import zurückrollen")
+
+ error_row = _row_for(r.text, "fehler.pdf")
+ assert _has_active_button(error_row, "Verwerfen")
+ assert _has_disabled_button(error_row, "Übernehmen")
+ assert _has_disabled_button(error_row, "Import zurückrollen")
+
+
def test_upload_corrupt_pdf_produces_error_statement_not_500(client, tmp_path, monkeypatch):
# Datei besteht die Endungspruefung (.pdf), ist aber strukturell kein
# gueltiges PDF -> pdfplumber wirft eine eigene Exception (keine