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:
ManikandanandClaude Opus 5.5 committed 2026-10-23 18:38:17 +05:30
commit ba15a574ab
205 files changed
+56772

No files matched your search

+410
View File
@@ -0,0 +1,410 @@
r"""
Every claim starts from a clean search page (operator, 2026-10-12): what
the last claim left open is closed, Claim No and Lookup are checked to be
on screen with nothing over them, and only then is the claim searched. A
page that will not come clean makes that ONE claim Review — the next claim
clears and checks again. Live run that day: claim 1's viewer stayed open
and claims 2 and 3 were never searched.
python tests\test_clean_between_claims.py
"""
import io
import sys
import pathlib
import inspect
import contextlib
ROOT = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "src"))
FIXTURES = pathlib.Path(__file__).resolve().parent / "fixtures"
HCFA = FIXTURES / "hcfa_claim.html"
from ecw_bot import settings as cfg # noqa: E402
from ecw_bot.flows import export_medcor_ready as flow # noqa: E402
from ecw_bot.pages import claim_form, claim_results # noqa: E402
from ecw_bot.selectors import claim_lookup as lsel # noqa: E402
CASES = []
def case(fn):
CASES.append(fn)
return fn
def _quiet(fn, *a, **k):
with contextlib.redirect_stdout(io.StringIO()) as out:
result = fn(*a, **k)
return result, out.getvalue()
# The two controls the search needs, as eCW's Claim Lookup has them.
_LOOKUP = """
<div id="filters" style="padding:20px">
<input id="claimLookupIpt10" ng-model="_InvId" style="width:200px">
<button id="btnclaimlookup">Lookup</button>
</div>
"""
# A viewer-like dialog over the whole page, with a patient's name in it.
_COVER = """
<div class="modal in" id="pnViewer" style="display:block;position:fixed;
left:0;top:0;width:100%;height:100%;z-index:1050;background:#fff">
PATIENT DOE, JANE</div>
"""
# ── the check before the search ──────────────────────────────────────────────
@case
def a_clean_search_page_is_ready(page):
page.set_content(_LOOKUP)
assert claim_results.search_blocker(page) is None
@case
def a_dialog_over_lookup_is_named_without_its_text(page):
page.set_content(_LOOKUP + _COVER)
why = claim_results.search_blocker(page)
assert why and "covered by" in why and "pnViewer" in why, why
assert "DOE" not in why and "JANE" not in why, why
@case
def a_missing_lookup_button_is_named(page):
page.set_content('<input id="claimLookupIpt10">')
why = claim_results.search_blocker(page)
assert why == "Lookup is not on screen", why
@case
def a_disabled_lookup_button_is_named(page):
page.set_content(_LOOKUP.replace("<button id", "<button disabled id"))
assert claim_results.search_blocker(page) == "Lookup is disabled"
@contextlib.contextmanager
def _ready_wait(ms):
"""The search-ready wait shortened for a test that has to sit it out."""
orig = claim_results._SEARCH_READY_MS
claim_results._SEARCH_READY_MS = ms
try:
yield
finally:
claim_results._SEARCH_READY_MS = orig
@case
def search_claim_types_nothing_when_the_page_is_not_ready(page):
page.set_content(_LOOKUP + _COVER)
with _ready_wait(1_000):
try:
_quiet(claim_results.search_claim, page, "9103000")
except RuntimeError as exc:
assert "not ready after 1s" in str(exc), exc
assert "covered by" in str(exc), exc
else:
raise AssertionError("search_claim went ahead over a dialog")
assert page.locator(lsel.SEL_CLAIM_NO).input_value() == ""
@case
def a_page_taking_no_clicks_is_still_loading_not_covered(page):
"""Live run 2026-10-13, claim 9126000: eCW loading, the click fell
through to <html> — logged as "covered by html.jelly-page"."""
page.set_content(_LOOKUP)
page.evaluate("() => { document.body.style.pointerEvents = 'none'; }")
why = claim_results.search_blocker(page)
assert why == "Claim No is not taking clicks yet (eCW still loading)", why
@case
def the_route_spinner_on_screen_means_still_loading(page):
page.set_content(_LOOKUP + '<div id="routeLoader" '
'style="width:40px;height:40px">...</div>')
why = claim_results.search_blocker(page)
assert why == "eCW is still loading (its spinner is on screen)", why
@case
def a_page_busy_for_a_moment_is_ready_once_it_comes_free(page):
"""The claim is searched, not sent to Review, when the page clears
within the wait."""
page.set_content(_LOOKUP + _COVER)
page.evaluate("() => setTimeout(() => "
"document.getElementById('pnViewer').remove(), 1200)")
why, out = _quiet(claim_results.wait_search_ready, page, "9103000")
assert why is None, (why, out)
assert "not ready yet" in out and "came free after" in out, out
assert "DOE" not in out and "JANE" not in out, out
@case
def a_page_that_stays_busy_gives_its_last_reason_after_the_wait(page):
page.set_content(_LOOKUP + _COVER)
with _ready_wait(1_000):
why, _ = _quiet(claim_results.wait_search_ready, page, "9103000")
assert why and "covered by" in why and "pnViewer" in why, why
# ── clearing the screen ──────────────────────────────────────────────────────
@case
def a_clean_page_needs_nothing_and_says_nothing(page):
page.set_content(_LOOKUP)
done, out = _quiet(claim_form.clear_screen, page, "9103000",
page.context.pages)
assert done == [] and out == "", (done, out)
@case
def a_tab_opened_during_a_claim_is_closed_and_the_bot_tabs_kept(page):
page.set_content(_LOOKUP)
keep = list(page.context.pages)
reader = page.context.new_page() # e.g. a note reader tab
done, out = _quiet(claim_form.clear_screen, page, "9103000", keep)
assert reader.is_closed(), out
assert not page.is_closed()
assert "an extra browser tab" in done, (done, out)
@case
def a_claim_form_left_open_is_cancelled(page):
page.goto(HCFA.as_uri())
assert claim_form.any_claim_form_open(page)
done, out = _quiet(claim_form.clear_screen, page, "9103000",
page.context.pages)
assert "a claim form" in done, (done, out)
assert not claim_form.any_claim_form_open(page), out
@case
def a_leftover_form_is_described_in_the_log_before_it_is_cancelled(page):
page.goto(HCFA.as_uri())
_, out = _quiet(claim_form.clear_screen, page, "9103000",
page.context.pages)
assert "Open form found:" in out, out
assert "a click there hits" in out, out
@case
def a_ghost_form_says_how_it_is_hidden_and_what_takes_its_click(page):
"""The evidence for a form the bot sees open that a person does not."""
page.set_content("""
<div class="modal fade claimModalBackdrop" id="ghost"
style="display:block;opacity:0;position:fixed;left:0;top:0;
width:400px;height:200px">
<button ng-click="closeClaimScreen()" style="margin:40px">Cancel
</button>PATIENT DOE, JANE</div>
<div id="cover" style="position:fixed;left:0;top:0;width:400px;
height:200px;z-index:5">grid</div>
""")
lines = claim_form.describe_shown(page, claim_form.sel.SEL_CANCEL_HCFA)
assert len(lines) == 1, lines
line = lines[0]
assert "opacity 0" in line, line
assert "div#ghost" in line, line
assert "hits div#cover" in line, line
assert "DOE" not in line and "JANE" not in line, line
# ── a claim window is Cancelled, never Escaped ───────────────────────────────
# Escape drops the HCFA window without closeClaimScreen() (the fixture's own
# keydown handler does what uib does live); `closeClaimScreenRan` tells a
# real Cancel apart from that.
class _Stop(Exception):
pass
def _clear_with_cancel_failing(page):
orig = claim_form._close_leftover_form
def will_not_close(page, claim_id):
raise RuntimeError("would not close")
claim_form._close_leftover_form = will_not_close
try:
return _quiet(claim_form.clear_screen, page, "9103000",
page.context.pages)
finally:
claim_form._close_leftover_form = orig
@case
def clear_screen_cancels_a_claim_form_and_does_not_escape_it(page):
page.goto(HCFA.as_uri())
done, out = _quiet(claim_form.clear_screen, page, "9103000",
page.context.pages)
assert "a claim form" in done, (done, out)
assert page.evaluate("window.closeClaimScreenRan") is True, out
assert "LAST RESORT" not in out, out
@case
def an_hcfa_form_cancel_will_not_close_is_escaped_as_the_last_resort(page):
"""Operator, 2026-10-14: left up, it covered the search page and every
later claim of the run went to Review."""
page.goto(HCFA.as_uri())
done, out = _clear_with_cancel_failing(page)
assert "LAST RESORT" in out and "still up (would not close)" in out, out
assert "closeClaimScreen() does NOT run" in out, out
assert "a claim form" in done, (done, out)
assert not claim_form.any_claim_form_open(page), out
assert page.evaluate("window.closeClaimScreenRan") is False
@case
def a_ub_form_cancel_will_not_close_is_not_escaped(page):
page.goto((FIXTURES / "ub_claim.html").as_uri())
assert claim_form.any_claim_form_open(page)
done, out = _clear_with_cancel_failing(page)
assert "LAST RESORT" not in out, out
assert "Could not close a claim form" in out, out
assert "a claim form" not in done, (done, out)
assert claim_form.any_claim_form_open(page), out
@case
def open_claim_cancels_a_form_already_up_and_never_escapes_it(page):
"""eCW opens the next HCFA claim's form by itself after the Lookup (live
run 2026-10-13): open_claim()'s stray-panel check came first and would
have Escaped it."""
page.goto(HCFA.as_uri())
orig = claim_form._close_leftover_form
def then_stop(page, claim_id):
orig(page, claim_id)
raise _Stop()
claim_form._close_leftover_form = then_stop
try:
_quiet(claim_form.open_claim, page, "9103000")
except _Stop:
pass
finally:
claim_form._close_leftover_form = orig
assert page.evaluate("window.closeClaimScreenRan") is True, \
"the form was gone before _close_leftover_form() could Cancel it"
assert not claim_form.any_claim_form_open(page)
# ── the loop ─────────────────────────────────────────────────────────────────
def _run(ids, search, events=None):
"""
`_process_claims` with the page stubbed out. Clears are appended to
`events` (the search can append to the same list); returns it and the
outcomes.
"""
events = [] if events is None else events
outcomes = {}
orig = (cfg.SHOW_EACH_CLAIM, cfg.SHOW_CLAIM_IDS, cfg.CLAIM_PROCESS_LIMIT,
flow.claim_results.search_claim, flow._read_claim_form,
flow.claim_filters.clear_claim_no, flow._write_charge_summary,
flow._save_results_db, flow.claim_form.clear_screen)
cfg.SHOW_EACH_CLAIM = False
cfg.SHOW_CLAIM_IDS = False
cfg.CLAIM_PROCESS_LIMIT = 0
flow.claim_results.search_claim = search
flow._read_claim_form = lambda page, cid, head: {
"claim_no": cid,
"cpt": [{"code": "99213", "checked": True, "fee": "100.00"}],
"icd": [{"code": "Z00.00"}],
"progress_notes": {"opened": "modal", "text": "Office visit."},
"_conditions": {"fired": [], "saved": False, "failed": [],
"review": [], "comments": {}}}
flow.claim_filters.clear_claim_no = lambda page: None
flow._write_charge_summary = lambda info, o: outcomes.update(o)
flow._save_results_db = lambda info, o, d: None
flow.claim_form.clear_screen = (
lambda page, cid, keep=(): events.append(("clear", cid)) or [])
try:
with contextlib.redirect_stdout(io.StringIO()):
flow._process_claims(None, {"ids": list(ids), "file": None})
finally:
(cfg.SHOW_EACH_CLAIM, cfg.SHOW_CLAIM_IDS, cfg.CLAIM_PROCESS_LIMIT,
flow.claim_results.search_claim, flow._read_claim_form,
flow.claim_filters.clear_claim_no, flow._write_charge_summary,
flow._save_results_db, flow.claim_form.clear_screen) = orig
return events, outcomes
@case
def every_claim_is_cleared_before_its_search_and_the_run_ends_clear():
events = []
def search(page, cid):
events.append(("search", cid))
return {"id": cid, "count": 1, "found": True, "seconds": 1.0}
_run(["1", "2"], search, events)
assert events == [("clear", "1"), ("search", "1"),
("clear", "2"), ("search", "2"),
("clear", "2")], events
@case
def a_page_that_will_not_clear_costs_one_claim_not_the_rest():
searched = []
def search(page, cid):
if cid == "1":
raise RuntimeError("The search page is not ready: Lookup is "
"covered by div#pnViewer.modal")
searched.append(cid)
return {"id": cid, "count": 1, "found": True, "seconds": 1.0}
_, outcomes = _run(["1", "2", "3"], search)
assert searched == ["2", "3"], searched
assert outcomes["1"]["Category"] == "Review", outcomes["1"]
assert "not ready" in outcomes["1"]["Comments"], outcomes["1"]
assert outcomes["2"]["Category"] == "Pass", outcomes["2"]
assert outcomes["3"]["Category"] == "Pass", outcomes["3"]
def _run_cases() -> int:
from playwright.sync_api import sync_playwright
failures = 0
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=True)
# A context of its own, so a test can open a second tab in it.
page = browser.new_context(
viewport={"width": 1600, "height": 1000}).new_page()
for fn in CASES:
name = fn.__name__.replace("_", " ")
try:
if len(inspect.signature(fn).parameters) == 0:
fn()
else:
fn(page)
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
browser.close()
print(f"\n{len(CASES) - failures}/{len(CASES)} passed")
return 1 if failures else 0
def main() -> int:
"""
shot() writes into the operator's dated Screenshots\\ folder; a test
run must not, so the cases run with it pointed at a temp folder.
"""
import tempfile
from ecw_bot import settings as _cfg
orig = _cfg.SCREENSHOTS
with tempfile.TemporaryDirectory() as tmp:
_cfg.SCREENSHOTS = pathlib.Path(tmp)
try:
return _run_cases()
finally:
_cfg.SCREENSHOTS = orig
if __name__ == "__main__":
sys.exit(main())