Files
ecw-charge-validation/tests/test_telehealth_fix.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

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