r""" core/msgbox - which button Enter answers. python tests\test_msgbox.py Review item P1 (2026-10-01): the per-claim save box's default button was OK, so pressing Enter - out of habit, or to dismiss what looked like one more info box - would save the claim to live eCW. `default_cancel=True` adds MB_DEFBUTTON2 so Enter/Space answers Cancel; every other caller (captcha, security image) must keep the box it always had. No real message box is shown: `_raw_call`, the one function that reaches user32, is replaced by a recorder that answers with a chosen return code. """ import sys import pathlib ROOT = pathlib.Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "src")) from ecw_bot.core import msgbox # noqa: E402 CASES = [] def case(fn): CASES.append(fn) return fn _DEFBUTTON2 = 0x00000100 _IDOK, _IDCANCEL = 1, 2 def _capture(fn, rc=_IDCANCEL): """Call fn() with _raw_call recording its flags. Returns (result, flags).""" seen = [] orig = msgbox._raw_call msgbox._raw_call = lambda text, caption, flags, timeout_s: ( seen.append(flags) or rc) try: result = fn() finally: msgbox._raw_call = orig assert len(seen) == 1, seen return result, seen[0] @case def defbutton2_constant_is_the_win32_value(): # Pinned: a typo here would silently leave OK as the default button. assert msgbox._MB_DEFBUTTON2 == _DEFBUTTON2, hex(msgbox._MB_DEFBUTTON2) @case def confirm_default_keeps_ok_as_the_default_button(): _, flags = _capture(lambda: msgbox.confirm("t", "m", 5)) assert not flags & _DEFBUTTON2, hex(flags) assert flags & msgbox._MB_OKCANCEL, hex(flags) @case def confirm_default_cancel_makes_cancel_the_default_button(): _, flags = _capture( lambda: msgbox.confirm("t", "m", 5, default_cancel=True)) assert flags & _DEFBUTTON2, hex(flags) # Still the same OK/Cancel warning box, on top of the browser. assert flags & msgbox._MB_OKCANCEL, hex(flags) assert flags & msgbox._MB_ICONWARN, hex(flags) assert flags & msgbox._MB_TOPMOST, hex(flags) @case def ok_cancel_passes_default_cancel_through(): _, flags = _capture(lambda: msgbox.ok_cancel("t", "m", default_cancel=True)) assert flags & _DEFBUTTON2, hex(flags) @case def ok_cancel_default_is_unchanged_for_existing_callers(): """captcha.py and login.py call ok_cancel(title, msg) - no Enter change.""" _, flags = _capture(lambda: msgbox.ok_cancel("t", "m")) assert not flags & _DEFBUTTON2, hex(flags) @case def ok_cancel_default_cancel_still_true_only_on_ok(): """The default button changes; what each answer means does not.""" ok, _ = _capture(lambda: msgbox.ok_cancel("t", "m", default_cancel=True), rc=_IDOK) assert ok is True, ok cancel, _ = _capture( lambda: msgbox.ok_cancel("t", "m", default_cancel=True), rc=_IDCANCEL) assert cancel is False, cancel timed_out, _ = _capture( lambda: msgbox.ok_cancel("t", "m", default_cancel=True), rc=msgbox._IDTIMEOUT) assert timed_out is False, timed_out @case def info_box_never_gets_defbutton2(): _, flags = _capture(lambda: msgbox.info("t", "m", 5), rc=_IDOK) assert not flags & _DEFBUTTON2, hex(flags) # ── [TEST] title prefix (review item D9, 2026-10-05) ───────────────────────── # A TEST_CLAIM_IDS_FILE run drives live eCW too, so every box it shows says # so in its title. Applied in the raw info() / confirm() (and so in the # info_box / ok_cancel wrappers built on them): the 2FA manual box, "Run the # Lookup?" and the export report box call the raw ones directly, and the 2FA # box is reached at login in a test run (review fix round 1, 2026-10-05). def _captions(fn, test_file, rc=_IDOK): seen = [] orig_raw, orig_file = msgbox._raw_call, msgbox.cfg.TEST_CLAIM_IDS_FILE msgbox._raw_call = lambda text, caption, flags, timeout_s: ( seen.append(caption) or rc) msgbox.cfg.TEST_CLAIM_IDS_FILE = test_file try: fn() finally: msgbox._raw_call = orig_raw msgbox.cfg.TEST_CLAIM_IDS_FILE = orig_file return seen @case def info_box_title_says_test_in_test_mode(): seen = _captions(lambda: msgbox.info_box("eCW Bot — Claim 1", "m"), "C:/x/Standard Claims.xlsx") assert seen == ["[TEST] eCW Bot — Claim 1"], seen @case def ok_cancel_title_says_test_in_test_mode(): seen = _captions(lambda: msgbox.ok_cancel("eCW Bot — Save?", "m", default_cancel=True), "C:/x/Standard Claims.xlsx", rc=_IDCANCEL) assert seen == ["[TEST] eCW Bot — Save?"], seen @case def live_run_titles_are_unchanged(): seen = _captions(lambda: (msgbox.info_box("A", "m"), msgbox.ok_cancel("B", "m")), "") assert seen == ["A", "B"], seen @case def raw_confirm_title_says_test_in_test_mode(): """auth/mailbox.py's 2FA manual box calls msgbox.confirm directly.""" seen = _captions(lambda: msgbox.confirm("eCW Bot — 2FA (Salud)", "m", 5), "C:/x/Standard Claims.xlsx", rc=_IDCANCEL) assert seen == ["[TEST] eCW Bot — 2FA (Salud)"], seen @case def raw_info_title_says_test_in_test_mode(): """pages/claim_report.py's export report box calls msgbox.info directly.""" seen = _captions(lambda: msgbox.info("eCW Bot — EXCEL report", "m", 5), "C:/x/Standard Claims.xlsx") assert seen == ["[TEST] eCW Bot — EXCEL report"], seen @case def raw_boxes_are_unchanged_on_a_live_run(): seen = _captions(lambda: (msgbox.info("A", "m", 5), msgbox.confirm("B", "m", 5)), "") assert seen == ["A", "B"], seen @case def test_prefix_is_never_doubled(): # The wrappers apply it and so do the raw functions under them. seen = _captions(lambda: (msgbox.info_box("[TEST] A", "m"), msgbox.info_box("B", "m"), msgbox.ok_cancel("C", "m")), "x.xlsx") assert seen == ["[TEST] A", "[TEST] B", "[TEST] C"], seen @case def quiet_boxes_send_an_info_box_to_the_log_not_the_screen(): import io import contextlib shown = [] # TEST_CLAIM_IDS_FILE cleared: the operator's .env sets it, and then # titled() puts "[TEST] " in front of the title. orig = (msgbox._raw_call, msgbox.cfg.QUIET_BOXES, msgbox.cfg.TEST_CLAIM_IDS_FILE) msgbox._raw_call = lambda *a: shown.append(a) or 1 msgbox.cfg.QUIET_BOXES = True msgbox.cfg.TEST_CLAIM_IDS_FILE = "" out = io.StringIO() try: with contextlib.redirect_stdout(out): msgbox.info_box("eCW Bot — Claim 1", "Summary : Pass\nmore") rc = msgbox.info("eCW Bot — CSV report", "Saved.", 5) finally: (msgbox._raw_call, msgbox.cfg.QUIET_BOXES, msgbox.cfg.TEST_CLAIM_IDS_FILE) = orig assert shown == [], shown assert rc == msgbox.OK text = out.getvalue() assert "[BOX] eCW Bot — Claim 1: Summary : Pass" in text, text assert "more" not in text, text @case def quiet_boxes_still_show_a_box_that_needs_a_person(): shown = [] orig = (msgbox._raw_call, msgbox.cfg.QUIET_BOXES) msgbox._raw_call = lambda *a: shown.append(a) or 1 msgbox.cfg.QUIET_BOXES = True try: msgbox.info_box("eCW Bot — Security Image", "Click Continue", needs_person=True) msgbox.confirm("eCW Bot — CAPTCHA", "Type it", 5) finally: msgbox._raw_call, msgbox.cfg.QUIET_BOXES = orig assert len(shown) == 2, shown @case def without_quiet_boxes_an_info_box_is_shown(): shown = [] orig = (msgbox._raw_call, msgbox.cfg.QUIET_BOXES) msgbox._raw_call = lambda *a: shown.append(a) or 1 msgbox.cfg.QUIET_BOXES = False try: msgbox.info_box("eCW Bot — Claim 1", "Summary : Pass") finally: msgbox._raw_call, msgbox.cfg.QUIET_BOXES = orig assert len(shown) == 1, shown def main() -> int: failures = 0 for fn in CASES: name = fn.__name__.replace("_", " ") try: fn() print(f" ok {name}") except AssertionError as exc: print(f" FAIL {name}: {exc}") failures += 1 except Exception as exc: print(f" ERROR {name}: {type(exc).__name__}: {exc}") failures += 1 print(f"\n{len(CASES) - failures}/{len(CASES)} passed") return 1 if failures else 0 if __name__ == "__main__": sys.exit(main())