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,657 @@
|
||||
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<timestamp>" 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 = (
|
||||
'<div class="modal fade ng-isolate-scope claimModalBackdrop in" '
|
||||
'style="position:fixed;top:0;left:0;width:100%;height:100%;'
|
||||
'background:rgba(0,0,0,0.01);z-index:1050;"></div>'
|
||||
)
|
||||
|
||||
_RESULTS_GRID = (
|
||||
'<span id="claimLink">9128000</span>'
|
||||
'<div id="UBClaimDiv" style="display:none">'
|
||||
' <div class="ubClaimNumberText"><span id="claimNoText"></span></div>'
|
||||
# Non-empty and sized: SEL_ANY_CLAIM_FORM_OPEN is a comma (OR) selector
|
||||
# and this element comes first in DOM order — an empty div has zero
|
||||
# area, so wait_for_selector's state="visible" check would sit waiting
|
||||
# on *this* forever even once #UBClaimBtn17 is genuinely visible.
|
||||
' <div id="BillType" style="width:10px;height:10px;">Claim</div>'
|
||||
' <button id="UBClaimBtn17">Cancel</button>'
|
||||
'</div>'
|
||||
'<script>'
|
||||
'document.addEventListener("keydown", function (e) {'
|
||||
' if (e.key === "Escape") {'
|
||||
' var b = document.querySelector(".claimModalBackdrop");'
|
||||
' if (b) b.remove();'
|
||||
' }'
|
||||
'});'
|
||||
'document.getElementById("claimLink").addEventListener("click", function () {'
|
||||
' document.getElementById("UBClaimDiv").style.display = "block";'
|
||||
' document.getElementById("claimNoText").textContent = "9128000";'
|
||||
'});'
|
||||
'</script>'
|
||||
)
|
||||
|
||||
|
||||
@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 = (
|
||||
'<span id="claimLink">9125000</span>'
|
||||
'<div id="dentalClaim7777777777777" style="display:none">'
|
||||
' <button id="cancelDentalClaimButton">Cancel</button>'
|
||||
'</div>'
|
||||
'<script>'
|
||||
'document.getElementById("claimLink").addEventListener("click", function () {'
|
||||
' document.getElementById("dentalClaim7777777777777").style.display = "block";'
|
||||
'});'
|
||||
'</script>'
|
||||
)
|
||||
|
||||
|
||||
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 = (
|
||||
'<div id="UBClaimDiv">'
|
||||
' <div id="BillType" style="width:10px;height:10px;">Claim</div>'
|
||||
' <button id="UBClaimBtn17">Cancel</button>'
|
||||
' <button id="UBClaimBtn3">X</button>'
|
||||
'</div>'
|
||||
'<script>'
|
||||
'document.addEventListener("keydown", function (e) {'
|
||||
' if (e.key === "Escape") {'
|
||||
' var b = document.querySelector(".claimModalBackdrop");'
|
||||
' if (b) b.remove();'
|
||||
' }'
|
||||
'});'
|
||||
'function closeUb() { document.getElementById("UBClaimDiv").style.display = "none"; }'
|
||||
'document.getElementById("UBClaimBtn17").addEventListener("click", closeUb);'
|
||||
'document.getElementById("UBClaimBtn3").addEventListener("click", closeUb);'
|
||||
'</script>'
|
||||
)
|
||||
|
||||
|
||||
@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 = (
|
||||
'<div id="UBClaimDiv" style="display:none">'
|
||||
' <div id="BillType">Claim</div>'
|
||||
' <button id="UBClaimBtn17">Cancel</button>'
|
||||
' <button id="UBClaimBtn3">X</button>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
|
||||
@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
|
||||
+ '<div id="hcfa"><button ng-click="closeClaimScreen()" '
|
||||
'onclick="document.getElementById(\'hcfa\').style.display=\'none\'">'
|
||||
'Cancel</button></div>')
|
||||
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 = (
|
||||
'<span id="claimLink">9128000</span>'
|
||||
'<div id="UBClaimDiv" style="display:none">'
|
||||
' <div class="ubClaimNumberText"><span id="claimNoText">9128000</span></div>'
|
||||
' <div id="BillType" style="width:10px;height:10px;">Claim</div>'
|
||||
' <button id="UBClaimBtn17">Cancel</button>'
|
||||
'</div>'
|
||||
'<div id="unknownOverlay" style="position:fixed;top:0;left:0;'
|
||||
'width:100%;height:100%;background:rgba(0,0,0,0.01);z-index:2000;">'
|
||||
'</div>'
|
||||
'<script>'
|
||||
'setTimeout(function () {'
|
||||
' document.getElementById("UBClaimDiv").style.display = "block";'
|
||||
'}, 1000);'
|
||||
'</script>'
|
||||
)
|
||||
|
||||
|
||||
@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 = (
|
||||
'<span id="claimLink">9128000</span>'
|
||||
'<div id="UBClaimDiv" style="display:none">'
|
||||
' <div class="ubClaimNumberText"><span id="claimNoText">9128000</span></div>'
|
||||
' <div id="BillType" style="width:10px;height:10px;">Claim</div>'
|
||||
' <button id="UBClaimBtn17">Cancel</button>'
|
||||
'</div>'
|
||||
'<div id="unknownOverlay" style="position:fixed;top:0;left:0;'
|
||||
'width:100%;height:100%;background:rgba(0,0,0,0.01);z-index:2000;">'
|
||||
'</div>'
|
||||
'<script>'
|
||||
'window.escapes = 0;'
|
||||
'document.addEventListener("keydown", function (e) {'
|
||||
' if (e.key === "Escape") window.escapes++;'
|
||||
'});'
|
||||
'setTimeout(function () {'
|
||||
' document.getElementById("UBClaimDiv").style.display = "block";'
|
||||
'}, 6000);'
|
||||
'</script>'
|
||||
)
|
||||
|
||||
|
||||
@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 = (
|
||||
'<span id="claimLink">9128000</span>'
|
||||
'<div id="UBClaimDiv" style="display:block">'
|
||||
' <div class="ubClaimNumberText"><span id="claimNoText">9112000</span></div>'
|
||||
' <div id="BillType" style="width:10px;height:10px;">Claim</div>'
|
||||
' <button id="UBClaimBtn17">Cancel</button>'
|
||||
'</div>'
|
||||
'<script>'
|
||||
'window.cancels = 0; window.linkClicks = 0;'
|
||||
'document.getElementById("UBClaimBtn17").addEventListener("click", function () {'
|
||||
' window.cancels++;'
|
||||
' document.getElementById("UBClaimDiv").style.display = "none";'
|
||||
'});'
|
||||
'document.getElementById("claimLink").addEventListener("click", function () {'
|
||||
' window.linkClicks++;'
|
||||
' document.getElementById("claimNoText").textContent = "9128000";'
|
||||
' document.getElementById("UBClaimDiv").style.display = "block";'
|
||||
'});'
|
||||
'</script>'
|
||||
)
|
||||
|
||||
|
||||
@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 = (
|
||||
'<span id="claimLink">9128000</span>'
|
||||
'<div id="hcfa"><button ng-click="closeClaimScreen()" id="hcfaCancel">'
|
||||
'Cancel</button></div>'
|
||||
'<div id="unknownOverlay" style="position:fixed;top:0;left:0;'
|
||||
'width:100%;height:100%;background:rgba(0,0,0,0.01);z-index:2000;">'
|
||||
'</div>'
|
||||
'<script>'
|
||||
'window.cancels = 0;'
|
||||
'document.getElementById("hcfaCancel").addEventListener("click", function () {'
|
||||
' window.cancels++;'
|
||||
'});'
|
||||
'</script>'
|
||||
)
|
||||
|
||||
|
||||
@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 = (
|
||||
'<div class="modal fade claimModalBackdrop" style="display:none"></div>'
|
||||
'<div id="live" class="modal fade ng-isolate-scope claimModalBackdrop in" '
|
||||
'style="position:fixed;top:0;left:0;width:100%;height:100%;'
|
||||
'background:rgba(0,0,0,0.01);z-index:1050;"></div>'
|
||||
'<script>'
|
||||
'document.addEventListener("keydown", function (e) {'
|
||||
' if (e.key === "Escape") setTimeout(function () {'
|
||||
' var b = document.getElementById("live"); if (b) b.remove();'
|
||||
' }, 1000);'
|
||||
'});'
|
||||
'</script>'
|
||||
)
|
||||
|
||||
|
||||
@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: `<body class="modal-open">` 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 = (
|
||||
"<style>"
|
||||
"body.modal-open{overflow:hidden;}"
|
||||
".modal{display:none;}"
|
||||
".modal.in{display:block;width:200px;height:200px;}"
|
||||
"</style>"
|
||||
)
|
||||
|
||||
|
||||
def _doc(body_class, inner):
|
||||
return (f"<html><head>{_LOCK_CSS}</head>"
|
||||
f'<body class="{body_class}">{inner}'
|
||||
'<div style="height:3000px">tall enough to need scrolling</div>'
|
||||
"</body></html>")
|
||||
|
||||
|
||||
_CLOSED_DIALOG = '<div class="modal fade" id="someDialog">Account Inquiry</div>'
|
||||
_OPEN_DIALOG = '<div class="modal fade in" id="someDialog">Account Inquiry</div>'
|
||||
|
||||
|
||||
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 + '<div class="modal-backdrop fade in"></div>'
|
||||
'<div class="modal-backdrop fade in"></div>'))
|
||||
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(
|
||||
'<div id="grid" style="height:100px;overflow:auto;">'
|
||||
'<div style="height:2000px">rows</div></div>')
|
||||
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())
|
||||
Reference in new issue
Block a user