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"]*>" + re.escape(label) + 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"]*disabled[^>]*>" + re.escape(label) + 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