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>
658 lines
26 KiB
Python
658 lines
26 KiB
Python
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())
|