Files
CCMA/tests/test_ui_imports.py
T
Marcel PeterkauandClaude Opus 5 9e9bb7d668 Ask every member to check the data the club stores about them
The club has to keep its member data current, and until now that meant writing
to each member by hand. "Datenüberprüfung anfragen" in the members tab sends one
mail per member, each listing that member's own record: number, name, nickname,
birth date, contact data, address, status, member since, payment frequency, and
the bank details only for members who have any.

Two decisions the record itself forced:

A field with no value is printed as "(nicht hinterlegt)" rather than left out.
The point of the mail is to have gaps filled in, and a missing line is a gap
nobody sees.

The IBAN is masked down to its country code and last four digits. That is enough
to recognise the account, and it keeps a full account number out of a mail the
club sends to dozens of people at once.

Every member is listed as a recipient, with the live memberships that have an
address preselected -- a member who resigned at year's end may still need to
confirm their address, so the board can add them by hand. Members without an
address are skipped and reported instead of failing the run.

Delivery reuses the existing mail machinery: the configured delivery mode, one
SMTP/IMAP connection for the whole run, an archive copy in the member file, a
"data_review_email_sent" event, and the read-only guard before anything is
rendered. A member whose mail fails is reported as a warning and the run
continues -- one bad address must not stop a mailing to the whole club halfway
through. Because the run cannot be taken back, the recipient count is confirmed
once more before it starts.

