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,304 @@
|
||||
r"""
|
||||
pages/preflight.py and the DOM capture in core/page_utils.shot (2026-10-21):
|
||||
a changed eCW screen is found on the first claim, named, its HTML saved,
|
||||
and the run stopped (exit 7) — not every claim sent to Review one by one.
|
||||
|
||||
python tests\test_preflight.py
|
||||
|
||||
Screenshots and DOM files go to a temporary folder, never the operator's.
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
import inspect
|
||||
import pathlib
|
||||
import tempfile
|
||||
import contextlib
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
FIXTURES = pathlib.Path(__file__).resolve().parent / "fixtures"
|
||||
|
||||
from ecw_bot import settings as cfg # noqa: E402
|
||||
from ecw_bot.core import page_utils # noqa: E402
|
||||
from ecw_bot.pages import preflight # noqa: E402
|
||||
from ecw_bot.pages import claim_form # noqa: E402
|
||||
from ecw_bot.flows import export_medcor_ready as flow # noqa: E402
|
||||
|
||||
CASES = []
|
||||
|
||||
SEARCH_PAGE = """
|
||||
<input id="claimLookupIpt10"><button id="btnclaimlookup">Lookup</button>
|
||||
<select id="claimStatusCodeId"><option>Medcor Ready</option></select>
|
||||
<input id="fromdate"><input id="todate">
|
||||
"""
|
||||
|
||||
|
||||
def case(fn):
|
||||
CASES.append(fn)
|
||||
return fn
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _sandbox(**settings):
|
||||
"""Shots in a temp folder; settings set for the case, then put back."""
|
||||
keys = ["SCREENSHOTS", "PREFLIGHT", "PREFLIGHT_STOP",
|
||||
"SAVE_DOM_ON_FAILURE", "SAVE_DOM_LIMIT"]
|
||||
old = {k: getattr(cfg, k) for k in keys}
|
||||
old_saved = page_utils._dom_saved
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
cfg.SCREENSHOTS = pathlib.Path(tmp)
|
||||
cfg.PREFLIGHT, cfg.PREFLIGHT_STOP = True, True
|
||||
cfg.SAVE_DOM_ON_FAILURE, cfg.SAVE_DOM_LIMIT = True, 30
|
||||
for k, v in settings.items():
|
||||
setattr(cfg, k, v)
|
||||
page_utils._dom_saved = 0
|
||||
preflight.reset()
|
||||
try:
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
yield pathlib.Path(tmp)
|
||||
finally:
|
||||
for k, v in old.items():
|
||||
setattr(cfg, k, v)
|
||||
page_utils._dom_saved = old_saved
|
||||
preflight.reset()
|
||||
|
||||
|
||||
def _files(folder, suffix):
|
||||
return sorted(p.name for p in folder.glob(f"*{suffix}"))
|
||||
|
||||
|
||||
# ── DOM capture in shot() ─────────────────────────────────────────────────────
|
||||
|
||||
@case
|
||||
def a_failure_shot_saves_the_dom_beside_the_png(page):
|
||||
page.set_content("<div id='x'>fake page</div>"
|
||||
"<iframe srcdoc='<p id=inner>frame text</p>'></iframe>")
|
||||
page.wait_for_timeout(200)
|
||||
with _sandbox() as tmp:
|
||||
page_utils.shot(page, "claim_form_no_modal_C1")
|
||||
pngs, htmls = _files(tmp, ".png"), _files(tmp, ".html")
|
||||
assert len(pngs) == 1 and len(htmls) == 1, (pngs, htmls)
|
||||
assert htmls[0].endswith("_claim_form_no_modal_C1.html")
|
||||
assert htmls[0][:6] == pngs[0][:6], "same HHMMSS"
|
||||
text = (tmp / htmls[0]).read_text(encoding="utf-8")
|
||||
assert "fake page" in text and "frame 1" in text \
|
||||
and "frame text" in text, text[:300]
|
||||
|
||||
|
||||
@case
|
||||
def an_ordinary_shot_saves_no_dom(page):
|
||||
page.set_content("<div>ok</div>")
|
||||
with _sandbox() as tmp:
|
||||
for tag in ("logged_in", "landing", "claims_status",
|
||||
"progress_note", "claims_balance_cleared"):
|
||||
page_utils.shot(page, tag)
|
||||
assert _files(tmp, ".html") == []
|
||||
assert len(_files(tmp, ".png")) >= 1
|
||||
|
||||
|
||||
@case
|
||||
def dom_capture_can_be_off_and_is_capped(page):
|
||||
page.set_content("<div>x</div>")
|
||||
with _sandbox(SAVE_DOM_ON_FAILURE=False) as tmp:
|
||||
page_utils.shot(page, "claims_status_failed")
|
||||
assert _files(tmp, ".html") == []
|
||||
with _sandbox(SAVE_DOM_LIMIT=2) as tmp:
|
||||
for n in range(4):
|
||||
page_utils.shot(page, f"x{n}_failed")
|
||||
assert page_utils._dom_saved == 2
|
||||
|
||||
|
||||
@case
|
||||
def failure_tags_are_told_from_ordinary_ones():
|
||||
for tag in ("claims_status_failed", "login_unconfirmed",
|
||||
"search_not_ready_1", "claim_notfound_1",
|
||||
"claim_form_wrong_claim_1", "claim_form_cpt_grid_missing",
|
||||
"claims_report_nodownload", "2fa_timed_out",
|
||||
"preflight_failed_search_page"):
|
||||
assert page_utils.is_failure_tag(tag), tag
|
||||
for tag in ("logged_in", "landing", "captcha", "approval_page",
|
||||
"claims_results", "nav_claims", "progress_note",
|
||||
"claims_balance_cleared", "claims_datetype_open"):
|
||||
assert not page_utils.is_failure_tag(tag), tag
|
||||
|
||||
|
||||
# ── the search page ──────────────────────────────────────────────────────────
|
||||
|
||||
@case
|
||||
def the_search_page_passes_with_every_selector(page):
|
||||
page.set_content(SEARCH_PAGE)
|
||||
with _sandbox() as tmp:
|
||||
preflight.check_search(page)
|
||||
assert _files(tmp, ".png") == []
|
||||
|
||||
|
||||
@case
|
||||
def a_missing_search_selector_stops_the_run_named_and_saved(page):
|
||||
page.set_content(SEARCH_PAGE.replace('id="btnclaimlookup"', 'id="x"'))
|
||||
with _sandbox() as tmp:
|
||||
try:
|
||||
preflight.check_search(page)
|
||||
except preflight.PreflightFailed as exc:
|
||||
assert "Lookup button" in str(exc), exc
|
||||
else:
|
||||
raise AssertionError("no PreflightFailed")
|
||||
assert any("preflight_failed_search_page" in n
|
||||
for n in _files(tmp, ".html")), _files(tmp, ".html")
|
||||
|
||||
|
||||
@case
|
||||
def preflight_stop_off_only_warns_and_preflight_off_does_nothing(page):
|
||||
page.set_content("<p>not the search page</p>")
|
||||
with _sandbox(PREFLIGHT_STOP=False) as tmp:
|
||||
preflight.check_search(page)
|
||||
assert _files(tmp, ".png"), "still shot"
|
||||
with _sandbox(PREFLIGHT=False) as tmp:
|
||||
preflight.check_search(page)
|
||||
assert _files(tmp, ".png") == []
|
||||
|
||||
|
||||
@case
|
||||
def no_page_at_all_is_skipped_never_raised():
|
||||
with _sandbox():
|
||||
preflight.check_search(None)
|
||||
preflight.check_form(None)
|
||||
|
||||
|
||||
# ── the claim form ───────────────────────────────────────────────────────────
|
||||
|
||||
def _open(page, name):
|
||||
page.goto((FIXTURES / name).as_uri())
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
claim_form.open_cpt_hcpcs_tab(page)
|
||||
|
||||
|
||||
@case
|
||||
def each_fixture_form_passes(page):
|
||||
for name in ("ub_claim.html", "hcfa_claim.html", "dental_claim.html"):
|
||||
_open(page, name)
|
||||
with _sandbox() as tmp:
|
||||
preflight.check_form(page)
|
||||
assert _files(tmp, ".png") == [], name
|
||||
|
||||
|
||||
@case
|
||||
def a_missing_row_box_names_it(page):
|
||||
_open(page, "ub_claim.html")
|
||||
page.evaluate("document.querySelectorAll('[id^=\"uBClaimMod1\"]')"
|
||||
".forEach(e => e.remove())")
|
||||
with _sandbox():
|
||||
try:
|
||||
preflight.check_form(page)
|
||||
except preflight.PreflightFailed as exc:
|
||||
assert "ub claim form" in str(exc) and "M1 box" in str(exc), exc
|
||||
else:
|
||||
raise AssertionError("no PreflightFailed")
|
||||
|
||||
|
||||
@case
|
||||
def each_screen_is_checked_once_a_run(page):
|
||||
_open(page, "hcfa_claim.html")
|
||||
with _sandbox():
|
||||
preflight.check_form(page)
|
||||
page.evaluate("document.querySelectorAll('[data-fieldname="
|
||||
"\"ClaimCPTPOS\"]').forEach(e => e.remove())")
|
||||
preflight.check_form(page) # not checked again: no raise
|
||||
preflight.reset()
|
||||
try:
|
||||
preflight.check_form(page)
|
||||
except preflight.PreflightFailed as exc:
|
||||
assert "POS box" in str(exc), exc
|
||||
else:
|
||||
raise AssertionError("a new run checks again")
|
||||
|
||||
|
||||
@case
|
||||
def a_form_with_no_rows_waits_then_fails_on_the_third(page):
|
||||
_open(page, "hcfa_claim.html")
|
||||
page.evaluate("document.querySelectorAll('tr[id^=\"CPTROW_\"]')"
|
||||
".forEach(e => e.remove())")
|
||||
with _sandbox():
|
||||
preflight.check_form(page)
|
||||
preflight.check_form(page)
|
||||
try:
|
||||
preflight.check_form(page)
|
||||
except preflight.PreflightFailed as exc:
|
||||
assert "CPT rows" in str(exc), exc
|
||||
else:
|
||||
raise AssertionError("three claims with no rows should fail")
|
||||
|
||||
|
||||
# ── the flow ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@case
|
||||
def the_flow_lets_preflight_failed_through_and_still_closes_the_form():
|
||||
closed = []
|
||||
|
||||
def boom(page):
|
||||
raise preflight.PreflightFailed("eCW ub claim form: not found — M1")
|
||||
|
||||
stubs = {"open_claim": lambda page, cid: True,
|
||||
"wait_claim_loaded": lambda page, cid: None,
|
||||
"close_duplicate_forms": lambda page, cid: None,
|
||||
"is_dental_claim_form": lambda page: False,
|
||||
"open_cpt_hcpcs_tab": lambda page: None,
|
||||
"close_claim": lambda page: closed.append(1) or True}
|
||||
orig = {k: getattr(claim_form, k) for k in stubs}
|
||||
orig_check, orig_open = preflight.check_form, cfg.OPEN_CLAIM_FORM
|
||||
for k, v in stubs.items():
|
||||
setattr(claim_form, k, v)
|
||||
preflight.check_form = boom
|
||||
cfg.OPEN_CLAIM_FORM = True
|
||||
try:
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
try:
|
||||
flow._read_claim_form(None, "C1")
|
||||
except preflight.PreflightFailed:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("PreflightFailed was swallowed")
|
||||
assert closed == [1], "the form is still closed"
|
||||
finally:
|
||||
for k, v in orig.items():
|
||||
setattr(claim_form, k, v)
|
||||
preflight.check_form, cfg.OPEN_CLAIM_FORM = orig_check, orig_open
|
||||
|
||||
|
||||
@case
|
||||
def the_web_page_explains_exit_7():
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from web import run_status
|
||||
assert "selector" in run_status.exit_text(7)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
failures = 0
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=True)
|
||||
page = browser.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
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user