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>
312 lines
11 KiB
Python
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())
|