Subject and text come from a new "Datenüberprüfung" template, editable like the
others, with {{data.sheet}} for the whole record and a {{#data}} block for a
layout of the board's own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:05:38 +02:00

199 lines
7.5 KiB
Python

def test_ui_modules_import_without_creating_root_window() -> None:
import ccma.app # noqa: F401
import ccma.ui.asset_tab # noqa: F401
import ccma.ui.claim_tab # noqa: F401
import ccma.ui.data_review_mail_dialog # noqa: F401
import ccma.ui.main_window # noqa: F401
import ccma.ui.member_tab # noqa: F401
import ccma.ui.splash # noqa: F401
def test_splash_position_centers_on_pointer_and_stays_on_screen() -> None:
from ccma.ui.splash import centered_position
assert centered_position(
width=620,
height=330,
pointer_x=2500,
pointer_y=600,
screen_x=0,
screen_y=0,
screen_width=3840,
screen_height=1080,
) == (2190, 435)
assert centered_position(
width=620,
height=330,
pointer_x=10,
pointer_y=10,
screen_x=0,
screen_y=0,
screen_width=1920,
screen_height=1080,
) == (0, 0)
def test_splash_minimum_time_only_waits_for_remaining_duration() -> None:
from ccma.ui.splash import (
_member_delay_for_splash,
_progress_value,
_remaining_minimum_ms,
)
assert _remaining_minimum_ms(100.0, 5.0, 102.25) == 2750
assert _remaining_minimum_ms(100.0, 5.0, 106.0) == 0
assert _remaining_minimum_ms(100.0, 0.0, 100.0) == 0
assert _progress_value(2.5, 5.0, startup_finished=False) == 47.5
assert _progress_value(5.0, 5.0, startup_finished=False) == 95.0
assert _progress_value(2.5, 5.0, startup_finished=True) == 50.0
assert _progress_value(5.0, 5.0, startup_finished=True) == 100.0
assert _member_delay_for_splash(5.0, 10) == 0.5
assert _member_delay_for_splash(5.0, 0) == 0.0
assert _member_delay_for_splash(0.0, 10) == 0.0
def test_event_labels_hide_board_actor_but_keep_automatic_marker() -> None:
from ccma.domain.models import Event
from ccma.ui.member_tab import _event_label
user_event = Event("1", "2026-01-01T00:00:00+01:00", "comment", "Kommentar", "user", "Vorstand")
system_event = Event("2", "2026-01-01T00:00:00+01:00", "automatic", "Automatisch")
assert _event_label(user_event) == "Kommentar"
assert _event_label(system_event) == "[AUTO] Automatisch"
def test_housekeeper_details_are_multiline() -> None:
from datetime import date
from ccma.domain.models import HousekeeperFinding
from ccma.ui.work_tabs import _finding_details
finding = HousekeeperFinding(
severity="error",
code="invalid_member_record",
title="Mitgliederakte beschädigt",
detail="Die JSON-Datei ist leer und wird nicht automatisch überschrieben.",
member_id="member-1",
due_date=date(2026, 7, 31),
)
rendered = _finding_details(finding)
assert rendered.splitlines()[0] == "ERROR · invalid_member_record"
assert "Mitgliederakte beschädigt\nMitglied: member-1\nFällig:" in rendered
assert rendered.endswith("nicht automatisch überschrieben.")
def test_german_ui_labels_round_trip_to_english_storage_keys() -> None:
from ccma.domain.models import MEMBERSHIP_STATUS_LABELS
from ccma.ui.labels import (
CLAIM_ITEM_TYPE_LABELS,
THEME_LABELS,
display_label,
storage_key,
)
assert display_label(THEME_LABELS, "dark") == "Dunkel"
assert storage_key(THEME_LABELS, "Hell") == "light"
assert display_label(CLAIM_ITEM_TYPE_LABELS, "credit") == "Gutschrift"
assert storage_key(CLAIM_ITEM_TYPE_LABELS, "Dienstleistung") == "service"
assert display_label(MEMBERSHIP_STATUS_LABELS, "active") == "AKTIV"
assert storage_key(MEMBERSHIP_STATUS_LABELS, "EHRENMITGLIED") == "honorary"
def test_member_table_filter_only_keeps_selected_status() -> None:
from ccma.domain.models import Member
from ccma.ui.work_tabs import _filter_members, _selected_status_filter
members = [
Member("1", "0001", "Ada", "Lovelace", status="active"),
Member("2", "0002", "Grace", "Hopper", status="application"),
Member("3", "0003", "Linus", "Example", status="active"),
]
assert _selected_status_filter("Alle") == "all"
assert _selected_status_filter("AKTIV") == "active"
assert [member.member_id for member in _filter_members(members, "active")] == ["1", "3"]
assert [member.member_id for member in _filter_members(members, "all")] == ["1", "2", "3"]
def test_member_table_sort_uses_display_values() -> None:
from ccma.domain.models import Member
from ccma.ui.work_tabs import _sort_members
members = [
Member("1", "0002", "Grace", "Hopper", status="application"),
Member("2", "0001", "Ada", "Lovelace", status="active"),
Member("3", "0003", "Linus", "Example", status="honorary"),
]
assert [member.member_id for member in _sort_members(members, "number", False)] == ["2", "1", "3"]
assert [member.member_id for member in _sort_members(members, "first_name", False)] == ["2", "1", "3"]
assert [member.member_id for member in _sort_members(members, "last_name", False)] == ["3", "1", "2"]
assert [member.member_id for member in _sort_members(members, "status", False)] == ["2", "1", "3"]
def test_member_finding_severity_uses_highest_level_per_member() -> None:
from ccma.domain.models import HousekeeperFinding
from ccma.ui.work_tabs import _member_finding_severities
findings = [
HousekeeperFinding("info", "info", "Info", "", member_id="1"),
HousekeeperFinding("warning", "warning", "Warning", "", member_id="1"),
HousekeeperFinding("error", "error", "Error", "", member_id="2"),
HousekeeperFinding("info", "global", "Global", ""),
]
assert _member_finding_severities(findings) == {"1": "warning", "2": "error"}
def test_asset_table_filter_and_sort_use_status_and_holder_label() -> None:
from ccma.domain.models import Asset
from ccma.ui.work_tabs import _asset_table_value, _filter_assets, _selected_asset_filter
assets = [
Asset("1", "Clubraumschlüssel", status="issued", current_holder_member_id="member-1"),
Asset("2", "Beamer", status="available"),
]
assert _selected_asset_filter("Alle") == "all"
assert _selected_asset_filter("AUSGEGEBEN") == "issued"
assert [asset.asset_id for asset in _filter_assets(assets, "available")] == ["2"]
assert _asset_table_value(assets[0], "holder", "0001 · Ada") == "0001 · Ada"
def test_claim_table_sort_uses_due_date_by_raw_value() -> None:
from ccma.domain.models import ContributionData
from ccma.ui.member_tab import _claim_sort_value
data = ContributionData()
older = {"title": "Alt", "due_date": "2024-01-31", "amount": "75.00"}
newer = {"title": "Neu", "due_date": "2025-07-31", "amount": "50.00"}
assert _claim_sort_value(data, older, "due") < _claim_sort_value(data, newer, "due")
def test_negative_claims_are_labeled_as_credit() -> None:
from ccma.domain.contributions import claim_status
from ccma.domain.models import ContributionData
data = ContributionData()
claim = {"claim_id": "claim-1", "title": "Rueckzahlung", "amount": "-25.00"}
assert claim_status(data, claim) == "credit"
def test_housekeeper_details_include_asset_target() -> None:
from ccma.domain.models import HousekeeperFinding
from ccma.ui.work_tabs import _finding_details
finding = HousekeeperFinding(
severity="warning",
code="json_hash_mismatch",
title="Assetakte extern geändert",
detail="asset.json: Hash fehlt oder stimmt nicht.",
asset_id="asset-1",
target_type="asset",
)
rendered = _finding_details(finding)
assert "Asset: asset-1" in rendered