Initial import: eCW Charge Validation bot and results Portal
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>
This commit is contained in:
commit
ba15a574ab
205 files changed
+56772
No files matched your search
@@ -0,0 +1,175 @@
|
||||
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())
|
||||
Reference in new issue
Block a user