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