r""" open_claim() recovering from eCW's own stray "Account Inquiry" panel. python tests\test_open_claim_recovery.py Seen live (claim 9128000, Logs\run_20260915_195515.log): eCW opens its own modal-style panel over the results grid — `id="claimModal" class="modal fade bluetheme billing-width right-panel-mode in"`, with a `claimModalBackdrop`-classed backdrop div that covers the full page — and it intercepts the click meant for the claim number underneath. Playwright's own retry trace named this element; nothing in this codebase opens it on purpose. `open_claim()` retried the blocked click for the full 30s timeout and gave up before ever reaching the claim form, so Condition 1 (or any future condition) never got a chance to run. This is a navigation problem, not a telehealth-detection problem — it blocks every claim, not just telehealth ones — which is why it is tested here rather than in test_telehealth_fix.py. Local headless chromium only, exactly like the other page tests: no proxy, no login, no eCW. Everything is built with `page.set_content()` rather than a fixture file, the same pattern test_telehealth_fix.py already uses for its HCFA-fallback cases — this scenario is about a page state (a stray backdrop sitting over a claim link) that has nothing to do with the UB form fixture. """ import sys import pathlib import inspect import tempfile ROOT = pathlib.Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "src")) from ecw_bot.pages import claim_form # noqa: E402 from ecw_bot.selectors import claim_form as sel # noqa: E402 from ecw_bot.core import page_utils # noqa: E402 from ecw_bot import settings as cfg # noqa: E402 CASES = [] def case(fn): CASES.append(fn) return fn # A backdrop shaped like eCW's real one: covers the whole viewport, so a real # (non-forced) Playwright click on anything underneath is genuinely blocked — # this is what makes the test prove the fix rather than assume it. _BACKDROP = ( '' ) _RESULTS_GRID = ( '9128000' '' '' ) @case def dismiss_stray_modal_returns_false_when_nothing_is_there(page): page.set_content(_RESULTS_GRID) assert claim_form.dismiss_stray_modal(page) is False @case def dismiss_stray_modal_removes_a_visible_backdrop(page): page.set_content(_BACKDROP + _RESULTS_GRID) assert page.locator(sel.SEL_STRAY_MODAL_BACKDROP).first.is_visible() assert claim_form.dismiss_stray_modal(page) is True assert page.locator(sel.SEL_STRAY_MODAL_BACKDROP).count() == 0 @case def open_claim_succeeds_normally_with_no_stray_modal(page): """Regression check: the new dismiss step must not change the happy path.""" page.set_content(_RESULTS_GRID) assert claim_form.open_claim(page, "9128000") is True assert page.locator("#UBClaimDiv").is_visible() _DENTAL_GRID = ( '9125000' '' '' ) def _open_and_capture(page, claim_id): import io, contextlib buf = io.StringIO() with contextlib.redirect_stdout(buf): assert claim_form.open_claim(page, claim_id) is True return buf.getvalue() @case def open_claim_logs_a_dental_claim_as_dental_not_hcfa(page): """Live run 2026-09-30: every Dental claim logged "(HCFA/professional screen)" because the only check was "not UB".""" page.set_content(_DENTAL_GRID) out = _open_and_capture(page, "9125000") assert "Claim form open (Dental screen)" in out, out assert "HCFA" not in out, out @case def open_claim_dismisses_a_stray_modal_that_would_otherwise_block_the_click(page): """ The exact failure from run_20260915_195515.log, reproduced: a backdrop already covers the claim link before open_claim() is ever called. Without the proactive dismiss, this click blocks for the full timeout and raises — this proves it doesn't, not just that the function returns something. """ page.set_content(_BACKDROP + _RESULTS_GRID) assert claim_form.open_claim(page, "9128000") is True assert page.locator("#UBClaimDiv").is_visible() assert page.locator(sel.SEL_STRAY_MODAL_BACKDROP).count() == 0 # ── close_claim() recovering from the same stray backdrop ──────────────────── # The exact shape of the claim 9127000 failure (Logs\run_20260917_172622.log): # Cancel, X, then Cancel again all timed out trying to close the form. Nothing # in that log names what blocked the clicks, but it is the same failure mode # open_claim() already had — a stray overlay sitting on top of the button — # and dismiss_stray_modal() was never wired into close_claim()'s retry loop. _UB_FORM_OPEN = ( '
' '
Claim
' ' ' ' ' '
' '' ) @case def close_claim_succeeds_normally_with_no_stray_modal(page): """Regression check: the new dismiss step must not change the happy path.""" page.set_content(_UB_FORM_OPEN) assert claim_form.close_claim(page) is True assert page.locator("#UBClaimDiv").first.is_visible() is False @case def close_claim_dismisses_a_stray_modal_that_would_otherwise_block_cancel(page): """ Without the proactive dismiss, this click blocks for the full attempt chain (Cancel, X, HCFA Cancel — none of them present or clickable under the backdrop) and close_claim() gives up and returns False. This proves it doesn't, not just that the function returns something. """ page.set_content(_BACKDROP + _UB_FORM_OPEN) assert claim_form.close_claim(page) is True assert page.locator("#UBClaimDiv").first.is_visible() is False assert page.locator(sel.SEL_STRAY_MODAL_BACKDROP).count() == 0 # ── close_claim() when the operator already closed the form by hand ───────── # Seen live on claim 9123000 (Logs\run_20260923_165056.log): the operator # clicked eCW's own Cancel while the review box was up, and close_claim() # then burned ~45s on four click attempts at buttons that no longer existed, # ending in "Could not close the claim form" for a form that was closed. _UB_FORM_ALREADY_CLOSED = ( '' ) @case def close_claim_returns_at_once_when_the_form_was_already_closed_by_hand(page): import time page.set_content(_UB_FORM_ALREADY_CLOSED) started = time.monotonic() assert claim_form.close_claim(page) is True elapsed = time.monotonic() - started assert elapsed < 3, f"took {elapsed:.1f}s — still trying to click Cancel" @case def close_claim_still_closes_an_hcfa_form_behind_a_hidden_leftover_ub_form(page): """ The already-closed check must look at every screen's marker, not only the first match in the page: a hidden UB form left in the DOM ahead of an open HCFA form must not make it look like nothing is open. """ page.set_content( _UB_FORM_ALREADY_CLOSED + '
') assert claim_form.close_claim(page) is True assert page.locator("#hcfa").is_visible() is False, \ "the open HCFA form was skipped instead of closed" # A permanent overlay with NO known class (unlike .claimModalBackdrop) — # dismiss_stray_modal() cannot recognize or remove this one, so every click # attempt on claimLink genuinely fails, for the whole test. The form opens # underneath it regardless, exactly like the "Follow up" panel seen live on # claim 9129000 (Logs\run_20260916_162654.log) sitting beside an open, fully # populated UB form. # # It opens a second after load, while the first click is still being # blocked — not before open_claim() is called: a form already on screen # before the click is a LEFTOVER from an earlier claim, which open_claim() # now Cancels first (final review, 2026-10-05; see the leftover cases below). _ALREADY_OPEN_WITH_UNKNOWN_OVERLAY = ( '9128000' '' '
' '
' '' ) @case def open_claim_treats_the_form_as_open_even_when_the_click_keeps_failing(page): """ The exact shape of the claim 9129000 failure: a stray overlay this codebase has no selector for at all blocks every click attempt, but the claim form is already open and populated underneath it regardless. open_claim() must trust the form's own presence over a reported click failure, rather than raising when the claim actually did open. """ page.set_content(_ALREADY_OPEN_WITH_UNKNOWN_OVERLAY) assert claim_form.open_claim(page, "9128000") is True # Same unknown overlay, but the form is NOT open yet when the click gives up: # it finishes opening about a second later (6s after load; the first click # now gives up at 5s). Review item D11, 2026-10-01: the fallback used to be # `visible(timeout=1_000)`, which Playwright 1.62 answers instantly — so a # form still opening was missed, and attempt 2's Escape closed it (on HCFA # without closeClaimScreen()). Escapes are counted so the test can tell. _OPENS_LATE_UNDER_UNKNOWN_OVERLAY = ( '9128000' '' '
' '
' '' ) @case def open_claim_fallback_waits_for_a_form_still_opening(page): """ The fallback is `appears(..., 5_000)`, a real wait, and the first click gives up at 5s, not 30s. With the old 30s click this takes 30s+; with the old instant check it Escapes and spends another 15s on attempt 2 — both fail the timing and Escape assertions below. """ import time page.set_content(_OPENS_LATE_UNDER_UNKNOWN_OVERLAY) started = time.monotonic() out = _open_and_capture(page, "9128000") took = time.monotonic() - started assert took < 12, f"took {took:.1f}s" assert page.evaluate("window.escapes") == 0, \ "attempt 2 ran — the fallback did not wait for the form" # The click's own first line and its elapsed seconds, for the root-cause # study of why eCW's click reports failure on a form that opened. assert "Click reported failure, but the claim form is already open" \ in out, out assert "(click: " in out and "failed after 5." in out \ and "s later)" in out, out class _ClickRaisesBlank: """ The real page, except the claim link's click raises an exception with an EMPTY message. `str(exc).splitlines()[0]` is an IndexError on that — raised inside the except block, out of open_claim(), losing a claim whose form was open. Playwright's own errors always carry a message; this proves the log line does not depend on it. """ def __init__(self, page): self._page = page def __getattr__(self, name): return getattr(self._page, name) def get_by_text(self, *a, **k): class _Link: @property def first(self): return self def click(self, **kw): raise Exception() return _Link() @case def open_claim_survives_a_click_error_with_no_message(page): page.set_content(_ALREADY_OPEN_WITH_UNKNOWN_OVERLAY) out = _open_and_capture(_ClickRaisesBlank(page), "9128000") assert "already open" in out and "(click: Exception;" in out, out # ── A form left open by an earlier claim ───────────────────────────────────── # Final review, 2026-10-05: when the previous claim's close_claim() gave up, # its form was still on screen at the next open_claim(), and the "click # reported failure, but the claim form is already open" fallback accepted it # as the new claim's. On an HCFA screen with no claim-number field nothing # checks identity after that, so the conditions ran — and were logged — on # the wrong claim. open_claim() now Cancels a leftover form before its click # and refuses to go on over one that will not close. # An earlier claim's UB form still up (claim 9112000); its Cancel works. The # link opens this claim's form and fills in its number. _LEFTOVER_UB_THAT_CLOSES = ( '9128000' '
' '
9112000
' '
Claim
' ' ' '
' '' ) @case def open_claim_cancels_a_form_left_open_by_an_earlier_claim_first(page): page.set_content(_LEFTOVER_UB_THAT_CLOSES) out = _open_and_capture(page, "9128000") assert "already open — left from an earlier claim" in out, out assert page.evaluate("window.cancels") == 1, \ "the leftover form was not Cancelled before the click" assert page.evaluate("window.linkClicks") == 1 assert page.locator("#claimNoText").inner_text() == "9128000" # An earlier claim's HCFA screen (no claim-number field) still up, and the # claim link is blocked by an overlay nothing can dismiss — the exact shape # where the fallback used to take the stale form for this claim's. _LEFTOVER_HCFA_UNDER_OVERLAY = ( '9128000' '
' '
' '
' '' ) @case def open_claim_refuses_to_go_on_over_a_leftover_form_that_will_not_close(page): """ close_claim() gives up (stood in for: its four real attempts take the best part of two minutes). Without the guard the blocked click's fallback sees the HCFA screen and returns True — the stale form, read as 9128000. """ page.set_content(_LEFTOVER_HCFA_UNDER_OVERLAY) orig = claim_form.close_claim claim_form.close_claim = lambda p: False try: try: _open_and_capture(page, "9128000") except RuntimeError as exc: assert "still open and would not close" in str(exc), exc else: raise AssertionError("open_claim() accepted the leftover form") finally: claim_form.close_claim = orig @case def open_claim_does_not_trust_a_cancel_that_left_an_hcfa_form_up(page): """ close_claim()'s own "hidden" check is on the UB form's div, which the HCFA screen does not have, so it answers True while this Cancel (one that does nothing) leaves the screen up. open_claim() checks every screen itself before it clicks. """ page.set_content(_LEFTOVER_HCFA_UNDER_OVERLAY.replace( 'z-index:2000;', 'z-index:-1;')) try: _open_and_capture(page, "9128000") except RuntimeError as exc: assert "still open and would not close" in str(exc), exc else: raise AssertionError("open_claim() accepted the leftover form") assert page.evaluate("window.cancels") >= 1 # A hidden leftover backdrop FIRST in the DOM, the live panel second; Escape # starts the live one's close, which takes 1s (an animation). Final review, # 2026-10-05: the check counts any VISIBLE match, but the wait was on # `.first` — the hidden copy — so it returned at once, with the panel still # up over the grid. _HIDDEN_THEN_LIVE_BACKDROP = ( '' '' '' ) @case def dismiss_stray_modal_waits_for_the_visible_backdrop_not_a_hidden_copy(page): page.set_content(_HIDDEN_THEN_LIVE_BACKDROP) assert claim_form.dismiss_stray_modal(page) is True assert page.locator(".claimModalBackdrop").filter( visible=True).count() == 0, \ "returned while the live panel was still up" # ── The stranded scroll lock ────────────────────────────────────────────────── # Captured live on the Claim Lookup screen: `` with no # `.modal-backdrop` in the document and every `.modal` still `fade` without # `in`. Bootstrap's own stylesheet hangs `overflow: hidden` off that class, so # the page cannot scroll and there is no dialog on screen to explain it. # # The stylesheet is real here rather than mocked: without `body.modal-open # {overflow:hidden}` actually loaded, the assertions below would pass against a # body that was never locked in the first place and prove nothing. _LOCK_CSS = ( "" ) def _doc(body_class, inner): return (f"{_LOCK_CSS}" f'{inner}' '
tall enough to need scrolling
' "") _CLOSED_DIALOG = '' _OPEN_DIALOG = '' def _overflow(page): return page.evaluate("getComputedStyle(document.body).overflow") @case def clear_stale_modal_lock_is_false_when_body_was_never_locked(page): page.set_content(_doc("", _CLOSED_DIALOG)) assert claim_form.clear_stale_modal_lock(page) is False @case def clear_stale_modal_lock_unlocks_a_body_with_no_modal_showing(page): """The exact captured state: modal-open, no backdrop, nothing rendered.""" page.set_content(_doc("modal-open", _CLOSED_DIALOG)) assert _overflow(page) == "hidden", "fixture did not actually lock the body" assert claim_form.clear_stale_modal_lock(page) is True assert _overflow(page) != "hidden", "body is still scroll-locked" assert page.evaluate( "document.body.classList.contains('modal-open')") is False @case def clear_stale_modal_lock_leaves_a_genuinely_open_modal_alone(page): """ The guard that makes this safe to call unconditionally. A modal that is really on screen owns the lock legitimately — stripping it would let the page scroll around behind a live dialog, which is worse than the bug. """ page.set_content(_doc("modal-open", _OPEN_DIALOG)) assert claim_form.clear_stale_modal_lock(page) is False assert _overflow(page) == "hidden", "a live dialog's lock was stripped" assert page.evaluate( "document.body.classList.contains('modal-open')") is True @case def clear_stale_modal_lock_also_removes_orphaned_backdrops(page): page.set_content(_doc( "modal-open", _CLOSED_DIALOG + '' '')) assert claim_form.clear_stale_modal_lock(page) is True assert page.locator(".modal-backdrop").count() == 0 @case def dismiss_stray_modal_clears_a_lock_left_behind_with_no_backdrop(page): """ Why the lock check cannot be gated on a backdrop being present: there is no `.claimModalBackdrop` here at all, so the original backdrop-only path bails out at the first line and reports "nothing to do" over a body that cannot scroll. """ page.set_content(_doc("modal-open", _CLOSED_DIALOG)) assert claim_form.dismiss_stray_modal(page) is True assert _overflow(page) != "hidden", "body is still scroll-locked" @case def dismiss_stray_modal_handles_a_backdrop_and_a_lock_together(page): """ Escape closing the panel does not, on its own, put the class back — so the lock check runs after the Escape rather than instead of it. """ # _RESULTS_GRID carries the keydown handler that makes Escape actually # remove the backdrop, the same way the real panel responds to it. page.set_content(_doc("modal-open", _BACKDROP + _RESULTS_GRID + _CLOSED_DIALOG)) assert claim_form.dismiss_stray_modal(page) is True assert page.locator(sel.SEL_STRAY_MODAL_BACKDROP).count() == 0, "Escape should have removed the stray backdrop" assert _overflow(page) != "hidden", "backdrop went but the scroll lock stayed behind" @case def reset_scroll_names_the_inner_container_that_actually_scrolled(page): """ `window.scrollTo(0, 0)` is a no-op when the document is not the scroller — the whole reason reset_scroll sweeps for the real container and reports it. """ page.set_content( '
' '
rows
') page.evaluate("document.getElementById('grid').scrollTop = 400") moved = page_utils.reset_scroll(page) assert any("#grid" in m for m in moved), moved assert page.evaluate("document.getElementById('grid').scrollTop") == 0 def main() -> int: try: from playwright.sync_api import sync_playwright except ImportError: print(" SKIP playwright is not installed") return 0 failures = 0 # The leftover-form cases take a screenshot on their raise path: into a # temp folder, never the operators' real dated Screenshots folder. 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() 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() cfg.SCREENSHOTS = orig_shots print(f"\n{len(CASES) - failures}/{len(CASES)} passed") return 1 if failures else 0 if __name__ == "__main__": sys.exit(main())