Files
CCMA/tests/test_options_dialog_ui.py
T
Marcel PeterkauandClaude Opus 5 528c24ad42 Clear every read-only marker when the store becomes writable again
"Erneut prüfen" refreshed the banner but left the window title and the status bar
reading "NUR LESEN" -- both were built once and never updated -- while the banner
itself asked for a restart that the recheck exists to avoid.

Title, status bar and banner are refreshed from one place now, so none of them can
be left behind, and the main window owns the title instead of app.py setting it
once at startup. The banner points at "Erneut prüfen" and says outright that no
restart is needed. Since the startup pass was skipped, its task list is empty
rather than current, so a successful recheck offers the housekeeper run that
fills it.

UI tests cover both directions of the recheck; their Tk root moved into a shared
conftest fixture, because a second root in another module invalidates the icon
images bound to the first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 00:01:37 +02:00

162 lines
5.8 KiB
Python

from email.parser import BytesParser
from email.policy import default
import pytest
pytest.importorskip("tkinter")
from ccma.config import AppConfig # noqa: E402
from ccma.domain.mail_templates import placeholder_entries # noqa: E402
from ccma.services.welcome_mail import generate_and_send_welcome_mail # noqa: E402
from ccma.storage.repository import MemberRepository, RepositoryError # noqa: E402
@pytest.fixture
def repository(tmp_path):
repository = MemberRepository(tmp_path / "store")
repository.initialize()
organization = repository.get_configuration()["organization"]
organization.update(
{
"name": "Chaos Computer Club Mannheim e.V.",
"email": "verwaltung@example.org",
"iban": "DE98670505050038907751",
"bic": "MANSDE66XXX",
}
)
repository.save_organization(organization)
return repository
def _open_options(tk_root, repository):
from ccma.ui.options_dialog import OptionsDialog
dialog = OptionsDialog(tk_root, AppConfig(store_path=str(repository.root)), repository)
tk_root.update()
return dialog
def _block_row(dialog):
for row in dialog.mail_template_placeholders.get_children():
label = str(dialog.mail_template_placeholders.item(row, "values")[0])
if label.startswith("{{#claims}}"):
return row
raise AssertionError("Der Wiederholungsblock fehlt in der Platzhalterliste.")
def test_double_click_on_a_block_is_wired_up(tk_root, repository):
dialog = _open_options(tk_root, repository)
try:
assert dialog.mail_template_placeholders.bind("<Double-1>")
assert dialog.mail_template_snippets[_block_row(dialog)] == next(
entry.snippet
for entry in placeholder_entries("welcome")
if entry.label.startswith("{{#claims}}")
)
finally:
dialog.grab_release()
dialog.destroy()
def test_inserting_a_block_puts_editable_template_code_into_the_body(tk_root, repository):
dialog = _open_options(tk_root, repository)
try:
dialog.mail_template_body.delete("1.0", "end")
dialog.mail_template_body.insert("1.0", "Offen sind aktuell:")
dialog.mail_template_placeholders.selection_set(_block_row(dialog))
dialog._insert_mail_placeholder()
tk_root.update()
body = dialog.mail_template_body.get("1.0", "end-1c")
finally:
dialog.grab_release()
dialog.destroy()
assert "…" not in body
assert "{{#claims}}" in body and "{{/claims}}" in body
assert "{{claim.description}}" in body
# The block was appended behind existing text and still starts on its own line.
assert body.splitlines()[0] == "Offen sind aktuell:"
assert body.splitlines()[1] == "{{#claims}}"
def test_edited_template_survives_saving_and_produces_real_lines_in_the_mail(
tk_root, repository, tmp_path
):
member = repository.create_member(
first_name="Ada", last_name="Lovelace", birth_date="1990-01-01"
)
member.email = "ada@example.org"
member.accepted_at = "2026-08-20"
repository.save_member(member)
repository.create_manual_claim(
member.member_id, title="Aufnahmegebühr", amount="15.00", due_date="2026-09-17"
)
dialog = _open_options(tk_root, repository)
try:
dialog.mail_template_subject_var.set("Willkommen, {{member.first_name}}")
dialog.mail_template_body.delete("1.0", "end")
dialog.mail_template_body.insert("1.0", "Offen sind aktuell:\n")
dialog.mail_template_placeholders.selection_set(_block_row(dialog))
dialog._insert_mail_placeholder()
tk_root.update()
dialog._save_mail_templates()
finally:
dialog.grab_release()
dialog.destroy()
generated = generate_and_send_welcome_mail(
repository,
member.member_id,
delivery_mode="local",
output_path=tmp_path / "Willkommen.eml",
sender_name="Verwaltung C3MA",
sender_email="verwaltung@example.org",
signature="Der Vorstand",
)
content = BytesParser(policy=default).parsebytes(
generated.export_path.read_bytes()
).get_content()
assert "Aufnahmegebühr (fällig 17.09.2026): 15.00 Euro" in content
assert "…" not in content
def test_the_dialog_refuses_to_save_an_unbalanced_block(tk_root, repository):
stored = repository.mail_templates_root / "willkommen.txt"
before = stored.read_text(encoding="utf-8")
dialog = _open_options(tk_root, repository)
try:
dialog.mail_template_body.delete("1.0", "end")
dialog.mail_template_body.insert("1.0", "Offen sind aktuell:\n{{#claims}}\n{{claim.title}}")
with pytest.raises(RepositoryError, match="wird nicht geschlossen"):
dialog._save_mail_templates()
finally:
dialog.grab_release()
dialog.destroy()
assert stored.read_text(encoding="utf-8") == before
def test_a_broken_template_does_not_half_save_the_others(tk_root, repository):
reminder_file = repository.mail_templates_root / "mahnung.txt"
before = reminder_file.read_text(encoding="utf-8")
dialog = _open_options(tk_root, repository)
try:
# Edit a valid template first, then break the second one.
dialog.mail_template_var.set("Mahnung")
dialog._select_mail_template()
dialog.mail_template_body.insert("end", "\nP.S. Bitte Mitgliedsnummer angeben.")
dialog.mail_template_var.set("Willkommen & Erstrechnung")
dialog._select_mail_template()
dialog.mail_template_body.insert("end", "\n{{#claims}}\n{{claim.title}}")
with pytest.raises(RepositoryError, match="wird nicht geschlossen"):
dialog._save_mail_templates()
finally:
dialog.grab_release()
dialog.destroy()
assert reminder_file.read_text(encoding="utf-8") == before