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,690 @@
|
||||
r"""
|
||||
Waiting for the HCFA and Dental forms to finish loading (review item D7).
|
||||
|
||||
python tests\test_claim_loaded.py
|
||||
|
||||
Seen live (run_20260930_163006): `open_claim()` returned the moment the HCFA
|
||||
form's Cancel was visible, Angular had not filled the form yet, `extract()`
|
||||
read 0 CPT rows, Prog. Notes read "disabled" — and the claim went to the
|
||||
summary as Pass. `wait_claim_loaded()` waits for the claim number and for the
|
||||
service-line codes to be filled and steady; a timeout is a reason string on
|
||||
the claim's data, not an exception.
|
||||
|
||||
Local headless chromium against `tests\fixtures\hcfa_claim.html` and
|
||||
`dental_claim.html`, whose opt-in `?load=` / `?pn=` query strings simulate
|
||||
Angular filling the form late (or never, or with the wrong claim). No eCW.
|
||||
Screenshots go to a temp folder, never the real Screenshots\.
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
import time
|
||||
import pathlib
|
||||
import inspect
|
||||
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"
|
||||
HCFA = FIXTURES / "hcfa_claim.html"
|
||||
DENTAL = FIXTURES / "dental_claim.html"
|
||||
UB = FIXTURES / "ub_claim.html"
|
||||
|
||||
from ecw_bot import settings as cfg # noqa: E402
|
||||
from ecw_bot.pages import claim_form # noqa: E402
|
||||
from ecw_bot.flows import export_medcor_ready as flow # noqa: E402
|
||||
|
||||
CASES = []
|
||||
|
||||
|
||||
def case(fn):
|
||||
CASES.append(fn)
|
||||
return fn
|
||||
|
||||
|
||||
def _go(page, fixture, query=""):
|
||||
page.goto(fixture.as_uri() + (f"?{query}" if query else ""))
|
||||
|
||||
|
||||
def _timed(fn, *args, **kwargs):
|
||||
"""(result, seconds, captured stdout)."""
|
||||
buf = io.StringIO()
|
||||
started = time.monotonic()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
out = fn(*args, **kwargs)
|
||||
return out, time.monotonic() - started, buf.getvalue()
|
||||
|
||||
|
||||
# ── HCFA ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
@case
|
||||
def hcfa_already_filled_costs_about_half_a_second(page):
|
||||
"""The normal claim: the only cost is the 500 ms stability window."""
|
||||
_go(page, HCFA)
|
||||
out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000")
|
||||
assert out is None, out
|
||||
assert took < 1.5, f"took {took:.1f}s on a form that was already loaded"
|
||||
assert "HCFA form loaded" in log and "3 CPT rows" in log, log
|
||||
|
||||
|
||||
@case
|
||||
def hcfa_filled_late_is_waited_for(page):
|
||||
_go(page, HCFA, "load=delayed")
|
||||
# Proves the fixture: right now the form is blank, the way eCW serves it.
|
||||
assert claim_form.extract(page)["cpt"] == [], "fixture is not blank"
|
||||
out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000")
|
||||
assert out is None, out
|
||||
assert took >= 0.7, f"returned after {took:.2f}s — before the form filled"
|
||||
assert len(claim_form.extract(page)["cpt"]) == 3, "read short after wait"
|
||||
|
||||
|
||||
@case
|
||||
def hcfa_rows_filled_one_at_a_time_are_waited_for_until_they_hold_still(page):
|
||||
"""
|
||||
The stability rule (D7): the same non-zero count held for 500 ms. Angular
|
||||
fills the rows one digest at a time — here row 1 at 0.8 s, row 2 at
|
||||
1.1 s, row 3 at 1.4 s. A wait that took the first non-zero count would
|
||||
return at about 0.8-1.0 s and extract() would read 1 or 2 rows; only the
|
||||
500 ms window keeps it until 1.4 s + 0.5 s.
|
||||
"""
|
||||
_go(page, HCFA, "load=staggered")
|
||||
out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000")
|
||||
assert out is None, out
|
||||
assert took >= 1.9, f"returned after {took:.2f}s — rows still filling"
|
||||
assert len(claim_form.extract(page)["cpt"]) == 3, "read short after wait"
|
||||
assert "3 CPT rows" in log, log
|
||||
|
||||
|
||||
@case
|
||||
def hcfa_hidden_leftover_form_of_another_claim_is_not_read(page):
|
||||
"""
|
||||
eCW leaves closed forms in the DOM. A hidden copy of claim 5000002's form
|
||||
sits ahead of this one, and this form's claim number is still hidden
|
||||
while Angular fills it: the old document-wide read fell back to the
|
||||
hidden copy and raised a false "wrong claim" — the claim came back
|
||||
unread. Only the window holding the visible Cancel is read now.
|
||||
"""
|
||||
_go(page, HCFA, "load=delayed&stale=other")
|
||||
out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000")
|
||||
assert out is None, out
|
||||
assert took >= 0.7, f"returned after {took:.2f}s — before the form filled"
|
||||
assert "3 CPT rows" in log, log
|
||||
|
||||
|
||||
@case
|
||||
def hcfa_hidden_leftover_rows_do_not_count_as_loaded(page):
|
||||
"""The same hidden leftover, showing this claim's number and one filled
|
||||
row, in front of a form that never fills: its row must not make the
|
||||
blank form count as loaded (the old read returned None here)."""
|
||||
_go(page, HCFA, "load=never&stale=same")
|
||||
out, _, _ = _timed(claim_form.wait_claim_loaded, page, "9103000",
|
||||
timeout_ms=1_000)
|
||||
assert out == ("HCFA form did not finish loading in 1s "
|
||||
"(claim number never filled, 0 CPT rows)"), out
|
||||
|
||||
|
||||
@case
|
||||
def hcfa_never_filled_returns_a_reason_not_an_exception(page):
|
||||
_go(page, HCFA, "load=never")
|
||||
out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000",
|
||||
timeout_ms=2_000)
|
||||
assert out == ("HCFA form did not finish loading in 2s "
|
||||
"(claim number never filled, 0 CPT rows)"), out
|
||||
assert 1.8 <= took < 5.0, f"took {took:.1f}s"
|
||||
assert out in log, log
|
||||
|
||||
|
||||
@case
|
||||
def hcfa_wrong_claim_raises(page):
|
||||
_go(page, HCFA, "load=wrong")
|
||||
try:
|
||||
_timed(claim_form.wait_claim_loaded, page, "9103000")
|
||||
except RuntimeError as exc:
|
||||
assert "5000001" in str(exc) and "wrong claim" in str(exc), exc
|
||||
else:
|
||||
raise AssertionError("a form showing another claim did not raise")
|
||||
|
||||
|
||||
@case
|
||||
def hcfa_without_a_claim_number_field_still_waits_on_cpt_rows(page):
|
||||
_go(page, HCFA, "load=noclaimno")
|
||||
out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000")
|
||||
assert out is None, out
|
||||
assert took >= 0.7, f"returned after {took:.2f}s — rows were still blank"
|
||||
assert log.count("identity not checked") == 1, log
|
||||
|
||||
|
||||
# ── Dental ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@case
|
||||
def dental_filled_late_is_waited_for(page):
|
||||
_go(page, DENTAL, "load=delayed")
|
||||
out, took, log = _timed(claim_form.wait_claim_loaded, page, "5000000")
|
||||
assert out is None, out
|
||||
assert took >= 0.7, f"returned after {took:.2f}s — before the form filled"
|
||||
assert "Dental form loaded" in log and "2 service lines)" in log, log
|
||||
|
||||
|
||||
@case
|
||||
def dental_cancel_inside_a_dentalclaim_id_wrapper_still_reads_the_form(page):
|
||||
"""
|
||||
The window is found by walking up from the visible Cancel to the
|
||||
nearest SEL_DENTAL_WINDOW match. With an id-prefix-only selector, a
|
||||
`dentalClaimFooter…` wrapper around Cancel was that nearest match: no
|
||||
claim number and no service lines inside it, so the form read as blank
|
||||
and every Dental claim waited out the timeout as not evaluated. The
|
||||
window is the `.modal` itself now.
|
||||
"""
|
||||
_go(page, DENTAL, "load=delayed&footer=wrapped")
|
||||
assert page.locator("#dentalClaimFooter7777777777777 "
|
||||
"#cancelDentalClaimButton").count() == 1, "fixture did not wrap Cancel"
|
||||
out, took, log = _timed(claim_form.wait_claim_loaded, page, "5000000",
|
||||
timeout_ms=3_000)
|
||||
assert out is None, out
|
||||
assert took >= 0.7, f"returned after {took:.2f}s — before the form filled"
|
||||
assert "identity not checked" not in log, log
|
||||
assert "Dental form loaded" in log and "2 service lines)" in log, log
|
||||
|
||||
|
||||
@case
|
||||
def dental_never_filled_returns_a_reason(page):
|
||||
_go(page, DENTAL, "load=never")
|
||||
out, _, _ = _timed(claim_form.wait_claim_loaded, page, "5000000",
|
||||
timeout_ms=1_000)
|
||||
assert out == ("Dental form did not finish loading in 1s "
|
||||
"(claim number never filled, 0 service lines)"), out
|
||||
|
||||
|
||||
@case
|
||||
def dental_wrong_claim_raises(page):
|
||||
_go(page, DENTAL, "load=wrong")
|
||||
try:
|
||||
_timed(claim_form.wait_claim_loaded, page, "5000000")
|
||||
except RuntimeError as exc:
|
||||
assert "5000001" in str(exc), exc
|
||||
else:
|
||||
raise AssertionError("a Dental form showing another claim did not raise")
|
||||
|
||||
|
||||
@case
|
||||
def ub_form_is_left_to_open_claims_own_wait(page):
|
||||
"""No-op on UB: open_claim() already waits for its claim number."""
|
||||
_go(page, UB)
|
||||
out, took, _ = _timed(claim_form.wait_claim_loaded, page, "anything")
|
||||
assert out is None and took < 1.0, (out, took)
|
||||
|
||||
|
||||
# ── Elements that render late are waited for (final review, 2026-10-05) ──────
|
||||
# Both waits were `visible(timeout=10_000)` before P8 and answered instantly;
|
||||
# these fail if either is turned back into visible().
|
||||
|
||||
@case
|
||||
def a_cpt_grid_that_renders_late_is_waited_for(page):
|
||||
"""open_cpt_hcpcs_tab gives the grid 10s after the tab click."""
|
||||
_go(page, UB)
|
||||
page.evaluate("""() => {
|
||||
const g = document.querySelector('#CPTCodesDiv');
|
||||
g.style.display = 'none';
|
||||
setTimeout(() => { g.style.display = ''; }, 1800);
|
||||
}""")
|
||||
out, took, log = _timed(claim_form.open_cpt_hcpcs_tab, page)
|
||||
assert out is True, log
|
||||
assert took >= 1.5, f"returned after {took:.1f}s — before the grid showed"
|
||||
|
||||
|
||||
@case
|
||||
def a_prog_notes_button_that_renders_late_is_not_absent(page):
|
||||
"""open_progress_notes gives the button 10s to appear."""
|
||||
_go(page, HCFA)
|
||||
page.evaluate("""() => {
|
||||
const b = document.querySelector('[ng-click="viewProgNote()"]');
|
||||
b.style.display = 'none';
|
||||
setTimeout(() => { b.style.display = ''; }, 1500);
|
||||
}""")
|
||||
note, _, log = _timed(claim_form.open_progress_notes, page)
|
||||
assert note.get("opened") == "frame", (note, log)
|
||||
|
||||
|
||||
# ── Prog. Notes "disabled" is re-checked ──────────────────────────────────────
|
||||
|
||||
@case
|
||||
def prog_notes_enabled_late_is_not_reported_disabled(page):
|
||||
"""Angular enables the button only after load; read too early, a claim
|
||||
with a note was reported as having none."""
|
||||
_go(page, HCFA, "pn=late")
|
||||
assert page.locator('[ng-click="viewProgNote()"]').is_disabled()
|
||||
note, _, _ = _timed(claim_form.open_progress_notes, page)
|
||||
assert note.get("opened") == "frame", note
|
||||
|
||||
|
||||
@case
|
||||
def prog_notes_disabled_for_good_is_reported_after_about_5s(page):
|
||||
_go(page, HCFA, "pn=off")
|
||||
note, took, log = _timed(claim_form.open_progress_notes, page)
|
||||
assert note == {"opened": "disabled"}, note
|
||||
assert 4.5 <= took < 9.0, f"took {took:.1f}s"
|
||||
|
||||
|
||||
# ── The flow keeps the reason on the claim's data ─────────────────────────────
|
||||
|
||||
def _flow_read(dental: bool, not_loaded, dental_result, apply=False,
|
||||
calls=None, notes=False, dental_checks_on=True,
|
||||
results=(), save=False):
|
||||
"""
|
||||
Run the real _read_claim_form with every page step stubbed. `calls`, when
|
||||
given, collects ("run_all" | "save_claim" | "dental" | "wait" |
|
||||
"notes", ...) as they happen. `notes` turns OPEN_PROGRESS_NOTES on;
|
||||
`dental_checks_on` is CHECK_HIGH_RISK (CHECK_SLIDING_FEE stays off).
|
||||
"""
|
||||
calls = [] if calls is None else calls
|
||||
|
||||
def wait(page, cid):
|
||||
calls.append("wait")
|
||||
return not_loaded
|
||||
|
||||
def run_all(page, cid, data, before_apply=None, **kw):
|
||||
calls.append("run_all")
|
||||
return list(results)
|
||||
|
||||
def dental_checks(page, cid):
|
||||
calls.append("dental")
|
||||
return dict(dental_result)
|
||||
|
||||
stubs = {
|
||||
"open_claim": lambda page, cid: True,
|
||||
"wait_claim_loaded": wait,
|
||||
"open_progress_notes": lambda page: calls.append("notes") or {
|
||||
"opened": "frame", "sections": {"Procedure Codes": "x"}},
|
||||
"is_dental_claim_form": lambda page: dental,
|
||||
"open_cpt_hcpcs_tab": lambda page: None,
|
||||
"extract": lambda page: {"cpt": [{"code": "99213"}], "icd": [],
|
||||
"revenue": []},
|
||||
"save": lambda data: pathlib.Path("unused.json"),
|
||||
"close_claim": lambda page: True,
|
||||
"save_claim": lambda page: calls.append("save_claim") or True,
|
||||
}
|
||||
orig_cf = {k: getattr(claim_form, k) for k in stubs}
|
||||
orig_cfg = (cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES,
|
||||
cfg.APPLY_CORRECTIONS, cfg.CHECK_HIGH_RISK,
|
||||
cfg.CHECK_SLIDING_FEE, cfg.SAVE_CORRECTIONS,
|
||||
cfg.REVIEW_BEFORE_CLOSE)
|
||||
orig_box = (flow.msgbox.ok_cancel, flow.msgbox.info_box)
|
||||
orig_dental, orig_run_all = flow._dental_checks, flow.conditions.run_all
|
||||
for k, v in stubs.items():
|
||||
setattr(claim_form, k, v)
|
||||
cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES = True, notes
|
||||
cfg.APPLY_CORRECTIONS = apply
|
||||
# Not read from the operator's .env: a Dental case needs a check on to
|
||||
# reach _dental_checks at all (final review, 2026-10-05).
|
||||
cfg.CHECK_HIGH_RISK, cfg.CHECK_SLIDING_FEE = dental_checks_on, False
|
||||
cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE = save, False
|
||||
# Save, if ever offered, is answered OK — so "not saved" is the code's
|
||||
# doing, not the operator's.
|
||||
flow.msgbox.ok_cancel = lambda *a, **k: calls.append("save_box") or True
|
||||
flow.msgbox.info_box = lambda *a, **k: None
|
||||
flow._dental_checks = dental_checks
|
||||
flow.conditions.run_all = run_all
|
||||
try:
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
return flow._read_claim_form(None, "9103000")
|
||||
finally:
|
||||
for k, v in orig_cf.items():
|
||||
setattr(claim_form, k, v)
|
||||
(cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES,
|
||||
cfg.APPLY_CORRECTIONS, cfg.CHECK_HIGH_RISK,
|
||||
cfg.CHECK_SLIDING_FEE, cfg.SAVE_CORRECTIONS,
|
||||
cfg.REVIEW_BEFORE_CLOSE) = orig_cfg
|
||||
flow.msgbox.ok_cancel, flow.msgbox.info_box = orig_box
|
||||
flow._dental_checks = orig_dental
|
||||
flow.conditions.run_all = orig_run_all
|
||||
|
||||
|
||||
@case
|
||||
def flow_marks_an_hcfa_claim_not_evaluated_and_still_reads_it():
|
||||
why = "HCFA form did not finish loading in 20s (0 CPT rows)"
|
||||
data = _flow_read(False, why, {})
|
||||
assert data.get("_not_evaluated") == why, data
|
||||
assert data.get("cpt"), "the claim was not read after the timeout"
|
||||
|
||||
|
||||
@case
|
||||
def flow_leaves_a_loaded_claim_unmarked():
|
||||
data = _flow_read(False, None, {})
|
||||
assert "_not_evaluated" not in data, data
|
||||
|
||||
|
||||
@case
|
||||
def flow_marks_the_dental_result_not_evaluated():
|
||||
why = "Dental form did not finish loading in 20s (0 service lines)"
|
||||
data = _flow_read(True, why, {"claim_no": "9103000",
|
||||
"high_risk": {"high_risk": False}})
|
||||
assert data.get("_not_evaluated") == why, data
|
||||
|
||||
|
||||
@case
|
||||
def flow_runs_no_conditions_on_a_form_that_did_not_finish_loading():
|
||||
"""
|
||||
APPLY_CORRECTIONS=1: a half-loaded form is read and closed, never
|
||||
corrected — run_all() would read the same blank rows, and with
|
||||
SAVE_CORRECTIONS=1 an approved OK would save the half-loaded form back
|
||||
to live eCW. Nothing ran, so no `_conditions` either.
|
||||
"""
|
||||
calls = []
|
||||
why = "HCFA form did not finish loading in 20s (0 CPT rows)"
|
||||
data = _flow_read(False, why, {}, apply=True, calls=calls)
|
||||
assert "run_all" not in calls and "save_claim" not in calls, calls
|
||||
assert data.get("_not_evaluated") == why, data
|
||||
assert "_conditions" not in data, data
|
||||
|
||||
|
||||
@case
|
||||
def flow_still_runs_conditions_on_a_loaded_form():
|
||||
"""The control for the case above: the run_all stub is reachable."""
|
||||
calls = []
|
||||
data = _flow_read(False, None, {}, apply=True, calls=calls)
|
||||
# "wait": _flow_read records the load wait too since the final review.
|
||||
assert calls == ["wait", "run_all"], calls
|
||||
# Unit D (2026-10-05): run_all's report rides along too, so the summary
|
||||
# can tell "raised / declined" from "clean".
|
||||
# `changes` (2026-10-08): what each condition changed, for the results DB.
|
||||
# `detected` / `changed` (2026-10-12): the same, split for the DB.
|
||||
assert data.get("_conditions") == {"fired": [], "detected": [],
|
||||
"changed": [], "saved": False,
|
||||
"failed": [], "review": [],
|
||||
"comments": {}, "changes": {}}, data
|
||||
|
||||
|
||||
@case
|
||||
def flow_tells_the_dental_box_the_form_did_not_finish_loading():
|
||||
"""
|
||||
Updated for review item D9 (2026-10-05): `_dental_checks` no longer
|
||||
shows a box, so the reason is no longer passed into it. It reaches the
|
||||
claim's one box (`_claim_box`, after close) on the data instead, and
|
||||
that box's Dental part leads with NOT_LOADED_NOTE.
|
||||
"""
|
||||
calls = []
|
||||
why = "Dental form did not finish loading in 20s (0 service lines)"
|
||||
data = _flow_read(True, why, {"claim_no": "9103000",
|
||||
"high_risk": {"high_risk": False,
|
||||
"match": ""}},
|
||||
calls=calls)
|
||||
assert calls == ["wait", "dental"], calls
|
||||
assert data["_not_evaluated"] == why, data
|
||||
_, body = flow._claim_box("Claim 1/1", "9103000", data, None)
|
||||
detail = body.split(flow.RULE + "\n", 1)[1].splitlines()
|
||||
assert detail[0] == claim_form.NOT_LOADED_NOTE, body
|
||||
assert detail[1] == f"({why})", body
|
||||
|
||||
|
||||
@case
|
||||
def claim_summary_leads_with_the_not_loaded_line():
|
||||
"""The SHOW_EACH_CLAIM box must not show a confident "CPT 0" read off a
|
||||
form the bot logged as half-loaded."""
|
||||
why = "HCFA form did not finish loading in 20s (0 CPT rows)"
|
||||
text = claim_form.summary({"claim_no": "9103000", "cpt": [],
|
||||
"_not_evaluated": why})
|
||||
first = text.splitlines()
|
||||
assert first[0] == claim_form.NOT_LOADED_NOTE, text
|
||||
assert first[1] == f"({why})", text
|
||||
plain = claim_form.summary({"claim_no": "9103000", "cpt": []})
|
||||
assert claim_form.NOT_LOADED_NOTE not in plain, plain
|
||||
assert plain.splitlines()[0].startswith("Claim No"), plain
|
||||
|
||||
|
||||
@case
|
||||
def flow_reads_a_dental_claim_even_with_no_dental_check_on():
|
||||
"""
|
||||
Operator, 2026-10-12: a Dental claim runs every applicable condition,
|
||||
not only high risk / sliding fee — so it is waited for and read like
|
||||
any claim, whatever CHECK_HIGH_RISK / CHECK_SLIDING_FEE say. (It used
|
||||
to come back {} and blank in the summary.)
|
||||
"""
|
||||
calls = []
|
||||
data = _flow_read(True, None, {}, calls=calls, dental_checks_on=False,
|
||||
apply=True)
|
||||
assert calls == ["wait", "run_all"], calls
|
||||
assert data.get("screen") == "dental", data
|
||||
assert data.get("cpt"), data
|
||||
assert data.get("_conditions") is not None, data
|
||||
|
||||
|
||||
@case
|
||||
def flow_runs_the_conditions_on_a_dental_claim_after_its_own_checks():
|
||||
calls = []
|
||||
data = _flow_read(True, None, {"claim_no": "9103000",
|
||||
"high_risk": {"high_risk": True,
|
||||
"match": "x"}},
|
||||
calls=calls, apply=True, notes=True)
|
||||
assert calls == ["wait", "dental", "notes", "run_all"], calls
|
||||
assert data["high_risk"]["high_risk"] is True, "Dental result kept"
|
||||
assert data.get("screen") == "dental" and data.get("cpt"), data
|
||||
|
||||
|
||||
@case
|
||||
def flow_never_saves_a_dental_claim_even_with_save_on():
|
||||
"""Operator, 2026-10-12: Dental row writes are not live-verified yet."""
|
||||
from ecw_bot.conditions.base import ConditionResult
|
||||
fired = ConditionResult(condition_name="dummy_codes", changed=True,
|
||||
changes=[{"row": 1, "code": "DCONS",
|
||||
"unchecked": True}])
|
||||
calls = []
|
||||
data = _flow_read(True, None, {}, calls=calls, apply=True, save=True,
|
||||
results=[fired], dental_checks_on=False)
|
||||
assert "save_box" not in calls and "save_claim" not in calls, calls
|
||||
assert data["_conditions"]["saved"] is False, data
|
||||
|
||||
# The gate itself, and its control: the same result on a read that is
|
||||
# not Dental IS offered for Save.
|
||||
asked = []
|
||||
orig = (cfg.SAVE_CORRECTIONS, flow.msgbox.ok_cancel)
|
||||
cfg.SAVE_CORRECTIONS = True
|
||||
flow.msgbox.ok_cancel = lambda *a, **k: asked.append(1) or False
|
||||
try:
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
flow._confirm_and_save(None, "1", [fired], {},
|
||||
{"screen": "dental"})
|
||||
assert asked == [], "a Dental claim was offered for Save"
|
||||
flow._confirm_and_save(None, "1", [fired], {}, None)
|
||||
finally:
|
||||
cfg.SAVE_CORRECTIONS, flow.msgbox.ok_cancel = orig
|
||||
assert asked == [1], "the control was not offered"
|
||||
|
||||
|
||||
@case
|
||||
def flow_still_waits_on_a_dental_form_when_a_check_is_on():
|
||||
calls = []
|
||||
_flow_read(True, None, {"claim_no": "9103000",
|
||||
"high_risk": {"high_risk": False}}, calls=calls)
|
||||
assert calls == ["wait", "dental"], calls
|
||||
|
||||
|
||||
@case
|
||||
def flow_does_not_open_the_note_of_a_form_that_did_not_finish_loading():
|
||||
"""
|
||||
Final review, 2026-10-05: the conditions are skipped on such a claim
|
||||
and it is Review whatever the note says, so the note's up to ~45s
|
||||
(absent 10s, disabled 5s, nothing 20s + close) is not spent. The note
|
||||
is recorded as skipped, and the summary says it was not read.
|
||||
"""
|
||||
from ecw_bot.core import charge_summary as cs
|
||||
calls = []
|
||||
why = "HCFA form did not finish loading in 20s (0 CPT rows)"
|
||||
data = _flow_read(False, why, {}, apply=True, calls=calls, notes=True)
|
||||
assert "notes" not in calls, calls
|
||||
assert data["progress_notes"]["opened"] == "skipped", data
|
||||
reasons = cs.review_reasons(data)
|
||||
assert reasons[0] == why, reasons
|
||||
assert "Progress note not read (skipped) — telehealth not checked" in \
|
||||
reasons, reasons
|
||||
|
||||
|
||||
@case
|
||||
def flow_opens_the_note_of_a_loaded_form():
|
||||
"""The control: the stub is reachable and the note is read."""
|
||||
calls = []
|
||||
data = _flow_read(False, None, {}, calls=calls, notes=True)
|
||||
assert "notes" in calls, calls
|
||||
assert data["progress_notes"]["opened"] == "frame", data
|
||||
|
||||
|
||||
# ── The claim number is compared as a number (final review, 2026-10-05) ──────
|
||||
|
||||
@case
|
||||
def same_claim_no_ignores_formatting_but_not_a_different_number():
|
||||
same = claim_form.same_claim_no
|
||||
assert same("5000000", "5000000")
|
||||
assert same("05000000", "5000000")
|
||||
assert same("\u00a05000\u00a0000 ", "5000000")
|
||||
assert same("Claim # 5000000", "5000000")
|
||||
assert not same("5000001", "5000000")
|
||||
assert not same("50000001", "5000000")
|
||||
assert not same("", "5000000")
|
||||
assert same("abc", "abc") and not same("abc", "abd")
|
||||
|
||||
|
||||
@case
|
||||
def dental_claim_number_written_differently_is_not_a_wrong_claim(page):
|
||||
"""
|
||||
The Dental claim-number span is not yet live-verified: a leading zero
|
||||
or an inner non-breaking space must not raise "wrong claim" on every
|
||||
Dental claim. It is logged once, so the live format is on record.
|
||||
"""
|
||||
_go(page, DENTAL)
|
||||
page.evaluate("""() => {
|
||||
document.querySelector('[ng-bind="ClaimData.claimNo"]')
|
||||
.textContent = '0500\u00a00000';
|
||||
}""")
|
||||
out, _, log = _timed(claim_form.wait_claim_loaded, page, "5000000",
|
||||
timeout_ms=3_000)
|
||||
assert out is None, (out, log)
|
||||
assert log.count("the same claim, written differently") == 1, log
|
||||
|
||||
|
||||
@case
|
||||
def ub_claim_number_that_never_filled_is_a_not_loaded_reason(page):
|
||||
"""
|
||||
Final review, 2026-10-05: open_claim() only logged "Claim No never
|
||||
filled in — reading anyway", so the conditions ran on a UB form that
|
||||
may be half-loaded. The reason now reaches the claim's data, and the
|
||||
flow's not-evaluated branch skips the conditions as for HCFA/Dental.
|
||||
"""
|
||||
_go(page, UB)
|
||||
page.evaluate("""() => {
|
||||
for (const e of document.querySelectorAll(
|
||||
'#UBClaimDiv .ubClaimNumberText span')) e.textContent = '';
|
||||
}""")
|
||||
out, took, _ = _timed(claim_form.wait_claim_loaded, page, "9104000")
|
||||
assert out == ("UB form did not finish loading (claim number never "
|
||||
"filled)"), out
|
||||
assert took < 1.0, f"took {took:.1f}s — open_claim already waited"
|
||||
|
||||
|
||||
@case
|
||||
def flow_reports_a_wrong_claim_as_unread_and_still_closes_it():
|
||||
closed = []
|
||||
|
||||
def wrong(page, cid):
|
||||
raise RuntimeError("Opened the form for claim '5000001'")
|
||||
|
||||
orig = (claim_form.open_claim, claim_form.wait_claim_loaded,
|
||||
claim_form.close_claim, cfg.OPEN_CLAIM_FORM)
|
||||
claim_form.open_claim = lambda page, cid: True
|
||||
claim_form.wait_claim_loaded = wrong
|
||||
claim_form.close_claim = lambda page: closed.append(True)
|
||||
cfg.OPEN_CLAIM_FORM = True
|
||||
try:
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
data = flow._read_claim_form(None, "9103000")
|
||||
finally:
|
||||
(claim_form.open_claim, claim_form.wait_claim_loaded,
|
||||
claim_form.close_claim, cfg.OPEN_CLAIM_FORM) = orig
|
||||
# Unit D (2026-10-05): not {} any more — {} read exactly like "not
|
||||
# found". The error rides on the data and the summary says Review.
|
||||
assert data == {"claim_no": "9103000",
|
||||
"_error": "Opened the form for claim '5000001'"}, data
|
||||
assert closed == [True], "the wrong claim's form was left open"
|
||||
|
||||
|
||||
|
||||
# ── close_claim() on a page error ────────────────────────────────────────────
|
||||
|
||||
@case
|
||||
def any_claim_form_open_raises_on_a_page_error_rather_than_reading_closed():
|
||||
"""
|
||||
visible() swallows errors and answers False; close_claim() reads False as
|
||||
"already closed — nothing to cancel". any_claim_form_open() therefore
|
||||
keeps its own raising check (review of unit C, 2026-10-05).
|
||||
"""
|
||||
class Broken:
|
||||
def locator(self, selector):
|
||||
raise RuntimeError("Target page, context or browser has been closed")
|
||||
try:
|
||||
claim_form.any_claim_form_open(Broken())
|
||||
except RuntimeError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("a page error read as an answer, not an error")
|
||||
|
||||
|
||||
@case
|
||||
def close_claim_still_clicks_cancel_when_the_open_check_errors(page):
|
||||
"""A transient error while asking is not "closed": Cancel is still
|
||||
clicked (the form would otherwise stay open for the next claim), and the
|
||||
run is not ended by a raise from the flow's `finally`."""
|
||||
_go(page, DENTAL)
|
||||
orig = claim_form.any_claim_form_open
|
||||
|
||||
def broken(page):
|
||||
raise RuntimeError("Execution context was destroyed")
|
||||
|
||||
claim_form.any_claim_form_open = broken
|
||||
try:
|
||||
out, _, log = _timed(claim_form.close_claim, page)
|
||||
finally:
|
||||
claim_form.any_claim_form_open = orig
|
||||
assert out is True, (out, log)
|
||||
assert "trying Cancel anyway" in log, log
|
||||
assert "already closed" not in log, log
|
||||
assert not page.locator("#cancelDentalClaimButton").is_visible(), "Cancel was not clicked — the form is still open"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
except ImportError:
|
||||
print(" SKIP playwright is not installed")
|
||||
return 0
|
||||
|
||||
failures = 0
|
||||
orig_shots = cfg.SCREENSHOTS
|
||||
with tempfile.TemporaryDirectory() as tmp, sync_playwright() as pw:
|
||||
cfg.SCREENSHOTS = pathlib.Path(tmp)
|
||||
browser = pw.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
try:
|
||||
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
|
||||
finally:
|
||||
cfg.SCREENSHOTS = orig_shots
|
||||
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