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,311 @@
|
||||
r"""
|
||||
Patient Info -> Sliding Fee Schedule: the read-only "is the sliding fee
|
||||
still valid on the claim's DOS" check (pages/sliding_fee.py).
|
||||
|
||||
python tests\test_sliding_fee.py
|
||||
|
||||
Browser cases run against tests/fixtures/sliding_fee.html; the verdict
|
||||
cases need no browser.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import pathlib
|
||||
import inspect
|
||||
import tempfile
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
FIXTURE = pathlib.Path(__file__).resolve().parent / "fixtures" / "sliding_fee.html"
|
||||
|
||||
from ecw_bot.pages import sliding_fee # noqa: E402
|
||||
from ecw_bot.selectors import claim_form as sel # noqa: E402
|
||||
|
||||
CASES = []
|
||||
|
||||
ASSIGNED = {"from_date": "03/10/2026", "to_date": "03/10/2027"}
|
||||
|
||||
|
||||
def case(fn):
|
||||
CASES.append(fn)
|
||||
return fn
|
||||
|
||||
|
||||
# ── verdict (no browser) ─────────────────────────────────────────────────────
|
||||
|
||||
@case
|
||||
def dos_inside_the_range_is_active():
|
||||
assert sliding_fee.evaluate(ASSIGNED, "08/31/2026")["verdict"] == "active"
|
||||
|
||||
|
||||
@case
|
||||
def both_range_ends_count_as_inside():
|
||||
assert sliding_fee.evaluate(ASSIGNED, "03/10/2026")["verdict"] == "active"
|
||||
assert sliding_fee.evaluate(ASSIGNED, "03/10/2027")["verdict"] == "active"
|
||||
|
||||
|
||||
@case
|
||||
def dos_after_the_to_date_is_expired():
|
||||
r = sliding_fee.evaluate(ASSIGNED, "03/11/2027")
|
||||
assert r["verdict"] == "expired", r
|
||||
|
||||
|
||||
@case
|
||||
def dos_before_the_from_date_is_not_started():
|
||||
r = sliding_fee.evaluate(ASSIGNED, "03/09/2026")
|
||||
assert r["verdict"] == "not_started", r
|
||||
|
||||
|
||||
@case
|
||||
def only_the_dates_decide_other_fields_are_ignored():
|
||||
"""Operator, 2026-09-30: only the Date from/to against the DOS is
|
||||
checked — eCW's Status, poverty level, type are not."""
|
||||
r = sliding_fee.evaluate(dict(ASSIGNED, status="Expired"), "08/31/2026")
|
||||
assert r["verdict"] == "active", r
|
||||
|
||||
|
||||
@case
|
||||
def disabled_button_or_no_dates_is_none():
|
||||
assert sliding_fee.evaluate({"disabled": True}, "08/31/2026")["verdict"] == "none"
|
||||
blank = dict(ASSIGNED, from_date="", to_date="__/__/____")
|
||||
assert sliding_fee.evaluate(blank, "08/31/2026")["verdict"] == "none"
|
||||
|
||||
|
||||
@case
|
||||
def read_error_or_missing_dos_is_unknown():
|
||||
assert sliding_fee.evaluate({"error": "x"}, "08/31/2026")["verdict"] == "unknown"
|
||||
assert sliding_fee.evaluate(None, "08/31/2026")["verdict"] == "unknown"
|
||||
assert sliding_fee.evaluate(ASSIGNED, "")["verdict"] == "unknown"
|
||||
|
||||
|
||||
@case
|
||||
def claim_dos_is_the_first_readable_sdos():
|
||||
data = {"cpt": [{"sdos": "__/__/____"}, {"sdos": "08/31/2026"},
|
||||
{"sdos": "09/01/2026"}]}
|
||||
assert sliding_fee.claim_dos(data) == "08/31/2026"
|
||||
assert sliding_fee.claim_dos({}) == ""
|
||||
|
||||
|
||||
# ── the screen (fixture) ─────────────────────────────────────────────────────
|
||||
|
||||
def _reset(page, **knobs):
|
||||
page.goto(FIXTURE.as_uri())
|
||||
page.evaluate("k => Object.assign(window.fixture, k)", knobs)
|
||||
|
||||
|
||||
def _all_closed_nothing_saved(page):
|
||||
assert not page.locator(sel.SEL_PT_INFO_MODAL).is_visible(), "ptInfo open"
|
||||
assert not page.locator(sel.SEL_SLIDING_FEE_MODAL).is_visible(), "modal open"
|
||||
assert page.evaluate("window.saved") is False, "a Save was clicked"
|
||||
|
||||
|
||||
@case
|
||||
def reads_the_assigned_dates_once_angular_fills_them(page):
|
||||
_reset(page, values=ASSIGNED)
|
||||
info = sliding_fee.read(page)
|
||||
assert info["from_date"] == "03/10/2026", info
|
||||
assert info["to_date"] == "03/10/2027", info
|
||||
assert set(info) == {"from_date", "to_date"}, info
|
||||
_all_closed_nothing_saved(page)
|
||||
|
||||
|
||||
@case
|
||||
def check_gives_the_verdict_for_the_claim_dos(page):
|
||||
_reset(page, values=ASSIGNED)
|
||||
r = sliding_fee.check(page, "T1", {"cpt": [{"sdos": "04/01/2027"}]})
|
||||
assert r["verdict"] == "expired", r
|
||||
_all_closed_nothing_saved(page)
|
||||
|
||||
|
||||
@case
|
||||
def disabled_button_is_reported_and_still_closes(page):
|
||||
_reset(page, disabled=True, values=ASSIGNED)
|
||||
info = sliding_fee.read(page)
|
||||
assert info == {"disabled": True}, info
|
||||
_all_closed_nothing_saved(page)
|
||||
|
||||
|
||||
@case
|
||||
def unsaved_prompt_is_answered_dont_save(page):
|
||||
_reset(page, values=ASSIGNED, prompt=True)
|
||||
sliding_fee.read(page)
|
||||
assert not page.locator("#savePrompt").is_visible()
|
||||
_all_closed_nothing_saved(page)
|
||||
|
||||
|
||||
@case
|
||||
def a_hidden_leftover_info_button_first_in_the_dom_is_not_clicked(page):
|
||||
"""
|
||||
appears() waits on the VISIBLE Info button; the click that follows must
|
||||
use that one too. With a hidden copy first in the DOM, an unfiltered
|
||||
`.first` clicked the hidden copy and sat out its 15s click timeout, then
|
||||
reported "error" for a claim whose Info button was on screen.
|
||||
"""
|
||||
import time
|
||||
_reset(page, values=ASSIGNED)
|
||||
page.evaluate("""() => {
|
||||
const live = document.querySelector('#claimPatientInfo1790766147287');
|
||||
const copy = live.cloneNode(true);
|
||||
copy.style.display = 'none';
|
||||
document.body.insertBefore(copy, document.body.firstChild);
|
||||
}""")
|
||||
started = time.monotonic()
|
||||
info = sliding_fee.read(page)
|
||||
assert info.get("from_date") == "03/10/2026", info
|
||||
assert time.monotonic() - started < 10, "clicked the hidden copy first"
|
||||
_all_closed_nothing_saved(page)
|
||||
|
||||
|
||||
@case
|
||||
def a_hidden_leftover_sliding_fee_button_first_in_the_dom_is_not_clicked(page):
|
||||
"""
|
||||
Final review, 2026-10-05: appears() waits on the VISIBLE Sliding Fee
|
||||
Schedule button, and the click must take that one too. With a hidden
|
||||
copy first in the DOM, an unfiltered `.first` clicked the hidden copy
|
||||
and sat out its 15s click timeout, then reported "error".
|
||||
"""
|
||||
import time
|
||||
_reset(page, values=ASSIGNED)
|
||||
page.evaluate("""() => {
|
||||
const live = document.querySelector('#btnSlidingFeeschedule');
|
||||
const copy = live.cloneNode(true);
|
||||
copy.style.display = 'none';
|
||||
copy.removeAttribute('onclick');
|
||||
copy.removeAttribute('data-ng-click');
|
||||
document.body.insertBefore(copy, document.body.firstChild);
|
||||
}""")
|
||||
started = time.monotonic()
|
||||
info = sliding_fee.read(page)
|
||||
assert info.get("from_date") == "03/10/2026", info
|
||||
assert time.monotonic() - started < 10, "clicked the hidden copy first"
|
||||
_all_closed_nothing_saved(page)
|
||||
|
||||
|
||||
@case
|
||||
def hidden_leftover_close_and_cancel_buttons_are_not_clicked(page):
|
||||
"""
|
||||
_close_patient_info() checks with visible(), which answers True when ANY
|
||||
copy is on screen (P8) — so its clicks must take the visible copy too. A
|
||||
hidden leftover Cancel FIRST in Patient Information (what `.first`
|
||||
took) and a hidden leftover Close LAST in the Sliding Fee modal (what
|
||||
`.last` took) each sat out a 10s click timeout, and Patient Information
|
||||
stayed open over the claim form (review of unit C, 2026-10-05).
|
||||
"""
|
||||
import time
|
||||
_reset(page, values=ASSIGNED)
|
||||
page.evaluate("""() => {
|
||||
const hide = e => { e.style.display = 'none'; e.onclick = null;
|
||||
e.removeAttribute('id'); return e; };
|
||||
const cancel = document.querySelector('#patient-demographicsBtn57');
|
||||
cancel.parentNode.insertBefore(hide(cancel.cloneNode(true)),
|
||||
cancel.parentNode.firstChild);
|
||||
const close = document.querySelector('#slidingfeescheduleBtn8');
|
||||
close.parentNode.appendChild(hide(close.cloneNode(true)));
|
||||
}""")
|
||||
started = time.monotonic()
|
||||
info = sliding_fee.read(page)
|
||||
assert info.get("from_date") == "03/10/2026", info
|
||||
took = time.monotonic() - started
|
||||
assert took < 10, f"took {took:.1f}s — clicked a hidden copy"
|
||||
_all_closed_nothing_saved(page)
|
||||
|
||||
|
||||
@case
|
||||
def no_info_button_is_an_error_not_a_crash(page):
|
||||
_reset(page, values=ASSIGNED)
|
||||
page.evaluate("document.querySelector('#claimPatientInfo1790766147287').remove()")
|
||||
info = sliding_fee.read(page)
|
||||
assert "error" in info, info
|
||||
assert sliding_fee.evaluate(info, "08/31/2026")["verdict"] == "unknown"
|
||||
|
||||
|
||||
@case
|
||||
def message_box_text_carries_the_verdict_dos_and_dates_only():
|
||||
text = sliding_fee.message(sliding_fee.evaluate(ASSIGNED, "04/01/2027"))
|
||||
for want in ("EXPIRED", "04/01/2027", "03/10/2026", "03/10/2027"):
|
||||
assert want in text, (want, text)
|
||||
for gone in ("Status", "Poverty", "Scale type", "Fee schedule"):
|
||||
assert gone not in text, (gone, text)
|
||||
assert "UNKNOWN" in sliding_fee.message(sliding_fee.evaluate(None, ""))
|
||||
|
||||
|
||||
DENTAL_ROWS = """
|
||||
<div id="dentalClaim7777777777777"><table id="claimCPTCodesTable7777777777777">
|
||||
<tr><td><input id="DentalClaimCPTSDOS0" data-fieldname="DentalClaimCPTSDOS"
|
||||
ng-model="CPT.SDOS" placeholder="__/__/____"></td></tr>
|
||||
<tr><td><input id="DentalClaimCPTSDOS1" ng-model="CPT.SDOS"></td></tr>
|
||||
</table><input id="servicedate" ng-model="ClaimData.ServiceDt"></div>
|
||||
"""
|
||||
|
||||
|
||||
@case
|
||||
def dental_dos_reads_the_first_service_line_then_the_claim_date(page):
|
||||
page.set_content(DENTAL_ROWS)
|
||||
# Angular sets .value, not the attribute — so does the test.
|
||||
page.evaluate("""() => {
|
||||
document.querySelector('#DentalClaimCPTSDOS0').value = '__/__/____';
|
||||
document.querySelector('#DentalClaimCPTSDOS1').value = '08/31/2026';
|
||||
document.querySelector('#servicedate').value = '09/01/2026'; }""")
|
||||
assert sliding_fee.dental_dos(page) == "08/31/2026"
|
||||
page.evaluate("document.querySelector('#DentalClaimCPTSDOS1').value = ''")
|
||||
assert sliding_fee.dental_dos(page) == "09/01/2026"
|
||||
page.evaluate("document.querySelector('#servicedate').value = ''")
|
||||
assert sliding_fee.dental_dos(page) == ""
|
||||
|
||||
|
||||
@case
|
||||
def check_on_a_dental_claim_uses_the_dental_dos(page):
|
||||
_reset(page, values=ASSIGNED)
|
||||
page.evaluate("h => document.body.insertAdjacentHTML('beforeend', h)",
|
||||
DENTAL_ROWS)
|
||||
page.evaluate("document.querySelector('#DentalClaimCPTSDOS0').value = '08/31/2026'")
|
||||
r = sliding_fee.check(page, "D1", {})
|
||||
assert r["dos"] == "08/31/2026" and r["verdict"] == "active", r
|
||||
_all_closed_nothing_saved(page)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not FIXTURE.exists():
|
||||
print(f" FAIL fixture missing: {FIXTURE}")
|
||||
return 1
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
except ImportError:
|
||||
print(" SKIP playwright is not installed")
|
||||
return 0
|
||||
|
||||
failures = 0
|
||||
# read() screenshots every success (shot(page, "sliding_fee")): into a
|
||||
# temp folder, never the operator's dated Screenshots\ folder (final
|
||||
# review, 2026-10-05 — the suite was mixing fixture PNGs into it).
|
||||
from ecw_bot import settings as cfg
|
||||
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()
|
||||
page.goto(FIXTURE.as_uri())
|
||||
|
||||
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()
|
||||
cfg.SCREENSHOTS = orig_shots
|
||||
|
||||
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