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,176 @@
|
||||
r"""
|
||||
`visible()` and `appears()` — the instant check and the real wait.
|
||||
|
||||
python tests\test_page_utils.py
|
||||
|
||||
Review item P8 (2026-10-01): Playwright 1.62's `is_visible(timeout=...)`
|
||||
ignores its timeout, so `visible(page, sel, timeout=10_000)` was always an
|
||||
instant check, and every caller that meant "give it ten seconds" got none.
|
||||
And `.first` answered for whichever match came first in the DOM — a hidden
|
||||
leftover copy made the live element read as absent.
|
||||
|
||||
Local headless chromium, `page.set_content()` only. No eCW.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
import pathlib
|
||||
import inspect
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from ecw_bot.core import page_utils # noqa: E402
|
||||
|
||||
CASES = []
|
||||
|
||||
|
||||
def case(fn):
|
||||
CASES.append(fn)
|
||||
return fn
|
||||
|
||||
|
||||
# Two matches for one selector: a hidden leftover first, the live one second —
|
||||
# the shape eCW leaves behind when it renders an id twice.
|
||||
_HIDDEN_THEN_SHOWN = (
|
||||
'<button class="b" style="display:none">old</button>'
|
||||
'<button class="b">live</button>'
|
||||
)
|
||||
|
||||
|
||||
@case
|
||||
def visible_is_true_when_the_second_of_two_matches_is_the_visible_one(page):
|
||||
page.set_content(_HIDDEN_THEN_SHOWN)
|
||||
# The old `.first.is_visible()` answered for the hidden copy.
|
||||
assert page.locator(".b").first.is_visible() is False, \
|
||||
"fixture no longer puts the hidden copy first"
|
||||
assert page_utils.visible(page, ".b") is True
|
||||
|
||||
|
||||
@case
|
||||
def visible_is_false_when_every_match_is_hidden(page):
|
||||
page.set_content('<p class="x" style="display:none">a</p>'
|
||||
'<p class="x" style="visibility:hidden">b</p>')
|
||||
assert page_utils.visible(page, ".x") is False
|
||||
|
||||
|
||||
@case
|
||||
def visible_is_instant_and_ignores_its_timeout(page):
|
||||
"""The documented contract: `timeout` is accepted and ignored."""
|
||||
page.set_content("<div></div>")
|
||||
started = time.monotonic()
|
||||
assert page_utils.visible(page, "#never", timeout=5_000) is False
|
||||
assert time.monotonic() - started < 1.0, "visible() waited"
|
||||
|
||||
|
||||
@case
|
||||
def visible_never_raises_on_a_bad_selector(page):
|
||||
page.set_content("<div></div>")
|
||||
assert page_utils.visible(page, "[[[not a selector") is False
|
||||
|
||||
|
||||
@case
|
||||
def appears_waits_for_an_element_shown_after_a_delay(page):
|
||||
page.set_content(
|
||||
'<div id="late" style="display:none">x</div>'
|
||||
'<script>setTimeout(function () {'
|
||||
' document.getElementById("late").style.display = "block";'
|
||||
'}, 800);</script>')
|
||||
# The instant check misses it — the bug P8 is about.
|
||||
assert page_utils.visible(page, "#late", timeout=5_000) is False
|
||||
started = time.monotonic()
|
||||
assert page_utils.appears(page, "#late", timeout=5_000) is True
|
||||
took = time.monotonic() - started
|
||||
assert 0.5 < took < 4.0, f"took {took:.1f}s"
|
||||
|
||||
|
||||
@case
|
||||
def appears_returns_false_at_its_timeout(page):
|
||||
page.set_content('<div id="gone" style="display:none">x</div>')
|
||||
started = time.monotonic()
|
||||
assert page_utils.appears(page, "#gone", timeout=600) is False
|
||||
took = time.monotonic() - started
|
||||
assert 0.5 < took < 3.0, f"took {took:.1f}s"
|
||||
|
||||
|
||||
@case
|
||||
def appears_skips_a_hidden_leftover_ahead_of_the_live_element(page):
|
||||
"""`.first` alone would wait on the hidden copy and time out."""
|
||||
page.set_content(_HIDDEN_THEN_SHOWN)
|
||||
started = time.monotonic()
|
||||
assert page_utils.appears(page, ".b", timeout=2_000) is True
|
||||
assert time.monotonic() - started < 1.0
|
||||
|
||||
|
||||
# ── The callers that mean "wait" (final review, 2026-10-05) ─────────────────
|
||||
# Each of these was `visible(timeout=...)` before P8 and answered instantly.
|
||||
# A test per caller that fails if it is turned back into visible().
|
||||
|
||||
_LATE_SUBMENU = (
|
||||
'<button id="claimLookupBtn71">View Claims Report</button>'
|
||||
'<ul id="claimLookupUl1000" style="display:none">'
|
||||
' <li><button>EXCEL</button></li></ul>'
|
||||
'<script>'
|
||||
'window.clicked = false;'
|
||||
'const b = document.getElementById("claimLookupBtn71");'
|
||||
'b.addEventListener("mouseenter", () => setTimeout(() => {'
|
||||
' document.getElementById("claimLookupUl1000").style.display = "block";'
|
||||
'}, 1000));'
|
||||
'b.addEventListener("click", () => { window.clicked = true; });'
|
||||
'</script>'
|
||||
)
|
||||
|
||||
|
||||
@case
|
||||
def a_report_submenu_slower_than_the_hover_pause_is_not_clicked(page):
|
||||
"""
|
||||
`_open_report_submenu` hovers, pauses 600 ms, then gives the submenu
|
||||
1.5s to appear before falling back to a click — the click its docstring
|
||||
warns can close the parent dropdown instead. Here the submenu takes
|
||||
1s: with an instant visible() the 1.5s was never waited and the click
|
||||
fired.
|
||||
"""
|
||||
import io
|
||||
import contextlib
|
||||
from ecw_bot.pages import claim_report
|
||||
page.set_content(_LATE_SUBMENU)
|
||||
out = io.StringIO()
|
||||
with contextlib.redirect_stdout(out):
|
||||
claim_report._open_report_submenu(page)
|
||||
assert "Report submenu opened (hover)" in out.getvalue(), out.getvalue()
|
||||
assert page.evaluate("window.clicked") is False, "fell back to the click"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
except ImportError:
|
||||
print(" SKIP playwright is not installed")
|
||||
return 0
|
||||
|
||||
failures = 0
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
for fn in CASES:
|
||||
name = fn.__name__.replace("_", " ")
|
||||
try:
|
||||
if len(inspect.signature(fn).parameters) == 0:
|
||||
fn()
|
||||
else:
|
||||
fn(page)
|
||||
print(f" ok {name}")
|
||||
except AssertionError as exc:
|
||||
print(f" FAIL {name}: {exc}")
|
||||
failures += 1
|
||||
except Exception as exc:
|
||||
print(f" ERROR {name}: {type(exc).__name__}: {exc}")
|
||||
failures += 1
|
||||
browser.close()
|
||||
|
||||
print(f"\n{len(CASES) - failures}/{len(CASES)} passed")
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user