Files
ecw-charge-validation/tests/test_sliding_fee.py
T
ManikandanandClaude Opus 5.5 ba15a574ab 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>
2026-10-23 18:38:17 +05:30

312 lines
11 KiB
Python

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