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 = """ """ 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("
fake page
" "") 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("
ok
") 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("
x
") 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("

not the search page

") 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())