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>
691 lines
28 KiB
Python
691 lines
28 KiB
Python
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())
|