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,258 @@
|
||||
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())
|
||||
Reference in new issue
Block a user