Snapshot of the working project as of 2026-10-23 (local 2d4d4fc), one commit with no earlier history: the tracked files were de-identified first (synthetic claim ids and patient data only; see Context.md section 7). Credentials, run logs, screenshots, claim reports and client workbooks are gitignored and not included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
176 lines
5.5 KiB
Python
176 lines
5.5 KiB
Python
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("<p>x</p>")
|
|
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())
|