r""" Dental claims: "HIGH RISK" in Billing Notes (document #15), and the claim's one per-claim box (SHOW_EACH_CLAIM) that reports it with the sliding fee — since review item D9, not a box of _dental_checks' own. python tests\test_dental_high_risk.py """ import sys import pathlib import inspect ROOT = pathlib.Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "src")) from ecw_bot.pages import dental_notes # noqa: E402 from ecw_bot.flows import export_medcor_ready as flow # noqa: E402 CASES = [] # Shaped like a live claim's Billing Notes, fictional text: 79+ characters # from HIGH RISK to the end. NOTES_HIGH = ("notesEligibility :2026-09-04 @ 12:04:10 MDT : Eligible : " "Eligible; HIGH RISK for caries - caries risk assessment done, " "fluoride varnish applied, recall 3 months") NOTES_PLAIN = "notesEligibility :2026-09-04 @ 12:04:10 MDT : Eligible : Eligible" DENTAL = """
""" def case(fn): CASES.append(fn) return fn def _dental(page, notes): page.set_content(DENTAL) # Angular sets .value, not the text node — so does the test. page.evaluate("t => document.querySelector('#dentalClaimNotes').value = t", notes) # ── keyword (no browser) ───────────────────────────────────────────────────── @case def high_risk_matches_any_case_and_spacing(): for t in (NOTES_HIGH, "high risk", "High-Risk pt", "HIGHRISK", "high risk"): assert dental_notes.mentions_high_risk(t) is True, t tail = " " + NOTES_PLAIN + "; " + NOTES_PLAIN # well over 79 for t in ("high risk", "High-Risk pt", "HIGHRISK", "high risk"): assert dental_notes.is_high_risk(t + tail) is True, t @case def plain_notes_are_not_high_risk(): for t in (NOTES_PLAIN, "", None, "risk assessment high", NOTES_PLAIN * 3): assert dental_notes.is_high_risk(t) is False, t # ── the length rule (operator, 2026-10-12; from HIGH RISK since 2026-10-21) ─ def _notes_of(length): """Notes whose part from HIGH RISK to the end is exactly `length` characters, after a first line that must not count.""" head = "HIGH RISK for caries" return "Pt called for appt; eligible\n" + head + "x" * (length - len(head)) @case def high_risk_needs_79_characters_or_more(): assert dental_notes.HIGH_RISK_MIN_CHARS == 79 assert dental_notes.is_high_risk(_notes_of(79)) is True assert dental_notes.is_high_risk(_notes_of(120)) is True assert dental_notes.is_high_risk(_notes_of(78)) is False @case def trailing_blanks_do_not_count_toward_79(): assert dental_notes.is_high_risk(_notes_of(78) + " \n ") is False assert dental_notes.is_high_risk(_notes_of(79) + " ") is True @case def text_before_high_risk_does_not_count(): """Operator 2026-10-21: count from HIGH RISK, not the whole notes.""" before = NOTES_PLAIN * 3 + "\n" # 190+ characters before assert dental_notes.is_high_risk(before + "HIGH RISK for caries") is False assert dental_notes.high_risk_length(before + "HIGH RISK for caries") == 20 @case def later_lines_after_high_risk_count(): """HIGH RISK on line 2: line 2 and every line after it count.""" notes = ("Pt called for appt\nHIGH RISK - caries risk assessment done\n" "fluoride varnish applied, recall in 3 months") assert dental_notes.high_risk_length(notes) == len(notes) - 19 assert dental_notes.is_high_risk(notes) is True assert dental_notes.high_risk_length("no marker here " * 10) == 0 @case def short_notes_saying_high_risk_are_not_high_risk_and_say_why(page): import io import contextlib _dental(page, _notes_of(50)) with contextlib.redirect_stdout(io.StringIO()) as out: r = dental_notes.check(page, "5000000") assert r["high_risk"] is False and r["match"] == "", r assert r["length"] == 50, r assert "50 characters (79 needed)" in out.getvalue(), out.getvalue() @case def message_says_yes_no_or_unknown(): assert "YES" in dental_notes.message({"high_risk": True, "match": "HIGH RISK"}) assert "NO" in dental_notes.message({"high_risk": False, "match": ""}) assert "UNKNOWN" in dental_notes.message({"high_risk": None, "match": ""}) # ── the Dental screen (browser) ────────────────────────────────────────────── @case def check_reads_billing_notes_and_flags_high_risk(page): _dental(page, NOTES_HIGH) r = dental_notes.check(page, "9115000") assert r["high_risk"] is True, r assert "HIGH RISK" in r["match"], r @case def check_says_not_high_risk_for_plain_notes(page): _dental(page, NOTES_PLAIN) assert dental_notes.check(page, "5000000")["high_risk"] is False @case def missing_notes_box_is_unknown_not_a_crash(page): page.set_content("
") assert dental_notes.check(page, "X")["high_risk"] is None # ── the claim's one box (review items D9/P6, 2026-10-05) ──────────────────── # _dental_checks used to show a box of its own, ungated, and the per-claim # box after it printed a page of "?". Now _dental_checks shows nothing and # the per-claim box (_claim_box, gated by SHOW_EACH_CLAIM) carries the # Dental results. Updated: these three cases used to assert the box came # from _dental_checks itself. HEAD = "Claim 1/1\n\nClaim No : 9115000" def _checks(page, claim_id): """_dental_checks with info_box forbidden: it must not show a box.""" def _no_box(*a, **k): raise AssertionError("_dental_checks must not show a box (D9/P6)") old_box, old_sf = flow.msgbox.info_box, flow.cfg.CHECK_SLIDING_FEE old_hr = flow.cfg.CHECK_HIGH_RISK flow.msgbox.info_box = _no_box flow.cfg.CHECK_SLIDING_FEE = False # no Patient Info on this page flow.cfg.CHECK_HIGH_RISK = True try: return flow._dental_checks(page, claim_id) finally: flow.msgbox.info_box, flow.cfg.CHECK_SLIDING_FEE = old_box, old_sf flow.cfg.CHECK_HIGH_RISK = old_hr @case def dental_checks_shows_no_box_and_the_claim_box_carries_high_risk(page): _dental(page, NOTES_HIGH) data = _checks(page, "9115000") assert data["high_risk"]["high_risk"] is True, data title, body = flow._claim_box(HEAD, "9115000", data, flow.charge_summary.claim_outcome(True, data)) assert "9115000" in title and "HIGH RISK" in title, title assert "YES" in body and "Read-only" in body, body assert body.startswith(HEAD), body @case def dental_claim_box_title_has_no_high_risk_tag_when_not(page): _dental(page, NOTES_PLAIN) data = _checks(page, "5000000") title, body = flow._claim_box(HEAD, "5000000", data, None) assert "HIGH RISK" not in title, title assert "not a high risk claim" in body, body assert flow.claim_form.NOT_LOADED_NOTE not in body, body @case def dental_claim_box_leads_with_not_loaded_when_the_form_never_loaded(page): """ wait_claim_loaded() gave up: "not a high risk claim" was read off Billing Notes that may simply not have arrived yet, so the Dental part of the box says so first rather than showing it as a confident result (review of unit C). The caller puts the reason on the data as `_not_evaluated`. """ _dental(page, NOTES_PLAIN) why = "Dental form did not finish loading in 20s (0 service lines)" data = _checks(page, "5000000") data["_not_evaluated"] = why _, body = flow._claim_box(HEAD, "5000000", data, None) detail = body.split(flow.RULE + "\n", 1)[1].splitlines() assert detail[0] == flow.claim_form.NOT_LOADED_NOTE, body assert detail[1] == f"({why})", body assert "not a high risk claim" in body, body def main() -> int: try: from playwright.sync_api import sync_playwright except ImportError: print(" SKIP playwright is not installed") return 0 failures = 0 with sync_playwright() as pw: browser = pw.chromium.launch(headless=True) page = browser.new_page() 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() print(f"\n{len(CASES) - failures}/{len(CASES)} passed") return 1 if failures else 0 if __name__ == "__main__": sys.exit(main())