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,165 @@
|
||||
r"""
|
||||
core/fallback.py and the selector chains (2026-10-21): a renamed id falls
|
||||
back to an alternative taken from the live DOM, and the log says so.
|
||||
|
||||
python tests\test_fallback_selectors.py
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
import inspect
|
||||
import pathlib
|
||||
import contextlib
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from ecw_bot.core import fallback # noqa: E402
|
||||
from ecw_bot.selectors.fallbacks import CHAINS # noqa: E402
|
||||
from ecw_bot.selectors import claim_form as form_sel # noqa: E402
|
||||
from ecw_bot.selectors import claim_lookup as lookup_sel # noqa: E402
|
||||
from ecw_bot.selectors import login as login_sel # noqa: E402
|
||||
|
||||
CASES = []
|
||||
|
||||
|
||||
def case(fn):
|
||||
CASES.append(fn)
|
||||
return fn
|
||||
|
||||
|
||||
def _quiet(fn, *a):
|
||||
out = io.StringIO()
|
||||
with contextlib.redirect_stdout(out):
|
||||
result = fn(*a)
|
||||
return result, out.getvalue()
|
||||
|
||||
|
||||
# ── the chains themselves ────────────────────────────────────────────────────
|
||||
|
||||
@case
|
||||
def each_selector_is_its_chain_with_the_primary_first():
|
||||
pairs = [(login_sel.SEL_PASSWORD, login_sel.PASSWORD_CHAIN, "#passwordField"),
|
||||
(login_sel.SEL_LOGIN_BTN, login_sel.LOGIN_BTN_CHAIN, "#Login"),
|
||||
(lookup_sel.SEL_CLAIM_NO, lookup_sel.CLAIM_NO_CHAIN,
|
||||
"#claimLookupIpt10"),
|
||||
(lookup_sel.SEL_LOOKUP_BTN, lookup_sel.LOOKUP_BTN_CHAIN,
|
||||
"#btnclaimlookup"),
|
||||
(form_sel.SEL_CANCEL, form_sel.CANCEL_CHAIN, "#UBClaimBtn17"),
|
||||
(form_sel.SEL_CANCEL_DENTAL, form_sel.CANCEL_DENTAL_CHAIN,
|
||||
"#cancelDentalClaimButton"),
|
||||
(form_sel.SEL_PROGRESS_NOTES, form_sel.PROGRESS_NOTES_CHAIN,
|
||||
'button[ng-click="viewProgNote()"]')]
|
||||
for joined, chain, primary in pairs:
|
||||
assert chain[0] == primary and len(chain) >= 2, chain
|
||||
assert joined == ", ".join(chain), joined
|
||||
|
||||
|
||||
@case
|
||||
def every_alternative_is_plain_css(page):
|
||||
# Used in querySelectorAll / :has() too: Playwright-only syntax breaks
|
||||
# there. querySelectorAll throws on anything the browser cannot parse.
|
||||
page.set_content("<div></div>")
|
||||
for name, (_, chain) in CHAINS.items():
|
||||
for selector in chain:
|
||||
assert ":has-text" not in selector and "text=" not in selector, \
|
||||
(name, selector)
|
||||
page.evaluate("s => document.querySelectorAll(s)", selector)
|
||||
|
||||
|
||||
@case
|
||||
def cancel_alternatives_never_match_ok_or_the_x(page):
|
||||
page.set_content("""
|
||||
<div id="UBClaimDiv">
|
||||
<button id="UBClaimBtn3" class="close" ng-click="closeUBClaimScreen()">x</button>
|
||||
<button id="UBClaimBtn18" ng-click="saveAllData(true)">OK</button>
|
||||
<button id="NewCancelId" ng-click="closeUBClaimScreen()">Cancel</button>
|
||||
</div>
|
||||
<div class="modal" id="dentalClaim123">
|
||||
<button class="close" ng-click="closeDntl()">x</button>
|
||||
<button id="saveDental" ng-click="saveData()">OK</button>
|
||||
<button id="newDentalCancel" ng-click="closeDntl()">Cancel</button>
|
||||
</div>""")
|
||||
ub = page.locator(form_sel.SEL_CANCEL)
|
||||
assert ub.count() == 1 and ub.get_attribute("id") == "NewCancelId"
|
||||
dental = page.locator(form_sel.SEL_CANCEL_DENTAL)
|
||||
assert dental.count() == 1 \
|
||||
and dental.get_attribute("id") == "newDentalCancel"
|
||||
|
||||
|
||||
# ── report() ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@case
|
||||
def the_primary_present_is_not_reported(page):
|
||||
fallback.reset()
|
||||
page.set_content('<input id="claimLookupIpt10" ng-model="_InvId">'
|
||||
'<button id="btnclaimlookup">Lookup</button>')
|
||||
names, log = _quiet(fallback.report, page,
|
||||
["Claim No box", "Lookup button"], CHAINS)
|
||||
assert names == [] and log == "", log
|
||||
|
||||
|
||||
@case
|
||||
def a_renamed_primary_is_reported_once_with_what_matched(page):
|
||||
fallback.reset()
|
||||
page.set_content('<input id="newId" ng-model="_InvId">'
|
||||
"<button id=\"b2\" ng-click=\"getClaimsListFromLookup"
|
||||
"('lookup')\">Lookup</button>")
|
||||
names, log = _quiet(fallback.report, page,
|
||||
["Claim No box", "Lookup button"], CHAINS)
|
||||
assert names == ["Claim No box", "Lookup button"], names
|
||||
assert "#claimLookupIpt10 not found" in log \
|
||||
and 'matched input[ng-model="_InvId"]' in log \
|
||||
and "selectors/claim_lookup.py" in log, log
|
||||
names, log = _quiet(fallback.report, page, ["Claim No box"], CHAINS)
|
||||
assert names == [] and log == "", "once a run"
|
||||
|
||||
|
||||
@case
|
||||
def the_bot_still_acts_through_the_alternative(page):
|
||||
page.set_content('<input id="pw2" type="password">'
|
||||
'<input id="btn2" type="button" value="Log In" '
|
||||
'onclick="window.clicked = 1">')
|
||||
page.fill(login_sel.SEL_PASSWORD, "x")
|
||||
page.click(login_sel.SEL_LOGIN_BTN)
|
||||
assert page.input_value("#pw2") == "x"
|
||||
assert page.evaluate("window.clicked") == 1
|
||||
|
||||
|
||||
@case
|
||||
def nothing_shown_is_not_a_fallback(page):
|
||||
fallback.reset()
|
||||
page.set_content("<p>other page</p>")
|
||||
names, log = _quiet(fallback.report, page, list(CHAINS), CHAINS)
|
||||
assert names == [] and log == ""
|
||||
|
||||
|
||||
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()
|
||||
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