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>
305 lines
11 KiB
Python
305 lines
11 KiB
Python
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())
|