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>
This commit is contained in:
commit
ba15a574ab
205 files changed
+56772
No files matched your search
@@ -0,0 +1,542 @@
|
||||
r"""
|
||||
The professional/HCFA "Claim" screen (Commercial payers) has no separate
|
||||
CPT/HCPCS tab and a different set of per-field ids from the UB Claim form —
|
||||
see hcfa_claim.html's own comment for exactly what differs and why.
|
||||
|
||||
python tests\test_hcfa_claim.py
|
||||
|
||||
Real live bug this guards (claim 9103000, Logs\run_20260917_162939.log):
|
||||
`open_cpt_hcpcs_tab()` waited 30s for a tab that does not exist on this
|
||||
screen, raised, and aborted the whole claim read before Progress Notes was
|
||||
ever reached — so it never opened for that claim at all. Fixed by making
|
||||
`open_cpt_hcpcs_tab()` a no-op here, and by giving `extract()` and
|
||||
`set_cpt_pos_modifier()` a `data-fieldname` fallback for this screen's
|
||||
differently-shaped CPT row fields.
|
||||
|
||||
Since then: ICD and the payer (#billingClaimTbl8) are read here too, and
|
||||
close_claim() no longer Escapes this form's own window. The rest of
|
||||
extract() (header, revenue, payments) is still UB-only.
|
||||
"""
|
||||
|
||||
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" / "hcfa_claim.html"
|
||||
|
||||
from ecw_bot.pages import claim_form # noqa: E402
|
||||
from ecw_bot.selectors import claim_form as sel # noqa: E402
|
||||
|
||||
|
||||
CASES = []
|
||||
|
||||
|
||||
def case(fn):
|
||||
CASES.append(fn)
|
||||
return fn
|
||||
|
||||
|
||||
@case
|
||||
def is_ub_claim_form_is_false_for_the_hcfa_screen(page):
|
||||
assert claim_form.is_ub_claim_form(page) is False
|
||||
|
||||
|
||||
@case
|
||||
def open_cpt_hcpcs_tab_is_a_no_op_here_and_does_not_raise(page):
|
||||
"""The real bug: this used to wait 30s for a tab that will never appear,
|
||||
then raise and abort the whole claim read. It must now return promptly
|
||||
without touching the page at all."""
|
||||
assert claim_form.open_cpt_hcpcs_tab(page) is False
|
||||
# Still on the same screen, untouched — no exception propagated.
|
||||
assert claim_form.is_ub_claim_form(page) is False
|
||||
|
||||
|
||||
@case
|
||||
def extract_reads_all_three_real_cpt_rows_via_data_fieldname(page):
|
||||
data = claim_form.extract(page)
|
||||
cpt = data.get("cpt") or []
|
||||
assert len(cpt) == 3, cpt
|
||||
|
||||
row0, row1, row2 = cpt
|
||||
assert row0["code"] == "99213", row0
|
||||
assert row0["pos"] == "11" and row0["mod1"] == "", row0
|
||||
assert row0["icd1"] == "1" and row0["icd2"] == "2" and row0["icd3"] == "3", row0
|
||||
|
||||
assert row1["code"] == "J7298", row1
|
||||
assert row1["pos"] == "11" and row1["mod1"] == "", row1
|
||||
|
||||
assert row2["code"] == "G8510", row2
|
||||
assert row2["icd1"] == "1", row2
|
||||
|
||||
|
||||
@case
|
||||
def extract_reads_the_icd_table_via_id_prefix_on_the_hcfa_screen(page):
|
||||
data = claim_form.extract(page)
|
||||
icd = data.get("icd") or []
|
||||
assert len(icd) == 2, icd
|
||||
assert icd[0]["#"] == "1" and icd[0]["Code"] == "Z30.430", icd[0]
|
||||
assert icd[1]["#"] == "2" and icd[1]["Code"] == "Z13.31", icd[1]
|
||||
|
||||
|
||||
@case
|
||||
def extract_reads_the_payer_from_the_insurances_and_payment_grid(page):
|
||||
"""
|
||||
Every live HCFA snapshot had `insurance: []` (9109000, 9119000 —
|
||||
extract() only knew the UB form's #UBClaimTbl3), so telehealth never
|
||||
chose 95 and vaccine_admin_codes never fired on a Commercial claim.
|
||||
#billingClaimTbl8 carries the UB table's columns; #billingClaimTbl5's
|
||||
Name cell also holds an edit input, which the value-first cell reader
|
||||
would return instead of the payer.
|
||||
"""
|
||||
page.goto(FIXTURE.as_uri())
|
||||
ins = claim_form.extract(page).get("insurance") or []
|
||||
assert len(ins) == 1, ins
|
||||
assert ins[0]["Name"] == "United Health Care", ins[0]
|
||||
assert ins[0]["Ins Id"] == "1660", ins[0]
|
||||
|
||||
|
||||
@case
|
||||
def set_cpt_pos_modifier_writes_via_data_fieldname_and_blurs_mod1(page):
|
||||
changes = claim_form.set_cpt_pos_modifier(page, pos="2", modifier="95")
|
||||
assert len(changes) == 3, changes
|
||||
|
||||
row0, row1, row2 = changes
|
||||
assert row0["code"] == "99213" and row0["after"]["pos"] == "2" \
|
||||
and row0["after"]["mod1"] == "95", row0
|
||||
assert row1["code"] == "J7298" and row1["after"]["pos"] == "2" \
|
||||
and row1["after"]["mod1"] == "95", row1
|
||||
assert row2["code"] == "G8510" and row2["after"]["pos"] == "2" \
|
||||
and row2["after"]["mod1"] == "95", row2
|
||||
|
||||
mod1_field = page.locator(sel.SEL_CPT_ROWS).nth(0).locator(
|
||||
sel.SEL_CPT_MOD1).first
|
||||
assert mod1_field.get_attribute("data-blurred") == "1", \
|
||||
"M1 was filled but never blurred - updateCPTMod() would not fire"
|
||||
|
||||
|
||||
@case
|
||||
def retag_cpt_icd_pointer_moves_the_depression_pointer_onto_g8510_only(page):
|
||||
"""
|
||||
The real bug (claim 9103000): 99213 wrongly carried the depression
|
||||
pointer ("2") in ICD2 alongside its own diagnosis in ICD1 (and, in this
|
||||
fixture, a third pointer "3" in ICD3 to prove the shift-left behaviour).
|
||||
G8510 wrongly carried "1" (Z30.430) instead of the depression pointer.
|
||||
J7298 carries neither and must be left untouched entirely.
|
||||
"""
|
||||
page.goto(FIXTURE.as_uri())
|
||||
changes = claim_form.retag_cpt_icd_pointer(
|
||||
page, target_pointer="2", target_codes={"G8510", "G8431"})
|
||||
|
||||
by_code = {c["code"]: c for c in changes}
|
||||
assert set(by_code) == {"99213", "G8510"}, changes
|
||||
|
||||
row0 = by_code["99213"]
|
||||
assert row0["before"]["icd"] == ["1", "2", "3", ""], row0
|
||||
assert row0["after"]["icd"] == ["1", "3", "", ""], row0
|
||||
|
||||
row2 = by_code["G8510"]
|
||||
assert row2["before"]["icd"] == ["1", "", "", ""], row2
|
||||
assert row2["after"]["icd"] == ["2", "", "", ""], row2
|
||||
|
||||
# Read back from the live DOM, not just the returned dict.
|
||||
row0_fields = page.locator(sel.SEL_CPT_ROWS).nth(0)
|
||||
assert [row0_fields.locator(s).first.input_value() for s in sel.SEL_CPT_ICD] \
|
||||
== ["1", "3", "", ""]
|
||||
row2_fields = page.locator(sel.SEL_CPT_ROWS).nth(2)
|
||||
assert [row2_fields.locator(s).first.input_value() for s in sel.SEL_CPT_ICD] \
|
||||
== ["2", "", "", ""]
|
||||
|
||||
# J7298 (row 1) never carried the pointer - untouched, not even read-written.
|
||||
row1_fields = page.locator(sel.SEL_CPT_ROWS).nth(1)
|
||||
assert row1_fields.locator(sel.SEL_CPT_ICD1).first.input_value() == "1"
|
||||
|
||||
|
||||
@case
|
||||
def retag_cpt_icd_pointer_is_a_no_op_on_an_already_correct_claim(page):
|
||||
page.goto(FIXTURE.as_uri())
|
||||
claim_form.retag_cpt_icd_pointer(
|
||||
page, target_pointer="2", target_codes={"G8510", "G8431"})
|
||||
# Second pass over the now-corrected claim: nothing left to change.
|
||||
changes = claim_form.retag_cpt_icd_pointer(
|
||||
page, target_pointer="2", target_codes={"G8510", "G8431"})
|
||||
assert changes == [], changes
|
||||
|
||||
|
||||
@case
|
||||
def ensure_cpt_rows_checked_checks_the_unchecked_g8510_row(page):
|
||||
"""
|
||||
The real bug report (claim 9103000): G8510's own checkbox sat
|
||||
unchecked - excluded from the claim - and needed checking once its
|
||||
ICD pointer was corrected. 99213/J7298 are already checked and are not
|
||||
target codes anyway; neither should be touched.
|
||||
"""
|
||||
page.goto(FIXTURE.as_uri())
|
||||
changes = claim_form.ensure_cpt_rows_checked(
|
||||
page, {"G8510", "G8431"})
|
||||
assert changes == [{"row": 2, "code": "G8510"}], changes
|
||||
|
||||
checkbox = page.locator(sel.SEL_CPT_ROWS).nth(2).locator(
|
||||
sel.SEL_CPT_ASSIGN_TO_PATIENT).first
|
||||
assert checkbox.is_checked() is True
|
||||
|
||||
|
||||
@case
|
||||
def ensure_cpt_rows_checked_is_a_no_op_once_already_checked(page):
|
||||
page.goto(FIXTURE.as_uri())
|
||||
claim_form.ensure_cpt_rows_checked(page, {"G8510", "G8431"})
|
||||
changes = claim_form.ensure_cpt_rows_checked(page, {"G8510", "G8431"})
|
||||
assert changes == [], changes
|
||||
|
||||
|
||||
def _inject_g8510_duplicate_row(page, checked=False, fee="0.00"):
|
||||
"""
|
||||
A second G8510 row, after the fixture's own (CPTROW_2) — injected the
|
||||
same way `_inject_j2001_row` below adds its row, rather than growing
|
||||
the shared fixture and its row-count assumptions.
|
||||
"""
|
||||
page.evaluate(
|
||||
"""(args) => {
|
||||
const tbody = document.querySelector(
|
||||
'#claimCPTCodesTable8888888888888 tbody');
|
||||
const addRowTr = tbody.querySelector('tr:last-child');
|
||||
const tr = document.createElement('tr');
|
||||
tr.id = 'CPTROW_3';
|
||||
tr.innerHTML =
|
||||
'<td><input type="checkbox" ng-click="updateBillToIns(3)"'
|
||||
+ (args.checked ? ' checked' : '') + '></td>'
|
||||
+ '<td><input data-fieldname="claimCPTCode" value="G8510" readonly></td>'
|
||||
+ '<td><input data-fieldname="ClaimCPTBilledFee" value="' + args.fee + '"></td>';
|
||||
tbody.insertBefore(tr, addRowTr);
|
||||
}""", {"checked": checked, "fee": fee})
|
||||
|
||||
|
||||
@case
|
||||
def ensure_cpt_rows_checked_checks_only_the_first_row_for_a_code(page):
|
||||
"""
|
||||
Live claim 9131000: a second G8510 row, already excluded, got checked
|
||||
here and then unchecked again by zero_and_uncheck_duplicate_cpt_rows()
|
||||
— a no-op on the claim, reported as two contradictory changes. Only the
|
||||
first real row for a code is this function's to check. Both start
|
||||
unchecked: the first is checked, the second is left alone, and a second
|
||||
pass (the first now checked) must not fall through to the duplicate.
|
||||
"""
|
||||
page.goto(FIXTURE.as_uri())
|
||||
_inject_g8510_duplicate_row(page, checked=False)
|
||||
|
||||
changes = claim_form.ensure_cpt_rows_checked(page, {"G8510", "G8431"})
|
||||
assert changes == [{"row": 2, "code": "G8510"}], changes
|
||||
|
||||
rows = page.locator(sel.SEL_CPT_ROWS)
|
||||
assert rows.nth(2).locator(sel.SEL_CPT_ASSIGN_TO_PATIENT).first.is_checked() is True
|
||||
assert rows.nth(3).locator(sel.SEL_CPT_ASSIGN_TO_PATIENT).first.is_checked() is False
|
||||
|
||||
changes = claim_form.ensure_cpt_rows_checked(page, {"G8510", "G8431"})
|
||||
assert changes == [], changes
|
||||
assert rows.nth(3).locator(sel.SEL_CPT_ASSIGN_TO_PATIENT).first.is_checked() is False
|
||||
|
||||
|
||||
@case
|
||||
def ensure_cpt_rows_checked_checks_the_priced_copy_not_a_zero_fee_first_g8510(page):
|
||||
"""
|
||||
The operator's common rule (zero_fee_uncheck.py, 2026-09-30): a line
|
||||
billed at zero is never billed. zero_fee_uncheck runs first and
|
||||
unchecks a checked 0.00 G8510; checking it again here would be the
|
||||
same check/uncheck toggle as claim 9131000, between two conditions
|
||||
instead of two steps of one. Blank is zero by the same shared rule.
|
||||
|
||||
Flipped by the operator's ruling of the same day: the row kept for a
|
||||
code is the first whose fee is NOT zero, so the priced G8510 further
|
||||
down is the one re-included here — `kept_cpt_row_per_code()`, the same
|
||||
helper `zero_and_uncheck_duplicate_cpt_rows()` uses. It used to be left
|
||||
unchecked too ("first" by position alone), leaving no G8510 billed.
|
||||
"""
|
||||
for fee in ("0.00", ""):
|
||||
page.goto(FIXTURE.as_uri())
|
||||
page.evaluate("(fee) => { document.getElementById('billingClaimIpt17ngR2')"
|
||||
".value = fee; }", fee)
|
||||
_inject_g8510_duplicate_row(page, checked=False, fee="65.00")
|
||||
|
||||
changes = claim_form.ensure_cpt_rows_checked(page, {"G8510", "G8431"})
|
||||
assert changes == [{"row": 3, "code": "G8510"}], (fee, changes)
|
||||
|
||||
rows = page.locator(sel.SEL_CPT_ROWS)
|
||||
assert rows.nth(2).locator(sel.SEL_CPT_ASSIGN_TO_PATIENT).first.is_checked() is False, fee
|
||||
assert rows.nth(3).locator(sel.SEL_CPT_ASSIGN_TO_PATIENT).first.is_checked() is True, fee
|
||||
assert rows.nth(2).locator(sel.SEL_CPT_BILLED_FEE).first.input_value() == fee, \
|
||||
"the fee must never be written"
|
||||
assert rows.nth(3).locator(sel.SEL_CPT_BILLED_FEE).first.input_value() == "65.00"
|
||||
|
||||
|
||||
@case
|
||||
def set_cpt_pos_modifier_only_first_stops_after_row_0(page):
|
||||
page.goto(FIXTURE.as_uri())
|
||||
changes = claim_form.set_cpt_pos_modifier(
|
||||
page, pos="2", modifier="93", only_first=True)
|
||||
assert len(changes) == 1, changes
|
||||
assert changes[0]["code"] == "99213", changes
|
||||
|
||||
row1_mod1 = page.locator(sel.SEL_CPT_ROWS).nth(1).locator(
|
||||
sel.SEL_CPT_MOD1).first
|
||||
assert row1_mod1.input_value() != "93", \
|
||||
"only_first=True must not touch the second row on the HCFA screen"
|
||||
|
||||
|
||||
@case
|
||||
def zero_and_uncheck_other_cpt_rows_unchecks_the_checked_rows_here(page):
|
||||
"""
|
||||
The checkbox half of document condition #22, only provable on this
|
||||
screen: ub_claim.html's checkboxes carry no ng-click at all, but this
|
||||
fixture's do (`ng-click="updateBillToIns($index)"`, verified live on
|
||||
claim 9103000). 99213 and J7298 start checked with real fees; G8510
|
||||
starts unchecked on purpose and is the kept code here, so it must
|
||||
come back with no change entry at all.
|
||||
"""
|
||||
page.goto(FIXTURE.as_uri())
|
||||
changes = claim_form.zero_and_uncheck_other_cpt_rows(
|
||||
page, keep_codes={"G8510"})
|
||||
by_code = {c["code"]: c for c in changes}
|
||||
assert set(by_code) == {"99213", "J7298"}, changes
|
||||
assert by_code["99213"]["unchecked"] is True
|
||||
assert by_code["99213"]["fee"] == {"before": "210.00", "after": "0.00"}
|
||||
assert by_code["J7298"]["unchecked"] is True
|
||||
assert by_code["J7298"]["fee"] == {"before": "1200.00", "after": "0.00"}
|
||||
assert "G8510" not in by_code, changes
|
||||
|
||||
row0_checkbox = page.locator(sel.SEL_CPT_ROWS).nth(0).locator(
|
||||
sel.SEL_CPT_ASSIGN_TO_PATIENT).first
|
||||
assert row0_checkbox.is_checked() is False
|
||||
row2_fee = page.locator(sel.SEL_CPT_ROWS).nth(2).locator(
|
||||
sel.SEL_CPT_BILLED_FEE).first
|
||||
assert row2_fee.input_value() == "65.00", \
|
||||
"the kept code's fee must be left alone"
|
||||
|
||||
|
||||
def _inject_j2001_row(page, checked=True, fee="45.00"):
|
||||
"""
|
||||
hcfa_claim.html has no J2001 row of its own — injected the same way
|
||||
the medicare_telehealth tests inject a G2025 row, mirroring the real
|
||||
markup this fixture already uses for its own checked/fee rows.
|
||||
"""
|
||||
page.evaluate(
|
||||
"""(args) => {
|
||||
const tbody = document.querySelector(
|
||||
'#claimCPTCodesTable8888888888888 tbody');
|
||||
const addRowTr = tbody.querySelector('tr:last-child');
|
||||
const tr = document.createElement('tr');
|
||||
tr.id = 'CPTROW_3';
|
||||
tr.innerHTML =
|
||||
'<td><input type="checkbox" ng-click="updateBillToIns(3)"'
|
||||
+ (args.checked ? ' ng-checked="cpt.BillToIns==\\'1\\'" checked' : '')
|
||||
+ '></td>'
|
||||
+ '<td><input data-fieldname="claimCPTCode" value="J2001" readonly></td>'
|
||||
+ '<td><input data-fieldname="ClaimCPTBilledFee" value="' + args.fee + '"></td>';
|
||||
tbody.insertBefore(tr, addRowTr);
|
||||
}""", {"checked": checked, "fee": fee})
|
||||
|
||||
|
||||
@case
|
||||
def zero_and_uncheck_cpt_rows_excludes_only_the_target_codes(page):
|
||||
"""
|
||||
Document condition #5's J2001 half: "this code can be unchecked in
|
||||
the claim" plus zeroing its fee, the same uncheck+zero shape as
|
||||
condition #22's cleanup step but inverted — only the target codes are
|
||||
touched, every other row (99213, J7298, G8510) is left alone.
|
||||
"""
|
||||
page.goto(FIXTURE.as_uri())
|
||||
_inject_j2001_row(page, checked=True, fee="45.00")
|
||||
|
||||
changes = claim_form.zero_and_uncheck_cpt_rows(page, {"J2001"})
|
||||
by_code = {c["code"]: c for c in changes}
|
||||
assert set(by_code) == {"J2001"}, changes
|
||||
assert by_code["J2001"]["unchecked"] is True
|
||||
assert by_code["J2001"]["fee"] == {"before": "45.00", "after": "0.00"}
|
||||
|
||||
j2001_row = page.locator(sel.SEL_CPT_ROWS).nth(3)
|
||||
assert j2001_row.locator(sel.SEL_CPT_ASSIGN_TO_PATIENT).first.is_checked() is False
|
||||
assert j2001_row.locator(sel.SEL_CPT_BILLED_FEE).first.input_value() == "0.00"
|
||||
|
||||
row0_fee = page.locator(sel.SEL_CPT_ROWS).nth(0).locator(
|
||||
sel.SEL_CPT_BILLED_FEE).first
|
||||
assert row0_fee.input_value() == "210.00", \
|
||||
"an untargeted row's fee must be left alone"
|
||||
row0_checkbox = page.locator(sel.SEL_CPT_ROWS).nth(0).locator(
|
||||
sel.SEL_CPT_ASSIGN_TO_PATIENT).first
|
||||
assert row0_checkbox.is_checked() is True, \
|
||||
"an untargeted row's checkbox must be left alone"
|
||||
|
||||
# A second pass has nothing left to touch.
|
||||
assert claim_form.zero_and_uncheck_cpt_rows(page, {"J2001"}) == []
|
||||
|
||||
|
||||
@case
|
||||
def add_cpt_row_uses_the_real_add_button_and_fee_schedule_picker(page):
|
||||
"""
|
||||
The real flow (claim 9109000's own live evidence): click "Add", search
|
||||
the picker for the code, select the one exact match, click the
|
||||
picker's own OK — never typing straight into the grid's trailing "*"
|
||||
row, which is a different field entirely.
|
||||
"""
|
||||
page.goto(FIXTURE.as_uri())
|
||||
assert claim_form.cpt_code_present(page, "G2025") is False
|
||||
assert claim_form.add_cpt_row(page, "G2025") is True
|
||||
assert claim_form.cpt_code_present(page, "G2025") is True
|
||||
|
||||
assert page.locator(sel.SEL_CPT_ROWS).count() == 4
|
||||
new_row = page.locator(sel.SEL_CPT_ROWS).last
|
||||
assert new_row.locator(sel.SEL_CPT_CODE).first.input_value() == "G2025"
|
||||
# The picker itself must be closed afterward, not left open over the claim.
|
||||
assert page.locator("#claimFeeScheduleModal").is_visible() is False
|
||||
|
||||
|
||||
@case
|
||||
def add_cpt_row_searches_by_the_real_search_field_not_a_substring_of_everything(page):
|
||||
"""
|
||||
The catalog also carries G2026 (a decoy), which starts with the same
|
||||
"G20" prefix — proves the search actually narrows to the exact code
|
||||
asked for, not just whatever the picker happened to show first.
|
||||
"""
|
||||
page.goto(FIXTURE.as_uri())
|
||||
assert claim_form.add_cpt_row(page, "G2026") is True
|
||||
new_row = page.locator(sel.SEL_CPT_ROWS).last
|
||||
assert new_row.locator(sel.SEL_CPT_CODE).first.input_value() == "G2026"
|
||||
|
||||
|
||||
@case
|
||||
def add_cpt_row_cancels_the_picker_and_returns_false_on_no_match(page):
|
||||
"""
|
||||
A code the seeded catalog has no match for at all. Must close the
|
||||
picker with its own Cancel rather than leave it open — an open Fee
|
||||
Schedule modal would block every click on the claim form behind it,
|
||||
the exact class of bug dismiss_stray_modal() exists to recover from.
|
||||
"""
|
||||
page.goto(FIXTURE.as_uri())
|
||||
assert claim_form.add_cpt_row(page, "NOPE99") is False
|
||||
assert page.locator("#claimFeeScheduleModal").is_visible() is False
|
||||
|
||||
|
||||
@case
|
||||
def open_progress_notes_works_the_same_as_the_ub_form(page):
|
||||
page.goto(FIXTURE.as_uri())
|
||||
note = claim_form.open_progress_notes(page)
|
||||
assert note.get("opened") not in (None, "absent", "disabled", "nothing"), note
|
||||
|
||||
|
||||
@case
|
||||
def close_claim_uses_the_real_cancel_not_escape_no_wasted_fallback(page):
|
||||
"""
|
||||
Live, on every HCFA claim the bot closed itself (9109000,
|
||||
Logs\\run_20260929_160143.log): dismiss_stray_modal() took this form's
|
||||
own uib window (.claimModalBackdrop) for a stray panel and pressed
|
||||
Escape, which closed the form without closeClaimScreen() — then Cancel,
|
||||
X and the Dental Cancel each timed out on buttons already gone. ~45s
|
||||
and a false "Could not close the claim form", per claim.
|
||||
"""
|
||||
import time
|
||||
page.goto(FIXTURE.as_uri())
|
||||
assert page.locator("#billingClaimBtn36").first.is_visible()
|
||||
started = time.monotonic()
|
||||
assert claim_form.close_claim(page) is True
|
||||
elapsed = time.monotonic() - started
|
||||
assert page.evaluate("window.closeClaimScreenRan") is True, \
|
||||
"the form was closed by Escape, not by its own Cancel"
|
||||
assert elapsed < 5, f"took {elapsed:.1f}s — a fallback was tried"
|
||||
assert not page.locator("#billingClaimBtn36").first.is_visible()
|
||||
|
||||
|
||||
@case
|
||||
def close_claim_still_dismisses_a_real_stray_panel_over_the_form(page):
|
||||
"""
|
||||
The claim's own window is spared, not the stray check as a whole: a
|
||||
.claimModalBackdrop panel with no claim form inside it, sitting on top,
|
||||
still gets its Escape — and the form under it is then closed by Cancel.
|
||||
"""
|
||||
page.goto(FIXTURE.as_uri())
|
||||
page.evaluate("""() => {
|
||||
const p = document.createElement('div');
|
||||
p.className = 'modal fade ng-isolate-scope claimModalBackdrop in';
|
||||
p.id = 'strayPanel';
|
||||
p.style.cssText = 'position:fixed;top:0;left:0;width:100%;'
|
||||
+ 'height:100%;z-index:2000;display:block;';
|
||||
document.body.appendChild(p);
|
||||
// Escape goes to the topmost window only: the stray panel, first.
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && p.isConnected) {
|
||||
p.remove();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
}, true);
|
||||
}""")
|
||||
assert claim_form.close_claim(page) is True
|
||||
assert page.locator("#strayPanel").count() == 0
|
||||
assert page.evaluate("window.closeClaimScreenRan") is True
|
||||
|
||||
|
||||
@case
|
||||
def save_claim_clicks_the_hcfa_ok_and_the_modal_closes(page):
|
||||
page.goto(FIXTURE.as_uri())
|
||||
assert page.locator("#claimScreenOkBtn8888888888888").first.is_visible()
|
||||
assert claim_form.save_claim(page) is True
|
||||
assert not page.locator("#claimScreenOkBtn8888888888888").first.is_visible()
|
||||
|
||||
|
||||
def _run() -> 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)
|
||||
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
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""
|
||||
shot() writes into the operator's dated Screenshots\ folder; a test
|
||||
run must not, so the cases run with it pointed at a temp folder.
|
||||
"""
|
||||
import tempfile
|
||||
from ecw_bot import settings as _cfg
|
||||
orig = (_cfg.SCREENSHOTS, _cfg.SAVE_CORRECTIONS)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
_cfg.SCREENSHOTS = pathlib.Path(tmp)
|
||||
# These cases exercise eCW's own click and Add flow, as a LIVE-WRITE
|
||||
# run does. VERIFY clicks no box and adds no row (eCW saves both at
|
||||
# once, 2026-10-16): that is tests/test_verify_no_live_writes.py.
|
||||
_cfg.SAVE_CORRECTIONS = True
|
||||
try:
|
||||
return _run()
|
||||
finally:
|
||||
_cfg.SCREENSHOTS, _cfg.SAVE_CORRECTIONS = orig
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user