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

691 lines
28 KiB
Python

r"""
Waiting for the HCFA and Dental forms to finish loading (review item D7).
python tests\test_claim_loaded.py
Seen live (run_20260930_163006): `open_claim()` returned the moment the HCFA
form's Cancel was visible, Angular had not filled the form yet, `extract()`
read 0 CPT rows, Prog. Notes read "disabled" — and the claim went to the
summary as Pass. `wait_claim_loaded()` waits for the claim number and for the
service-line codes to be filled and steady; a timeout is a reason string on
the claim's data, not an exception.
Local headless chromium against `tests\fixtures\hcfa_claim.html` and
`dental_claim.html`, whose opt-in `?load=` / `?pn=` query strings simulate
Angular filling the form late (or never, or with the wrong claim). No eCW.
Screenshots go to a temp folder, never the real Screenshots\.
"""
import io
import sys
import time
import pathlib
import inspect
import tempfile
import contextlib
ROOT = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "src"))
FIXTURES = pathlib.Path(__file__).resolve().parent / "fixtures"
HCFA = FIXTURES / "hcfa_claim.html"
DENTAL = FIXTURES / "dental_claim.html"
UB = 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.flows import export_medcor_ready as flow # noqa: E402
CASES = []
def case(fn):
CASES.append(fn)
return fn
def _go(page, fixture, query=""):
page.goto(fixture.as_uri() + (f"?{query}" if query else ""))
def _timed(fn, *args, **kwargs):
"""(result, seconds, captured stdout)."""
buf = io.StringIO()
started = time.monotonic()
with contextlib.redirect_stdout(buf):
out = fn(*args, **kwargs)
return out, time.monotonic() - started, buf.getvalue()
# ── HCFA ──────────────────────────────────────────────────────────────────────
@case
def hcfa_already_filled_costs_about_half_a_second(page):
"""The normal claim: the only cost is the 500 ms stability window."""
_go(page, HCFA)
out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000")
assert out is None, out
assert took < 1.5, f"took {took:.1f}s on a form that was already loaded"
assert "HCFA form loaded" in log and "3 CPT rows" in log, log
@case
def hcfa_filled_late_is_waited_for(page):
_go(page, HCFA, "load=delayed")
# Proves the fixture: right now the form is blank, the way eCW serves it.
assert claim_form.extract(page)["cpt"] == [], "fixture is not blank"
out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000")
assert out is None, out
assert took >= 0.7, f"returned after {took:.2f}s — before the form filled"
assert len(claim_form.extract(page)["cpt"]) == 3, "read short after wait"
@case
def hcfa_rows_filled_one_at_a_time_are_waited_for_until_they_hold_still(page):
"""
The stability rule (D7): the same non-zero count held for 500 ms. Angular
fills the rows one digest at a time — here row 1 at 0.8 s, row 2 at
1.1 s, row 3 at 1.4 s. A wait that took the first non-zero count would
return at about 0.8-1.0 s and extract() would read 1 or 2 rows; only the
500 ms window keeps it until 1.4 s + 0.5 s.
"""
_go(page, HCFA, "load=staggered")
out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000")
assert out is None, out
assert took >= 1.9, f"returned after {took:.2f}s — rows still filling"
assert len(claim_form.extract(page)["cpt"]) == 3, "read short after wait"
assert "3 CPT rows" in log, log
@case
def hcfa_hidden_leftover_form_of_another_claim_is_not_read(page):
"""
eCW leaves closed forms in the DOM. A hidden copy of claim 5000002's form
sits ahead of this one, and this form's claim number is still hidden
while Angular fills it: the old document-wide read fell back to the
hidden copy and raised a false "wrong claim" — the claim came back
unread. Only the window holding the visible Cancel is read now.
"""
_go(page, HCFA, "load=delayed&stale=other")
out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000")
assert out is None, out
assert took >= 0.7, f"returned after {took:.2f}s — before the form filled"
assert "3 CPT rows" in log, log
@case
def hcfa_hidden_leftover_rows_do_not_count_as_loaded(page):
"""The same hidden leftover, showing this claim's number and one filled
row, in front of a form that never fills: its row must not make the
blank form count as loaded (the old read returned None here)."""
_go(page, HCFA, "load=never&stale=same")
out, _, _ = _timed(claim_form.wait_claim_loaded, page, "9103000",
timeout_ms=1_000)
assert out == ("HCFA form did not finish loading in 1s "
"(claim number never filled, 0 CPT rows)"), out
@case
def hcfa_never_filled_returns_a_reason_not_an_exception(page):
_go(page, HCFA, "load=never")
out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000",
timeout_ms=2_000)
assert out == ("HCFA form did not finish loading in 2s "
"(claim number never filled, 0 CPT rows)"), out
assert 1.8 <= took < 5.0, f"took {took:.1f}s"
assert out in log, log
@case
def hcfa_wrong_claim_raises(page):
_go(page, HCFA, "load=wrong")
try:
_timed(claim_form.wait_claim_loaded, page, "9103000")
except RuntimeError as exc:
assert "5000001" in str(exc) and "wrong claim" in str(exc), exc
else:
raise AssertionError("a form showing another claim did not raise")
@case
def hcfa_without_a_claim_number_field_still_waits_on_cpt_rows(page):
_go(page, HCFA, "load=noclaimno")
out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000")
assert out is None, out
assert took >= 0.7, f"returned after {took:.2f}s — rows were still blank"
assert log.count("identity not checked") == 1, log
# ── Dental ────────────────────────────────────────────────────────────────────
@case
def dental_filled_late_is_waited_for(page):
_go(page, DENTAL, "load=delayed")
out, took, log = _timed(claim_form.wait_claim_loaded, page, "5000000")
assert out is None, out
assert took >= 0.7, f"returned after {took:.2f}s — before the form filled"
assert "Dental form loaded" in log and "2 service lines)" in log, log
@case
def dental_cancel_inside_a_dentalclaim_id_wrapper_still_reads_the_form(page):
"""
The window is found by walking up from the visible Cancel to the
nearest SEL_DENTAL_WINDOW match. With an id-prefix-only selector, a
`dentalClaimFooter…` wrapper around Cancel was that nearest match: no
claim number and no service lines inside it, so the form read as blank
and every Dental claim waited out the timeout as not evaluated. The
window is the `.modal` itself now.
"""
_go(page, DENTAL, "load=delayed&footer=wrapped")
assert page.locator("#dentalClaimFooter7777777777777 "
"#cancelDentalClaimButton").count() == 1, "fixture did not wrap Cancel"
out, took, log = _timed(claim_form.wait_claim_loaded, page, "5000000",
timeout_ms=3_000)
assert out is None, out
assert took >= 0.7, f"returned after {took:.2f}s — before the form filled"
assert "identity not checked" not in log, log
assert "Dental form loaded" in log and "2 service lines)" in log, log
@case
def dental_never_filled_returns_a_reason(page):
_go(page, DENTAL, "load=never")
out, _, _ = _timed(claim_form.wait_claim_loaded, page, "5000000",
timeout_ms=1_000)
assert out == ("Dental form did not finish loading in 1s "
"(claim number never filled, 0 service lines)"), out
@case
def dental_wrong_claim_raises(page):
_go(page, DENTAL, "load=wrong")
try:
_timed(claim_form.wait_claim_loaded, page, "5000000")
except RuntimeError as exc:
assert "5000001" in str(exc), exc
else:
raise AssertionError("a Dental form showing another claim did not raise")
@case
def ub_form_is_left_to_open_claims_own_wait(page):
"""No-op on UB: open_claim() already waits for its claim number."""
_go(page, UB)
out, took, _ = _timed(claim_form.wait_claim_loaded, page, "anything")
assert out is None and took < 1.0, (out, took)
# ── Elements that render late are waited for (final review, 2026-10-05) ──────
# Both waits were `visible(timeout=10_000)` before P8 and answered instantly;
# these fail if either is turned back into visible().
@case
def a_cpt_grid_that_renders_late_is_waited_for(page):
"""open_cpt_hcpcs_tab gives the grid 10s after the tab click."""
_go(page, UB)
page.evaluate("""() => {
const g = document.querySelector('#CPTCodesDiv');
g.style.display = 'none';
setTimeout(() => { g.style.display = ''; }, 1800);
}""")
out, took, log = _timed(claim_form.open_cpt_hcpcs_tab, page)
assert out is True, log
assert took >= 1.5, f"returned after {took:.1f}s — before the grid showed"
@case
def a_prog_notes_button_that_renders_late_is_not_absent(page):
"""open_progress_notes gives the button 10s to appear."""
_go(page, HCFA)
page.evaluate("""() => {
const b = document.querySelector('[ng-click="viewProgNote()"]');
b.style.display = 'none';
setTimeout(() => { b.style.display = ''; }, 1500);
}""")
note, _, log = _timed(claim_form.open_progress_notes, page)
assert note.get("opened") == "frame", (note, log)
# ── Prog. Notes "disabled" is re-checked ──────────────────────────────────────
@case
def prog_notes_enabled_late_is_not_reported_disabled(page):
"""Angular enables the button only after load; read too early, a claim
with a note was reported as having none."""
_go(page, HCFA, "pn=late")
assert page.locator('[ng-click="viewProgNote()"]').is_disabled()
note, _, _ = _timed(claim_form.open_progress_notes, page)
assert note.get("opened") == "frame", note
@case
def prog_notes_disabled_for_good_is_reported_after_about_5s(page):
_go(page, HCFA, "pn=off")
note, took, log = _timed(claim_form.open_progress_notes, page)
assert note == {"opened": "disabled"}, note
assert 4.5 <= took < 9.0, f"took {took:.1f}s"
# ── The flow keeps the reason on the claim's data ─────────────────────────────
def _flow_read(dental: bool, not_loaded, dental_result, apply=False,
calls=None, notes=False, dental_checks_on=True,
results=(), save=False):
"""
Run the real _read_claim_form with every page step stubbed. `calls`, when
given, collects ("run_all" | "save_claim" | "dental" | "wait" |
"notes", ...) as they happen. `notes` turns OPEN_PROGRESS_NOTES on;
`dental_checks_on` is CHECK_HIGH_RISK (CHECK_SLIDING_FEE stays off).
"""
calls = [] if calls is None else calls
def wait(page, cid):
calls.append("wait")
return not_loaded
def run_all(page, cid, data, before_apply=None, **kw):
calls.append("run_all")
return list(results)
def dental_checks(page, cid):
calls.append("dental")
return dict(dental_result)
stubs = {
"open_claim": lambda page, cid: True,
"wait_claim_loaded": wait,
"open_progress_notes": lambda page: calls.append("notes") or {
"opened": "frame", "sections": {"Procedure Codes": "x"}},
"is_dental_claim_form": lambda page: dental,
"open_cpt_hcpcs_tab": lambda page: None,
"extract": lambda page: {"cpt": [{"code": "99213"}], "icd": [],
"revenue": []},
"save": lambda data: pathlib.Path("unused.json"),
"close_claim": lambda page: True,
"save_claim": lambda page: calls.append("save_claim") or True,
}
orig_cf = {k: getattr(claim_form, k) for k in stubs}
orig_cfg = (cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES,
cfg.APPLY_CORRECTIONS, cfg.CHECK_HIGH_RISK,
cfg.CHECK_SLIDING_FEE, cfg.SAVE_CORRECTIONS,
cfg.REVIEW_BEFORE_CLOSE)
orig_box = (flow.msgbox.ok_cancel, flow.msgbox.info_box)
orig_dental, orig_run_all = flow._dental_checks, flow.conditions.run_all
for k, v in stubs.items():
setattr(claim_form, k, v)
cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES = True, notes
cfg.APPLY_CORRECTIONS = apply
# Not read from the operator's .env: a Dental case needs a check on to
# reach _dental_checks at all (final review, 2026-10-05).
cfg.CHECK_HIGH_RISK, cfg.CHECK_SLIDING_FEE = dental_checks_on, False
cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE = save, False
# Save, if ever offered, is answered OK — so "not saved" is the code's
# doing, not the operator's.
flow.msgbox.ok_cancel = lambda *a, **k: calls.append("save_box") or True
flow.msgbox.info_box = lambda *a, **k: None
flow._dental_checks = dental_checks
flow.conditions.run_all = run_all
try:
with contextlib.redirect_stdout(io.StringIO()):
return flow._read_claim_form(None, "9103000")
finally:
for k, v in orig_cf.items():
setattr(claim_form, k, v)
(cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES,
cfg.APPLY_CORRECTIONS, cfg.CHECK_HIGH_RISK,
cfg.CHECK_SLIDING_FEE, cfg.SAVE_CORRECTIONS,
cfg.REVIEW_BEFORE_CLOSE) = orig_cfg
flow.msgbox.ok_cancel, flow.msgbox.info_box = orig_box
flow._dental_checks = orig_dental
flow.conditions.run_all = orig_run_all
@case
def flow_marks_an_hcfa_claim_not_evaluated_and_still_reads_it():
why = "HCFA form did not finish loading in 20s (0 CPT rows)"
data = _flow_read(False, why, {})
assert data.get("_not_evaluated") == why, data
assert data.get("cpt"), "the claim was not read after the timeout"
@case
def flow_leaves_a_loaded_claim_unmarked():
data = _flow_read(False, None, {})
assert "_not_evaluated" not in data, data
@case
def flow_marks_the_dental_result_not_evaluated():
why = "Dental form did not finish loading in 20s (0 service lines)"
data = _flow_read(True, why, {"claim_no": "9103000",
"high_risk": {"high_risk": False}})
assert data.get("_not_evaluated") == why, data
@case
def flow_runs_no_conditions_on_a_form_that_did_not_finish_loading():
"""
APPLY_CORRECTIONS=1: a half-loaded form is read and closed, never
corrected — run_all() would read the same blank rows, and with
SAVE_CORRECTIONS=1 an approved OK would save the half-loaded form back
to live eCW. Nothing ran, so no `_conditions` either.
"""
calls = []
why = "HCFA form did not finish loading in 20s (0 CPT rows)"
data = _flow_read(False, why, {}, apply=True, calls=calls)
assert "run_all" not in calls and "save_claim" not in calls, calls
assert data.get("_not_evaluated") == why, data
assert "_conditions" not in data, data
@case
def flow_still_runs_conditions_on_a_loaded_form():
"""The control for the case above: the run_all stub is reachable."""
calls = []
data = _flow_read(False, None, {}, apply=True, calls=calls)
# "wait": _flow_read records the load wait too since the final review.
assert calls == ["wait", "run_all"], calls
# Unit D (2026-10-05): run_all's report rides along too, so the summary
# can tell "raised / declined" from "clean".
# `changes` (2026-10-08): what each condition changed, for the results DB.
# `detected` / `changed` (2026-10-12): the same, split for the DB.
assert data.get("_conditions") == {"fired": [], "detected": [],
"changed": [], "saved": False,
"failed": [], "review": [],
"comments": {}, "changes": {}}, data
@case
def flow_tells_the_dental_box_the_form_did_not_finish_loading():
"""
Updated for review item D9 (2026-10-05): `_dental_checks` no longer
shows a box, so the reason is no longer passed into it. It reaches the
claim's one box (`_claim_box`, after close) on the data instead, and
that box's Dental part leads with NOT_LOADED_NOTE.
"""
calls = []
why = "Dental form did not finish loading in 20s (0 service lines)"
data = _flow_read(True, why, {"claim_no": "9103000",
"high_risk": {"high_risk": False,
"match": ""}},
calls=calls)
assert calls == ["wait", "dental"], calls
assert data["_not_evaluated"] == why, data
_, body = flow._claim_box("Claim 1/1", "9103000", data, None)
detail = body.split(flow.RULE + "\n", 1)[1].splitlines()
assert detail[0] == claim_form.NOT_LOADED_NOTE, body
assert detail[1] == f"({why})", body
@case
def claim_summary_leads_with_the_not_loaded_line():
"""The SHOW_EACH_CLAIM box must not show a confident "CPT 0" read off a
form the bot logged as half-loaded."""
why = "HCFA form did not finish loading in 20s (0 CPT rows)"
text = claim_form.summary({"claim_no": "9103000", "cpt": [],
"_not_evaluated": why})
first = text.splitlines()
assert first[0] == claim_form.NOT_LOADED_NOTE, text
assert first[1] == f"({why})", text
plain = claim_form.summary({"claim_no": "9103000", "cpt": []})
assert claim_form.NOT_LOADED_NOTE not in plain, plain
assert plain.splitlines()[0].startswith("Claim No"), plain
@case
def flow_reads_a_dental_claim_even_with_no_dental_check_on():
"""
Operator, 2026-10-12: a Dental claim runs every applicable condition,
not only high risk / sliding fee — so it is waited for and read like
any claim, whatever CHECK_HIGH_RISK / CHECK_SLIDING_FEE say. (It used
to come back {} and blank in the summary.)
"""
calls = []
data = _flow_read(True, None, {}, calls=calls, dental_checks_on=False,
apply=True)
assert calls == ["wait", "run_all"], calls
assert data.get("screen") == "dental", data
assert data.get("cpt"), data
assert data.get("_conditions") is not None, data
@case
def flow_runs_the_conditions_on_a_dental_claim_after_its_own_checks():
calls = []
data = _flow_read(True, None, {"claim_no": "9103000",
"high_risk": {"high_risk": True,
"match": "x"}},
calls=calls, apply=True, notes=True)
assert calls == ["wait", "dental", "notes", "run_all"], calls
assert data["high_risk"]["high_risk"] is True, "Dental result kept"
assert data.get("screen") == "dental" and data.get("cpt"), data
@case
def flow_never_saves_a_dental_claim_even_with_save_on():
"""Operator, 2026-10-12: Dental row writes are not live-verified yet."""
from ecw_bot.conditions.base import ConditionResult
fired = ConditionResult(condition_name="dummy_codes", changed=True,
changes=[{"row": 1, "code": "DCONS",
"unchecked": True}])
calls = []
data = _flow_read(True, None, {}, calls=calls, apply=True, save=True,
results=[fired], dental_checks_on=False)
assert "save_box" not in calls and "save_claim" not in calls, calls
assert data["_conditions"]["saved"] is False, data
# The gate itself, and its control: the same result on a read that is
# not Dental IS offered for Save.
asked = []
orig = (cfg.SAVE_CORRECTIONS, flow.msgbox.ok_cancel)
cfg.SAVE_CORRECTIONS = True
flow.msgbox.ok_cancel = lambda *a, **k: asked.append(1) or False
try:
with contextlib.redirect_stdout(io.StringIO()):
flow._confirm_and_save(None, "1", [fired], {},
{"screen": "dental"})
assert asked == [], "a Dental claim was offered for Save"
flow._confirm_and_save(None, "1", [fired], {}, None)
finally:
cfg.SAVE_CORRECTIONS, flow.msgbox.ok_cancel = orig
assert asked == [1], "the control was not offered"
@case
def flow_still_waits_on_a_dental_form_when_a_check_is_on():
calls = []
_flow_read(True, None, {"claim_no": "9103000",
"high_risk": {"high_risk": False}}, calls=calls)
assert calls == ["wait", "dental"], calls
@case
def flow_does_not_open_the_note_of_a_form_that_did_not_finish_loading():
"""
Final review, 2026-10-05: the conditions are skipped on such a claim
and it is Review whatever the note says, so the note's up to ~45s
(absent 10s, disabled 5s, nothing 20s + close) is not spent. The note
is recorded as skipped, and the summary says it was not read.
"""
from ecw_bot.core import charge_summary as cs
calls = []
why = "HCFA form did not finish loading in 20s (0 CPT rows)"
data = _flow_read(False, why, {}, apply=True, calls=calls, notes=True)
assert "notes" not in calls, calls
assert data["progress_notes"]["opened"] == "skipped", data
reasons = cs.review_reasons(data)
assert reasons[0] == why, reasons
assert "Progress note not read (skipped) — telehealth not checked" in \
reasons, reasons
@case
def flow_opens_the_note_of_a_loaded_form():
"""The control: the stub is reachable and the note is read."""
calls = []
data = _flow_read(False, None, {}, calls=calls, notes=True)
assert "notes" in calls, calls
assert data["progress_notes"]["opened"] == "frame", data
# ── The claim number is compared as a number (final review, 2026-10-05) ──────
@case
def same_claim_no_ignores_formatting_but_not_a_different_number():
same = claim_form.same_claim_no
assert same("5000000", "5000000")
assert same("05000000", "5000000")
assert same("\u00a05000\u00a0000 ", "5000000")
assert same("Claim # 5000000", "5000000")
assert not same("5000001", "5000000")
assert not same("50000001", "5000000")
assert not same("", "5000000")
assert same("abc", "abc") and not same("abc", "abd")
@case
def dental_claim_number_written_differently_is_not_a_wrong_claim(page):
"""
The Dental claim-number span is not yet live-verified: a leading zero
or an inner non-breaking space must not raise "wrong claim" on every
Dental claim. It is logged once, so the live format is on record.
"""
_go(page, DENTAL)
page.evaluate("""() => {
document.querySelector('[ng-bind="ClaimData.claimNo"]')
.textContent = '0500\u00a00000';
}""")
out, _, log = _timed(claim_form.wait_claim_loaded, page, "5000000",
timeout_ms=3_000)
assert out is None, (out, log)
assert log.count("the same claim, written differently") == 1, log
@case
def ub_claim_number_that_never_filled_is_a_not_loaded_reason(page):
"""
Final review, 2026-10-05: open_claim() only logged "Claim No never
filled in — reading anyway", so the conditions ran on a UB form that
may be half-loaded. The reason now reaches the claim's data, and the
flow's not-evaluated branch skips the conditions as for HCFA/Dental.
"""
_go(page, UB)
page.evaluate("""() => {
for (const e of document.querySelectorAll(
'#UBClaimDiv .ubClaimNumberText span')) e.textContent = '';
}""")
out, took, _ = _timed(claim_form.wait_claim_loaded, page, "9104000")
assert out == ("UB form did not finish loading (claim number never "
"filled)"), out
assert took < 1.0, f"took {took:.1f}s — open_claim already waited"
@case
def flow_reports_a_wrong_claim_as_unread_and_still_closes_it():
closed = []
def wrong(page, cid):
raise RuntimeError("Opened the form for claim '5000001'")
orig = (claim_form.open_claim, claim_form.wait_claim_loaded,
claim_form.close_claim, cfg.OPEN_CLAIM_FORM)
claim_form.open_claim = lambda page, cid: True
claim_form.wait_claim_loaded = wrong
claim_form.close_claim = lambda page: closed.append(True)
cfg.OPEN_CLAIM_FORM = True
try:
with contextlib.redirect_stdout(io.StringIO()):
data = flow._read_claim_form(None, "9103000")
finally:
(claim_form.open_claim, claim_form.wait_claim_loaded,
claim_form.close_claim, cfg.OPEN_CLAIM_FORM) = orig
# Unit D (2026-10-05): not {} any more — {} read exactly like "not
# found". The error rides on the data and the summary says Review.
assert data == {"claim_no": "9103000",
"_error": "Opened the form for claim '5000001'"}, data
assert closed == [True], "the wrong claim's form was left open"
# ── close_claim() on a page error ────────────────────────────────────────────
@case
def any_claim_form_open_raises_on_a_page_error_rather_than_reading_closed():
"""
visible() swallows errors and answers False; close_claim() reads False as
"already closed — nothing to cancel". any_claim_form_open() therefore
keeps its own raising check (review of unit C, 2026-10-05).
"""
class Broken:
def locator(self, selector):
raise RuntimeError("Target page, context or browser has been closed")
try:
claim_form.any_claim_form_open(Broken())
except RuntimeError:
pass
else:
raise AssertionError("a page error read as an answer, not an error")
@case
def close_claim_still_clicks_cancel_when_the_open_check_errors(page):
"""A transient error while asking is not "closed": Cancel is still
clicked (the form would otherwise stay open for the next claim), and the
run is not ended by a raise from the flow's `finally`."""
_go(page, DENTAL)
orig = claim_form.any_claim_form_open
def broken(page):
raise RuntimeError("Execution context was destroyed")
claim_form.any_claim_form_open = broken
try:
out, _, log = _timed(claim_form.close_claim, page)
finally:
claim_form.any_claim_form_open = orig
assert out is True, (out, log)
assert "trying Cancel anyway" in log, log
assert "already closed" not in log, log
assert not page.locator("#cancelDentalClaimButton").is_visible(), "Cancel was not clicked — the form is still open"
def main() -> int:
try:
from playwright.sync_api import sync_playwright
except ImportError:
print(" SKIP playwright is not installed")
return 0
failures = 0
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()
try:
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
finally:
cfg.SCREENSHOTS = orig_shots
browser.close()
print(f"\n{len(CASES) - failures}/{len(CASES)} passed")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())