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("
") 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("""
""") 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('' '') 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('' "") 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('' '') 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("

other page

") 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())