r""" `core/page_utils.auto_dialog` — how the bot answers a page's native dialog (fixed 2026-10-22; Context.md §10 had it as an open finding). python tests\test_auto_dialog.py The old handler called `dialog.message()`, but in Playwright Python `message` is a property: the TypeError was swallowed and every alert / confirm was left open — and an unanswered dialog crashed the tab ("Target crashed", seen in tests). What has to hold now: * **Every dialog is answered**, so the tab never wedges or crashes. * **alert** → OK: it only informs. * **confirm / prompt** → OK only in LIVE-WRITE (SAVE_CORRECTIONS=1). In VERIFY → Cancel: live eCW is read-only, and a confirm's OK may be a save. * **beforeunload** → dismissed: accepting means "yes, close this page". * Each one is in the log and is a Warning on the Runs page, with what the bot answered. * A second handler for the same dialog (it is registered on the context and on the page) does not raise. Fake dialogs for the rules, then a real Chromium for the crash. """ import io import sys import pathlib import contextlib ROOT = pathlib.Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "src")) from ecw_bot import settings as cfg # noqa: E402 from ecw_bot.core import page_utils # noqa: E402 CASES = [] def case(fn): CASES.append(fn) return fn class FakeDialog: """`type` and `message` as properties, like Playwright's.""" def __init__(self, kind, message="Are you sure?", raises=False): self._kind, self._message, self.raises = kind, message, raises self.answer = None @property def type(self): return self._kind @property def message(self): return self._message def accept(self, prompt_text=None): if self.raises: raise RuntimeError("Cannot accept dialog which is already handled") self.answer = "accept" def dismiss(self): if self.raises: raise RuntimeError("Cannot dismiss dialog which is already handled") self.answer = "dismiss" def _answer(kind, save=False, **kw): saved = cfg.SAVE_CORRECTIONS cfg.SAVE_CORRECTIONS = save d = FakeDialog(kind, **kw) out = io.StringIO() try: with contextlib.redirect_stdout(out): page_utils.auto_dialog(d) finally: cfg.SAVE_CORRECTIONS = saved return d.answer, out.getvalue() @case def an_alert_is_accepted_in_both_modes(): assert _answer("alert")[0] == "accept" assert _answer("alert", save=True)[0] == "accept" @case def a_confirm_or_prompt_is_cancelled_in_verify(): assert _answer("confirm")[0] == "dismiss" assert _answer("prompt")[0] == "dismiss" @case def a_confirm_or_prompt_is_accepted_in_live_write(): assert _answer("confirm", save=True)[0] == "accept" assert _answer("prompt", save=True)[0] == "accept" @case def beforeunload_is_always_dismissed(): assert _answer("beforeunload")[0] == "dismiss" assert _answer("beforeunload", save=True)[0] == "dismiss" @case def the_log_says_what_it_was_and_what_the_bot_answered(): _, out = _answer("confirm", message="Save changes?") assert "[DIALOG] confirm: 'Save changes?' -> Cancel" in out, out _, out = _answer("alert", message="Done") assert "-> OK" in out, out @case def an_already_answered_dialog_does_not_raise(): _answer("confirm", raises=True) # would raise out of the handler def _real_browser_cases() -> int: from playwright.sync_api import sync_playwright failures = 0 saved = cfg.SAVE_CORRECTIONS with sync_playwright() as pw: browser = pw.chromium.launch(headless=True) try: for save, want in ((False, False), (True, True)): name = (f"a real confirm answers {want} " f"in {'LIVE-WRITE' if save else 'VERIFY'}, no crash") cfg.SAVE_CORRECTIONS = save ctx = browser.new_context() # The way core/browser.py wires it: context AND page. ctx.on("page", lambda p: p.on("dialog", page_utils.auto_dialog)) page = ctx.new_page() page.on("dialog", page_utils.auto_dialog) page.set_default_timeout(10_000) page.set_content("
x
") try: with contextlib.redirect_stdout(io.StringIO()): page.evaluate("alert('note')") got = page.evaluate("confirm('Save changes?')") assert got is want, got assert page.evaluate("1 + 1") == 2, "tab still alive" print(f" ok {name}") except Exception as exc: print(f" FAIL {name}: {type(exc).__name__}: {exc}") failures += 1 ctx.close() finally: cfg.SAVE_CORRECTIONS = saved browser.close() return failures 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 failures += _real_browser_cases() total = len(CASES) + 2 print(f"\n{total - failures}/{total} passed") return 1 if failures else 0 if __name__ == "__main__": sys.exit(main())