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>
645 lines
27 KiB
Python
645 lines
27 KiB
Python
r"""
|
|
Condition 1: a telehealth visit needs POS=11 and M1=GT on every CPT/HCPCS row.
|
|
|
|
python tests\test_telehealth_fix.py
|
|
|
|
Reuses the existing `ub_claim.html` / `progress_note.html` fixtures rather than
|
|
adding new ones — the real note eCW served on 2026-09-07 already documents a
|
|
Telehealth visit (see `progress_note.html`'s History of Present Illness
|
|
section, `<u>TELEHEALTH</u>`), and detection never touches the DOM at all, so
|
|
the negative case is just a plain string.
|
|
|
|
Local headless chromium only, exactly like test_claim_form.py: no proxy, no
|
|
login, no eCW. This is the bot's first WRITE path, so what matters most here
|
|
is proven *before* it ever runs against a real claim:
|
|
|
|
* detection matches "telehealth", "telephone", and a standalone "TH" token
|
|
(case-insensitive for the first two, word-bounded and case-sensitive for
|
|
"TH" — see claim_form.note_mentions_telehealth's docstring for why) and
|
|
nothing looser than that — a payer/claim-specific "TeleHealth" or
|
|
"TELEHEALTH VISIT" must still match, but "MONTH" or "WITH" must not;
|
|
* the write only ever touches real CPT/HCPCS rows — the blank padding row
|
|
and the trailing "*" entry row (neither carries a CPTROW_ id, exactly the
|
|
trap `claim_form.extract()`'s own reader already guards against) must
|
|
never be touched;
|
|
* every field is read back after the fill, so a write that silently didn't
|
|
take is a **visible** assertion failure here, not a silent no-op on a
|
|
real claim;
|
|
* Cancel and OK are both wired in this fixture (they are not in
|
|
ub_claim.html's original form — see the added <script> block) so the
|
|
"declined -> nothing persists" and "confirmed -> the modal actually
|
|
closes" paths are both provably real, not assumed.
|
|
"""
|
|
|
|
import sys
|
|
import pathlib
|
|
import inspect
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT / "src"))
|
|
|
|
FIXTURE = pathlib.Path(__file__).resolve().parent / "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.conditions import telehealth # noqa: E402
|
|
from ecw_bot.selectors import claim_form as sel # noqa: E402
|
|
|
|
CASES = []
|
|
|
|
|
|
def case(fn):
|
|
CASES.append(fn)
|
|
return fn
|
|
|
|
|
|
@case
|
|
def detects_telehealth_case_insensitively():
|
|
assert claim_form.note_mentions_telehealth(
|
|
"Session Setting (PoS): TeleHealth") is True
|
|
assert claim_form.note_mentions_telehealth(
|
|
" ...TELEHEALTH VISIT... ") is True
|
|
assert claim_form.note_mentions_telehealth(
|
|
"Regular in-office visit, no video call.") is False
|
|
assert claim_form.note_mentions_telehealth("") is False
|
|
assert claim_form.note_mentions_telehealth(None) is False
|
|
|
|
|
|
@case
|
|
def detects_telephone_as_the_documented_mode_of_treatment():
|
|
assert claim_form.note_mentions_telehealth(
|
|
"Mode of Treatment: Telephone") is True
|
|
assert claim_form.note_mentions_telehealth(
|
|
"mode of treatment: telephone call") is True
|
|
|
|
|
|
@case
|
|
def detects_standalone_th_token_but_not_inside_other_words():
|
|
# The (TH) tag eCW appends to some procedure code descriptions.
|
|
assert claim_form.note_mentions_telehealth(
|
|
"H0023 Behavioral Health Outreach Service (TH). Modifiers: GT") is True
|
|
assert claim_form.note_mentions_telehealth("Visit type: TH") is True
|
|
# Word-bounded and case-sensitive: must not fire on ordinary English.
|
|
assert claim_form.note_mentions_telehealth(
|
|
"Patient seen this MONTH, follow up WITH PCP.") is False
|
|
assert claim_form.note_mentions_telehealth(
|
|
"the patient arrived on time") is False
|
|
|
|
|
|
@case
|
|
def telehealth_signal_names_which_one_matched():
|
|
"""
|
|
note_mentions_telehealth() only says yes/no; telehealth_signal() is
|
|
the same detection but names which of the three signals actually
|
|
matched, so a condition's message box can tell the operator why it
|
|
fired instead of just that it fired. Checked in the same order
|
|
note_mentions_telehealth() checks them (telehealth, then telephone,
|
|
then the TH tag), so nothing that returns True there ever comes back
|
|
unlabeled here.
|
|
"""
|
|
assert claim_form.telehealth_signal(
|
|
"Session Setting (PoS): TeleHealth") == "telehealth"
|
|
assert claim_form.telehealth_signal(
|
|
"Mode of Treatment: Telephone") == "telephone"
|
|
assert claim_form.telehealth_signal(
|
|
"H0023 Behavioral Health Outreach Service (TH).") == "TH tag"
|
|
# "telehealth" wins when a note happens to carry more than one signal —
|
|
# arbitrary but deterministic, and consistent with the boolean check
|
|
# treating either as sufficient.
|
|
assert claim_form.telehealth_signal(
|
|
"TeleHealth visit, Mode of Treatment: Telephone") == "telehealth"
|
|
assert claim_form.telehealth_signal("Regular in-office visit.") is None
|
|
assert claim_form.telehealth_signal("") is None
|
|
assert claim_form.telehealth_signal(None) is None
|
|
|
|
|
|
@case
|
|
def sets_pos_and_mod1_on_every_real_cpt_row_only(page):
|
|
"""
|
|
ub_claim.html has exactly 2 real CPTROW_ rows (CPTROW_0, CPTROW_1) plus
|
|
one blank "*" entry row with no CPTROW_ id at all, which must not be
|
|
touched — same shape claim_form.extract() already has to filter out.
|
|
"""
|
|
changes = claim_form.set_cpt_pos_modifier(page, pos="11", modifier="GT")
|
|
assert len(changes) == 2, changes
|
|
|
|
row0, row1 = changes
|
|
assert row0["code"] == "H0023", row0
|
|
assert row0["after"]["pos"] == "11" and row0["after"]["mod1"] == "GT", row0
|
|
# Row 0 already held POS=11/M1=GT in the fixture. Before review item D3
|
|
# (2026-10-05) a same-value fill was still written and reported as a
|
|
# change; now an already-correct row is reported changed=False and
|
|
# nothing is written - its M1 is never even blurred.
|
|
assert row0["changed"] is False, row0
|
|
assert row0["after"] == row0["before"], row0
|
|
row0_mod1 = page.locator(sel.SEL_CPT_ROWS).nth(0).locator(sel.SEL_CPT_MOD1).first
|
|
assert row0_mod1.get_attribute("data-blurred") is None, "an already-correct row must not be written at all"
|
|
|
|
assert row1["code"] == "90791", row1
|
|
# Row 1 is the one that actually needed the fix: mod1 started blank.
|
|
assert row1["before"]["mod1"] == "", row1
|
|
assert row1["after"]["pos"] == "11" and row1["after"]["mod1"] == "GT", row1
|
|
assert row1["changed"] is True and row1["ok"] is True, row1
|
|
|
|
# Prove it landed in the live DOM, not just in the returned dict.
|
|
mod1_field = page.locator(sel.SEL_CPT_ROWS).nth(1).locator(sel.SEL_CPT_MOD1).first
|
|
assert mod1_field.input_value() == "GT", mod1_field.input_value()
|
|
|
|
# Prove M1 was explicitly blurred, not just filled. The real DOM (claim
|
|
# 9120000) carries ng-blur="updateCPTMod(...)" on this field — eCW does
|
|
# not commit the modifier to its own model without it, so a fill() with
|
|
# no blur would look correct here and silently not take on a real claim.
|
|
assert mod1_field.get_attribute("data-blurred") == "1", \
|
|
"M1 was filled but never blurred - eCW's updateCPTMod() would not fire"
|
|
|
|
|
|
@case
|
|
def set_cpt_pos_modifier_only_first_stops_after_the_first_real_row(page):
|
|
"""
|
|
Fresh reload: the previous case already wrote to both real rows on this
|
|
page. Used by the Medicare-telehealth condition (document #22), which
|
|
touches only the claim's primary row — proving row 1 is untouched here
|
|
is what makes that condition's scope-down real, not assumed.
|
|
"""
|
|
page.goto(FIXTURE.as_uri())
|
|
changes = claim_form.set_cpt_pos_modifier(
|
|
page, pos="2", modifier="95", only_first=True)
|
|
assert len(changes) == 1, changes
|
|
assert changes[0]["code"] == "H0023", changes
|
|
assert changes[0]["after"]["pos"] == "2", changes
|
|
assert changes[0]["after"]["mod1"] == "95", changes
|
|
|
|
row1_mod1 = page.locator(sel.SEL_CPT_ROWS).nth(1).locator(
|
|
sel.SEL_CPT_MOD1).first
|
|
assert row1_mod1.input_value() != "95", \
|
|
"only_first=True must not touch the second row"
|
|
|
|
|
|
@case
|
|
def retag_cpt_icd_pointer_works_against_the_ub_forms_own_id_prefix(page):
|
|
"""
|
|
test_hcfa_claim.py covers the data-fieldname half of SEL_CPT_ICD*; this
|
|
proves the original uBClaimICD1-4 half (condition #6's primitive is
|
|
shared by both screens) still works unchanged. Both of ub_claim.html's
|
|
real rows (H0023, 90791) hold ICD1="1" and nothing else - "H0023" plays
|
|
the role of a G8510/G8431-equivalent target code here, since the
|
|
fixture carries no real G8510 row of its own.
|
|
"""
|
|
page.goto(FIXTURE.as_uri())
|
|
changes = claim_form.retag_cpt_icd_pointer(
|
|
page, target_pointer="1", target_codes={"H0023"})
|
|
|
|
by_code = {c["code"]: c for c in changes}
|
|
# H0023 (target) already held exactly ["1", "", "", ""] - a no-op.
|
|
assert "H0023" not in by_code, changes
|
|
# 90791 (not a target) held only the pointer being removed - it must
|
|
# end up fully blank, not left with a gap.
|
|
assert by_code["90791"]["before"]["icd"] == ["1", "", "", ""]
|
|
assert by_code["90791"]["after"]["icd"] == ["", "", "", ""]
|
|
|
|
row1_fields = page.locator(sel.SEL_CPT_ROWS).nth(1)
|
|
assert row1_fields.locator(sel.SEL_CPT_ICD1).first.input_value() == ""
|
|
|
|
|
|
@case
|
|
def add_cpt_row_returns_false_when_there_is_no_add_button_at_all(page):
|
|
"""
|
|
ub_claim.html has never captured evidence of the Add button + Fee
|
|
Schedule picker (that flow is only verified on the "Claim"/HCFA and
|
|
Dental screens — see test_hcfa_claim.py) — add_cpt_row() must report
|
|
"could not confirm" here rather than guessing at a flow with no
|
|
evidence behind it on this screen.
|
|
"""
|
|
page.set_content('<div id="UBClaimDiv"></div>')
|
|
# LIVE-WRITE: in VERIFY add_cpt_row() returns False before it ever
|
|
# looks for the button.
|
|
orig = cfg.SAVE_CORRECTIONS
|
|
cfg.SAVE_CORRECTIONS = True
|
|
try:
|
|
assert claim_form.add_cpt_row(page, "G2025") is False
|
|
finally:
|
|
cfg.SAVE_CORRECTIONS = orig
|
|
page.goto(FIXTURE.as_uri())
|
|
|
|
|
|
@case
|
|
def zero_and_uncheck_other_cpt_rows_zeroes_the_fee_on_every_other_real_row(page):
|
|
"""
|
|
Both real rows other than the kept code — H0023 (74.00) and 90791
|
|
(150.00) — must end up at fee 0.00; the kept code itself must be left
|
|
alone entirely (no change entry for it at all). H0023 stands in for
|
|
G2025 here since ub_claim.html has no Add-flow evidence to actually
|
|
seed a real G2025 row with — see test_telehealth_fix.py's own note on
|
|
add_cpt_row above.
|
|
|
|
ub_claim.html's checkboxes carry no ng-click at all (unconfirmed on
|
|
the UB form — see SEL_CPT_ASSIGN_TO_PATIENT's own comment), so
|
|
"unchecked" never appears here; that gap is covered on the HCFA screen
|
|
instead, in test_hcfa_claim.py.
|
|
"""
|
|
page.goto(FIXTURE.as_uri())
|
|
changes = claim_form.zero_and_uncheck_other_cpt_rows(
|
|
page, keep_codes={"H0023"})
|
|
by_code = {c["code"]: c for c in changes}
|
|
assert set(by_code) == {"90791"}, changes
|
|
assert by_code["90791"]["fee"] == {"before": "150.00", "after": "0.00"}
|
|
assert "unchecked" not in by_code["90791"], by_code["90791"]
|
|
|
|
# Landed in the live DOM, not just the returned dict.
|
|
row1_fee = page.locator(sel.SEL_CPT_ROWS).nth(1).locator(
|
|
sel.SEL_CPT_BILLED_FEE).first
|
|
assert row1_fee.input_value() == "0.00"
|
|
|
|
# A second pass has nothing left to zero.
|
|
assert claim_form.zero_and_uncheck_other_cpt_rows(
|
|
page, keep_codes={"H0023"}) == []
|
|
|
|
|
|
@case
|
|
def apply_medicare_telehealth_g2025_skips_cleanup_when_the_add_cannot_be_confirmed(page):
|
|
"""
|
|
The safety gate: if G2025 cannot be confirmed on the claim, nothing
|
|
else gets touched — a half-applied condition (other rows zeroed with
|
|
no G2025 to replace them) would leave the claim with nothing billable
|
|
at all.
|
|
"""
|
|
page.set_content('<div id="UBClaimDiv"></div>')
|
|
orig = cfg.SAVE_CORRECTIONS
|
|
cfg.SAVE_CORRECTIONS = True # LIVE-WRITE: the add is really tried
|
|
try:
|
|
result = claim_form.apply_medicare_telehealth_g2025(page, "95")
|
|
finally:
|
|
cfg.SAVE_CORRECTIONS = orig
|
|
assert result == {"g2025_added": False, "g2025_confirmed": False,
|
|
"g2025_verify_only": False,
|
|
"g2025_modifier_ok": None,
|
|
"g2025_present_modifier": None,
|
|
"g2025_present_modifier_ok": None, "zeroed": []}
|
|
page.goto(FIXTURE.as_uri())
|
|
|
|
|
|
@case
|
|
def telehealth_mode_is_audio_only_when_the_note_says_telephone():
|
|
assert claim_form.telehealth_mode("Mode of Treatment: Telephone") == "audio"
|
|
assert claim_form.telehealth_mode(" ...TELEPHONE call... ") == "audio"
|
|
|
|
|
|
@case
|
|
def telehealth_mode_defaults_to_video_otherwise():
|
|
"""Plain telehealth heading or a standalone TH tag carry no audio-only
|
|
signal, so they take the payer-specific-modifier (GT/95) path."""
|
|
assert claim_form.telehealth_mode("Session Setting (PoS): TeleHealth") == "video"
|
|
assert claim_form.telehealth_mode("Visit type: TH") == "video"
|
|
|
|
|
|
@case
|
|
def telehealth_mode_prefers_audio_when_the_note_mentions_both():
|
|
# "telephone" naming the actual modality used wins over a generic
|
|
# "telehealth" heading elsewhere in the same note.
|
|
assert claim_form.telehealth_mode(
|
|
"TELEHEALTH visit conducted via telephone today.") == "audio"
|
|
|
|
|
|
@case
|
|
def telehealth_mode_recognizes_audio_as_a_signal_even_without_the_word_telephone():
|
|
"""
|
|
Real live example: a progress note's Examination section reading
|
|
"THV Audio Only:Telehealth audio phone call only. No video." never
|
|
once says "telephone" — checking for that word alone missed this
|
|
real claim and would have mis-billed it as a video visit (modifier
|
|
95) instead of audio-only (93).
|
|
"""
|
|
assert claim_form.telehealth_mode(
|
|
"THV Audio Only:Telehealth audio phone call only. No video. "
|
|
"Patient is alert and oriented.") == "audio"
|
|
assert claim_form.telehealth_mode(
|
|
"Audio-only visit, no camera available.") == "audio"
|
|
|
|
|
|
@case
|
|
def th_in_a_modifiers_list_is_the_ob_modifier_not_telehealth():
|
|
"""
|
|
Review item D4 (2026-10-05): 6+ real in-person OB notes carry a Visit
|
|
Codes line like "99213 OB office visit level 3. Modifiers: TH" and
|
|
nothing else telehealth-related. TH there is the obstetric HCPCS
|
|
modifier (document rule #23 adds it for pregnancy), not a telehealth
|
|
tag — the bare \\bTH\\b search used to bill these as telehealth.
|
|
"""
|
|
for text in ("99213 OB office visit level 3. Modifiers: TH",
|
|
"99213 OB office visit level 3. Modifiers: 25, TH",
|
|
"Visit Codes:\n99213 OB office visit level 3. Modifier: TH\n"
|
|
"Follow up in 4 weeks."):
|
|
assert claim_form.note_mentions_telehealth(text) is False, text
|
|
assert claim_form.telehealth_signal(text) is None, text
|
|
|
|
|
|
@case
|
|
def th_in_an_upper_case_or_modifier_s_list_is_still_the_ob_modifier():
|
|
"""
|
|
Review fix round 1 (2026-10-05): the `Modifiers:` label is matched in
|
|
any case and in its "Modifier(s):" spelling — the same obstetric TH,
|
|
written differently, must not flip an in-person OB visit to telehealth.
|
|
"""
|
|
for text in ("99213 OB office visit level 3. MODIFIERS: TH",
|
|
"99213 OB office visit level 3. modifiers: 25, TH",
|
|
"99213 OB office visit level 3. Modifier(s): TH",
|
|
"99213 OB office visit level 3. MODIFIER(S): 25, TH"):
|
|
assert claim_form.telehealth_signal(text) is None, text
|
|
# The codes themselves stay upper-case only: the label match does not
|
|
# widen what counts as a code, and a TH tag after a lower-case word on
|
|
# the same line is still outside the list.
|
|
assert claim_form.telehealth_signal(
|
|
"Modifiers: none. Visit type: TH") == "TH tag"
|
|
|
|
|
|
@case
|
|
def th_tag_and_standalone_th_outside_a_modifiers_list_still_count():
|
|
assert claim_form.telehealth_signal(
|
|
"H0023 Behavioral Health Outreach Service (TH).") == "TH tag"
|
|
assert claim_form.telehealth_signal(
|
|
"H0031 Mental health assess non-physician (TH). Modifiers: HN") \
|
|
== "TH tag"
|
|
assert claim_form.telehealth_signal("Visit type: TH") == "TH tag"
|
|
assert claim_form.telehealth_signal("TELEHEALTH:") == "telehealth"
|
|
# The modifier strip is same-line only: a TH tag on the NEXT line after
|
|
# a Modifiers: list must not be swallowed by it.
|
|
assert claim_form.telehealth_signal(
|
|
"99213 OB visit. Modifiers: 25\nTH") == "TH tag"
|
|
|
|
|
|
@case
|
|
def mode_evidence_explicit_audio_only_phrases():
|
|
cases = {
|
|
"Mode of Treatment: Telephone": "Mode of Treatment: Telephone",
|
|
"THV Audio Only: Telehealth audio phone call only. No video.":
|
|
"Audio Only",
|
|
"Audio-only visit today": "Audio-only",
|
|
"Patient on camera? No video available": "No video",
|
|
"90834 Psychotherapy, 45 minutes (audio).": "(audio)",
|
|
"Telehealth phone call only": "phone call only",
|
|
}
|
|
for text, phrase in cases.items():
|
|
assert claim_form.telehealth_mode_evidence(text) == ("audio", phrase), \
|
|
(text, claim_form.telehealth_mode_evidence(text))
|
|
|
|
|
|
@case
|
|
def mode_evidence_explicit_video_phrases():
|
|
cases = {
|
|
"Mode of Treatment: Video": "Mode of Treatment: Video",
|
|
"real-time audio and video telecommunication": "audio and video",
|
|
"audio/video platform": "audio/video",
|
|
"Telehealth video visit with patient": "video visit",
|
|
"patient seen via video": "via video",
|
|
}
|
|
for text, phrase in cases.items():
|
|
assert claim_form.telehealth_mode_evidence(text) == ("video", phrase), \
|
|
(text, claim_form.telehealth_mode_evidence(text))
|
|
|
|
|
|
@case
|
|
def mode_evidence_audio_only_beats_video_and_video_beats_telephone():
|
|
# Precedence 1 over 2: explicit audio-only wins over a video phrase.
|
|
assert claim_form.telehealth_mode_evidence(
|
|
"Scheduled as a video visit; audio only due to connection.")[0] \
|
|
== "audio"
|
|
# Precedence 2 over 3: explicit video wins over a bare "telephone".
|
|
assert claim_form.telehealth_mode_evidence(
|
|
"Telephone reminder sent; seen via video.") == ("video", "via video")
|
|
|
|
|
|
@case
|
|
def mode_evidence_bare_telephone_is_still_audio():
|
|
assert claim_form.telehealth_mode_evidence(
|
|
"seen via telephone visit") == ("audio", "telephone")
|
|
assert claim_form.telehealth_mode_evidence(
|
|
"Telephone encounter regarding labs") == ("audio", "Telephone")
|
|
|
|
|
|
@case
|
|
def mode_evidence_defaults_to_video_with_no_phrase():
|
|
assert claim_form.telehealth_mode_evidence(
|
|
"Session Setting (PoS) Telehealth") == ("video", "")
|
|
assert claim_form.telehealth_mode_evidence("") == ("video", "")
|
|
assert claim_form.telehealth_mode_evidence(None) == ("video", "")
|
|
|
|
|
|
@case
|
|
def audiology_is_not_audio_evidence():
|
|
"""The old bare "audio" substring check called these audio-only."""
|
|
assert claim_form.telehealth_mode(
|
|
"TELEHEALTH: Patient referred to Audiology.") == "video"
|
|
assert claim_form.telehealth_mode(
|
|
"Telehealth visit. Audiometry results reviewed.") == "video"
|
|
|
|
|
|
@case
|
|
def consent_boilerplate_alone_does_not_decide_the_mode():
|
|
consent = ("Patient consented to behavioral health treatment by "
|
|
"telehealth (phone and/or video).")
|
|
assert claim_form.telehealth_mode_evidence(consent) == ("video", "")
|
|
assert claim_form.telehealth_mode_evidence(
|
|
consent + "\nMode of Treatment: Telephone") \
|
|
== ("audio", "Mode of Treatment: Telephone")
|
|
|
|
|
|
@case
|
|
def describe_mode_names_the_phrase_or_the_default():
|
|
assert claim_form.describe_mode("audio", "audio only") \
|
|
== 'mode audio (matched "audio only")'
|
|
assert claim_form.describe_mode("video", "") \
|
|
== "mode video (default — no audio-only evidence)"
|
|
|
|
|
|
def _mark_writes(page):
|
|
"""Flag every POS/M1 input that receives an `input` event (Playwright's
|
|
fill() dispatches one), so a test can prove a field was NOT written."""
|
|
page.evaluate("""(s) => document.querySelectorAll(s).forEach(el =>
|
|
el.addEventListener('input', () => { el.dataset.written = '1'; }))""",
|
|
f"{sel.SEL_CPT_POS}, {sel.SEL_CPT_MOD1}")
|
|
|
|
|
|
@case
|
|
def already_correct_row_with_02_and_lowercase_gt_writes_nothing(page):
|
|
"""
|
|
Review item D3: eCW's "02" is POS 2, and "gt" is GT — a row holding
|
|
those is already correct. Nothing is written (no fill, no blur) and the
|
|
row comes back changed=False with after == before.
|
|
"""
|
|
page.goto(FIXTURE.as_uri())
|
|
row0 = page.locator(sel.SEL_CPT_ROWS).nth(0)
|
|
row0.locator(sel.SEL_CPT_POS).first.fill("02")
|
|
row0.locator(sel.SEL_CPT_MOD1).first.fill("gt")
|
|
_mark_writes(page)
|
|
|
|
changes = claim_form.set_cpt_pos_modifier(
|
|
page, pos="2", modifier="GT", only_first=True)
|
|
assert changes == [{"row": 0, "code": "H0023",
|
|
"before": {"pos": "02", "mod1": "gt"},
|
|
"after": {"pos": "02", "mod1": "gt"},
|
|
"changed": False}], changes
|
|
assert row0.locator(sel.SEL_CPT_POS).first.get_attribute(
|
|
"data-written") is None
|
|
assert row0.locator(sel.SEL_CPT_MOD1).first.get_attribute(
|
|
"data-written") is None
|
|
# only_first stops at the first REAL row even when it did not change.
|
|
row1_mod1 = page.locator(sel.SEL_CPT_ROWS).nth(1).locator(
|
|
sel.SEL_CPT_MOD1).first
|
|
assert row1_mod1.input_value() == "" and \
|
|
row1_mod1.get_attribute("data-written") is None
|
|
page.goto(FIXTURE.as_uri())
|
|
|
|
|
|
@case
|
|
def only_the_differing_field_is_written(page):
|
|
"""Row 1 of ub_claim.html holds POS 11 and a blank M1: asked for POS 11
|
|
+ GT, only M1 is filled (and blurred) — POS is never touched."""
|
|
page.goto(FIXTURE.as_uri())
|
|
_mark_writes(page)
|
|
changes = claim_form.set_cpt_pos_modifier(page, pos="11", modifier="GT")
|
|
row1 = changes[1]
|
|
assert row1["changed"] is True and row1["ok"] is True, row1
|
|
assert row1["after"] == {"pos": "11", "mod1": "GT"}, row1
|
|
r1 = page.locator(sel.SEL_CPT_ROWS).nth(1)
|
|
assert r1.locator(sel.SEL_CPT_POS).first.get_attribute(
|
|
"data-written") is None, "POS already matched - must not be filled"
|
|
assert r1.locator(sel.SEL_CPT_MOD1).first.get_attribute(
|
|
"data-written") == "1"
|
|
assert r1.locator(sel.SEL_CPT_MOD1).first.get_attribute(
|
|
"data-blurred") == "1"
|
|
page.goto(FIXTURE.as_uri())
|
|
|
|
|
|
@case
|
|
def telehealth_apply_twice_is_idempotent(page):
|
|
"""
|
|
Review item D3's apply-twice test: the first apply fixes the primary
|
|
row (POS 11 -> 2), the second finds it already correct and returns
|
|
none() — not a change, not a review.
|
|
"""
|
|
page.goto(FIXTURE.as_uri())
|
|
data = {"progress_notes": {"text": "Session Setting (PoS): TeleHealth"},
|
|
"insurance": [{"Name": "BH COLORADO ACCESS"}]}
|
|
first = telehealth.apply(page, "9120000", data)
|
|
assert first.changed is True and first.review == "", first
|
|
second = telehealth.apply(page, "9120000", data)
|
|
assert second == telehealth.ConditionResult.none("telehealth"), second
|
|
page.goto(FIXTURE.as_uri())
|
|
|
|
|
|
@case
|
|
def detects_ub_claim_form_via_stable_markers(page):
|
|
"""ub_claim.html carries both #UBClaimBtn17 and #BillType, exactly like
|
|
a real UB Claim form — this is the positive case. The negative case
|
|
(the professional/HCFA "Claim" screen) is covered indirectly by the
|
|
close_claim() fallback test below, which builds a page with neither
|
|
marker present."""
|
|
assert claim_form.is_ub_claim_form(page) is True
|
|
|
|
|
|
@case
|
|
def declining_leaves_the_form_open_and_untouched_by_cancel(page):
|
|
"""Cancel is wired in the fixture; nothing about it is telehealth-specific
|
|
— it is the same button every other read-only pass in this codebase uses,
|
|
proven here so the "declined" branch of the flow has somewhere real to
|
|
land."""
|
|
assert page.locator(sel.SEL_MODAL).first.is_visible()
|
|
assert claim_form.close_claim(page) is True
|
|
assert not page.locator(sel.SEL_MODAL).first.is_visible()
|
|
|
|
|
|
@case
|
|
def save_clicks_ok_and_the_modal_closes(page):
|
|
"""A fresh page load: the previous case already closed this one via
|
|
Cancel."""
|
|
page.goto(FIXTURE.as_uri())
|
|
assert page.locator(sel.SEL_MODAL).first.is_visible()
|
|
assert claim_form.save_claim(page) is True
|
|
assert not page.locator(sel.SEL_MODAL).first.is_visible()
|
|
|
|
|
|
@case
|
|
def save_claim_falls_back_to_hcfa_ok_when_ub_button_is_absent(page):
|
|
"""
|
|
The professional/HCFA "Claim" screen has no #UBClaimBtn18 — only its own
|
|
save button, verified against a live claim:
|
|
ng-click="currentButtonAction='';saveAllData()" (no `true` argument,
|
|
unlike the UB form's saveAllData(true)). Without this fallback,
|
|
save_claim() would exhaust the UB attempt and give up, leaving a
|
|
confirmed correction unsaved on that screen.
|
|
"""
|
|
page.set_content(
|
|
'<div id="UBClaimDiv" style="display:block">'
|
|
"<button ng-click=\"currentButtonAction='';saveAllData()\" "
|
|
'onclick="document.getElementById(\'UBClaimDiv\').style.display='
|
|
'\'none\'">OK</button>'
|
|
'</div>')
|
|
assert page.locator(sel.SEL_MODAL).first.is_visible()
|
|
assert claim_form.save_claim(page) is True
|
|
assert not page.locator(sel.SEL_MODAL).first.is_visible()
|
|
# Fresh reload so the next case isn't left on this synthetic page.
|
|
page.goto(FIXTURE.as_uri())
|
|
|
|
|
|
@case
|
|
def close_claim_falls_back_to_hcfa_cancel_when_ub_buttons_are_absent(page):
|
|
"""
|
|
The professional/HCFA "Claim" screen (Commercial payers) has neither
|
|
#UBClaimBtn17 nor #UBClaimBtn3 — only its own differently-named Cancel
|
|
(verified against a live claim: ng-click="closeClaimScreen()"). Without
|
|
this fallback, close_claim() would exhaust both UB attempts and give up,
|
|
leaving that claim's modal open for the next one in the loop to collide
|
|
with. Last case in this file: replaces the page content entirely, so
|
|
nothing after it can assume the UB fixture is still loaded.
|
|
"""
|
|
page.set_content(
|
|
'<div id="UBClaimDiv" style="display:block">'
|
|
'<button ng-click="closeClaimScreen()" '
|
|
'onclick="document.getElementById(\'UBClaimDiv\').style.display='
|
|
'\'none\'">Cancel</button>'
|
|
'</div>')
|
|
assert page.locator(sel.SEL_MODAL).first.is_visible()
|
|
assert claim_form.is_ub_claim_form(page) is False
|
|
assert claim_form.close_claim(page) is True
|
|
assert not page.locator(sel.SEL_MODAL).first.is_visible()
|
|
|
|
|
|
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
|
|
with sync_playwright() as pw:
|
|
browser = pw.chromium.launch(headless=True) # local only, no proxy
|
|
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()
|
|
|
|
print(f"\n{len(CASES) - failures}/{len(CASES)} passed")
|
|
return 1 if failures else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|