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>
259 lines
9.9 KiB
Python
259 lines
9.9 KiB
Python
r"""
|
|
Stacked dialogs (live run 2026-10-12, Logs\run_20261012_160331.log): one
|
|
dialog left on top of another blocked every click after it, and five of
|
|
seven claims went to Review.
|
|
|
|
* Claim 1: Prog. Notes was clicked a second time while the first viewer
|
|
was still loading its document — two viewers. Close was clicked on the
|
|
LOWER one (`.first`), which the upper, blank one covered: 15 s timeout,
|
|
the viewer stayed, and the CPT tab, the form's Cancel and the next two
|
|
claims' searches were all intercepted by its iframe.
|
|
* Claim 6: eCW opened the same HCFA claim twice (two claim windows 648 ms
|
|
apart). The bot worked on the lower one; its Prog. Notes click was
|
|
intercepted by the upper one, and the next claim's search too.
|
|
|
|
python tests\test_stacked_dialogs.py
|
|
"""
|
|
|
|
import io
|
|
import sys
|
|
import pathlib
|
|
import inspect
|
|
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"
|
|
|
|
from ecw_bot import settings as cfg # noqa: E402
|
|
from ecw_bot.core import page_utils # noqa: E402
|
|
from ecw_bot.pages import claim_form, progress_note # noqa: E402
|
|
from ecw_bot.selectors import claim_form as sel # noqa: E402
|
|
|
|
CASES = []
|
|
|
|
|
|
def case(fn):
|
|
CASES.append(fn)
|
|
return fn
|
|
|
|
|
|
def _quiet(fn, *a, **k):
|
|
with contextlib.redirect_stdout(io.StringIO()) as out:
|
|
result = fn(*a, **k)
|
|
return result, out.getvalue()
|
|
|
|
|
|
# ── the helper ───────────────────────────────────────────────────────────────
|
|
|
|
@case
|
|
def click_topmost_clicks_the_button_on_top_not_the_first_in_the_dom(page):
|
|
page.set_content("""
|
|
<div id="low" style="position:fixed;left:0;top:0;width:400px;
|
|
height:200px;background:#eee;z-index:1050">
|
|
<button class="b" id="b1" style="margin:50px">Close</button></div>
|
|
<div id="high" style="position:fixed;left:0;top:0;width:400px;
|
|
height:200px;background:#ddd;z-index:1060">
|
|
<button class="b" id="b2" style="margin:50px">Close</button></div>
|
|
<script>window.hits = [];
|
|
for (const b of document.querySelectorAll('.b'))
|
|
b.addEventListener('click', () => window.hits.push(b.id));</script>
|
|
""")
|
|
assert page_utils.click_topmost(page, ".b", timeout=3_000) is True
|
|
assert page.evaluate("window.hits") == ["b2"]
|
|
|
|
|
|
@case
|
|
def click_topmost_says_false_when_nothing_matches(page):
|
|
page.set_content("<div>nothing here</div>")
|
|
assert page_utils.click_topmost(page, ".b", timeout=1_000) is False
|
|
|
|
|
|
# ── claim 1: two progress note viewers ───────────────────────────────────────
|
|
|
|
@case
|
|
def close_shuts_every_stacked_viewer_top_first(page):
|
|
page.goto(HCFA.as_uri())
|
|
btn = "#claimProgressNoteBtn8888888888888"
|
|
page.click(btn)
|
|
page.click(btn)
|
|
page.evaluate("""() => {
|
|
window.shut = [];
|
|
document.querySelectorAll('[ng-click="closeProgressNoteViwer()"]')
|
|
.forEach((b, i) => b.addEventListener('click',
|
|
() => window.shut.push(i)));
|
|
}""")
|
|
assert page.locator(sel.SEL_PN_CLOSE).filter(visible=True).count() == 2
|
|
ok, out = _quiet(progress_note.close, page, wait_ms=2_000)
|
|
assert ok is True, out
|
|
assert page.evaluate("window.shut") == [1, 0], "top (second) first"
|
|
assert page.locator(sel.SEL_PN_CLOSE).filter(visible=True).count() == 0
|
|
assert "2 progress note viewers" in out, out
|
|
|
|
|
|
@case
|
|
def one_viewer_with_two_close_buttons_is_one_viewer(page):
|
|
"""Live 2026-10-12: the viewer draws an X and a Close — one viewer."""
|
|
page.set_content("""
|
|
<div class="modal in" id="v" style="display:block">
|
|
<button ng-click="closeProgressNoteViwer()">x</button>
|
|
<button ng-click="closeProgressNoteViwer()">Close</button></div>
|
|
<script>for (const b of document.querySelectorAll('button'))
|
|
b.addEventListener('click',
|
|
() => document.getElementById('v').remove());</script>
|
|
""")
|
|
ok, out = _quiet(progress_note.close, page, wait_ms=1_000)
|
|
assert ok is True, out
|
|
assert "viewers were open" not in out, out
|
|
assert page.locator(sel.SEL_PN_CLOSE).count() == 0
|
|
|
|
|
|
@case
|
|
def no_second_click_while_a_viewer_is_still_loading_its_note(page):
|
|
"""The re-click must not fire while eCW's viewer frame is open."""
|
|
page.set_content("""
|
|
<button ng-click="viewProgNote()" id="pn">Prog. Notes</button>
|
|
<div class="modal in" style="display:block">
|
|
<iframe id="ProgNoteViwerFrame" name="ProgNoteViwerFrame"
|
|
style="width:300px;height:100px"></iframe></div>
|
|
<script>window.clicks = 0;
|
|
document.getElementById('pn').addEventListener('click',
|
|
() => window.clicks++);</script>
|
|
""")
|
|
old = cfg.PROGRESS_NOTE_WAIT_MS
|
|
cfg.PROGRESS_NOTE_WAIT_MS = 2_000
|
|
try:
|
|
(reader, where, _), out = _quiet(
|
|
claim_form._wait_for_note, page, len(page.context.pages))
|
|
finally:
|
|
cfg.PROGRESS_NOTE_WAIT_MS = old
|
|
assert reader is None and where == "nothing"
|
|
assert page.evaluate("window.clicks") == 0, out
|
|
assert "once more" not in out, out
|
|
|
|
|
|
@case
|
|
def the_second_click_still_fires_when_nothing_at_all_opened(page):
|
|
"""The control: the click that did not take is still retried."""
|
|
page.set_content("""
|
|
<button ng-click="viewProgNote()" id="pn">Prog. Notes</button>
|
|
<script>window.clicks = 0;
|
|
document.getElementById('pn').addEventListener('click',
|
|
() => window.clicks++);</script>
|
|
""")
|
|
old = cfg.PROGRESS_NOTE_WAIT_MS
|
|
cfg.PROGRESS_NOTE_WAIT_MS = 2_000
|
|
try:
|
|
_, out = _quiet(claim_form._wait_for_note, page,
|
|
len(page.context.pages))
|
|
finally:
|
|
cfg.PROGRESS_NOTE_WAIT_MS = old
|
|
assert page.evaluate("window.clicks") == 1, out
|
|
|
|
|
|
# ── claim 6: the same HCFA claim opened twice ────────────────────────────────
|
|
|
|
def _duplicate_hcfa_window(page):
|
|
"""A second copy of the claim window on top, as eCW opened it."""
|
|
page.evaluate("""() => {
|
|
const w = document.querySelector('.claimModalBackdrop');
|
|
const copy = w.cloneNode(true);
|
|
// Over the original, as angular-ui draws it: same place, higher.
|
|
const r = w.getBoundingClientRect();
|
|
Object.assign(copy.style, {position: 'absolute', zIndex: '1060',
|
|
left: (r.left + scrollX) + 'px', top: (r.top + scrollY) + 'px',
|
|
width: r.width + 'px', background: '#fff'});
|
|
copy.dataset.copy = '1';
|
|
document.body.appendChild(copy);
|
|
window.cancelled = [];
|
|
for (const win of document.querySelectorAll('.claimModalBackdrop')) {
|
|
const c = win.querySelector('[ng-click="closeClaimScreen()"]');
|
|
const clone = c.cloneNode(true); // drop the fixture's handler
|
|
c.replaceWith(clone);
|
|
clone.addEventListener('click', () => {
|
|
window.cancelled.push(win.dataset.copy ? 'copy' : 'original');
|
|
win.remove();
|
|
});
|
|
}
|
|
}""")
|
|
|
|
|
|
@case
|
|
def a_claim_opened_twice_loses_its_top_copy_by_cancel(page):
|
|
page.goto(HCFA.as_uri())
|
|
_duplicate_hcfa_window(page)
|
|
n, out = _quiet(claim_form.close_duplicate_forms, page, "9103000")
|
|
assert n == 1, out
|
|
assert page.evaluate("window.cancelled") == ["copy"]
|
|
assert page.locator(".claimModalBackdrop").count() == 1
|
|
assert "opened the claim twice" in out, out
|
|
|
|
|
|
@case
|
|
def one_claim_window_is_left_alone(page):
|
|
page.goto(HCFA.as_uri())
|
|
n, out = _quiet(claim_form.close_duplicate_forms, page, "9103000")
|
|
assert n == 0, out
|
|
assert page.locator(".claimModalBackdrop").count() == 1
|
|
|
|
|
|
@case
|
|
def close_claim_cancels_the_window_on_top(page):
|
|
"""With two windows up, Cancel goes to the one that can be clicked."""
|
|
page.goto(HCFA.as_uri())
|
|
_duplicate_hcfa_window(page)
|
|
page.evaluate("""() => { // the original's Cancel is covered
|
|
window.cancelled = window.cancelled || [];
|
|
}""")
|
|
ok, out = _quiet(claim_form.close_claim, page)
|
|
assert page.evaluate("window.cancelled")[0] == "copy", out
|
|
|
|
|
|
@case
|
|
def a_viewer_left_from_the_last_claim_is_closed_before_the_next_search(page):
|
|
"""
|
|
Defence in depth: claims 2 and 3 were lost to claim 1's viewer. Before
|
|
a claim is searched, any viewer still up is closed.
|
|
"""
|
|
page.goto(HCFA.as_uri())
|
|
page.click("#claimProgressNoteBtn8888888888888")
|
|
assert page.locator(sel.SEL_PN_CLOSE).filter(visible=True).count() == 1
|
|
done, out = _quiet(claim_form.clear_screen, page, "9103000",
|
|
page.context.pages)
|
|
assert page.locator(sel.SEL_PN_CLOSE).filter(visible=True).count() == 0
|
|
assert "a progress note viewer" in done, out
|
|
assert "Left over from the last claim" in out, out
|
|
|
|
|
|
def main() -> int:
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
failures = 0
|
|
with sync_playwright() as pw:
|
|
browser = pw.chromium.launch(headless=True)
|
|
page = browser.new_page(viewport={"width": 1600, "height": 1000})
|
|
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())
|