r""" eCW's post-login "This password expires in N days" box — answered with Cancel, never OK, so the login reaches the home screen. python tests\test_password_expiry_prompt.py Seen live 2026-10-05: both runs that day got through the 2FA approval and then stopped on this box (Screenshots\10-05-2026\*_login_unconfirmed.png), reporting "Login not confirmed" after two minutes. The box's real DOM was not captured, so the fixtures below are made-up markup with eCW's wording; the code finds the box by its text, which is what these pin. Local headless chromium, `page.set_content()` only. No eCW, no login. """ import io import sys import pathlib import tempfile import contextlib ROOT = pathlib.Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "src")) from ecw_bot import settings as cfg # noqa: E402 from ecw_bot.auth import login # noqa: E402 from ecw_bot.selectors import login as sel # noqa: E402 CASES = [] def case(fn): CASES.append(fn) return fn # The box over a page whose home link only appears once the box is gone — # the shape seen live. OK sets a flag (it must never be clicked); the page # also has an unrelated Cancel button outside the box. _PROMPT = """ """ _INPUT_BUTTONS = """
This password expires in 3 days. Do you want to change the password now?
""" _EXPIRED = """
Your password has expired. Please set a new password.
""" _NO_CANCEL = """
This password expires in 5 days.
""" def _quiet(fn, *args): out = io.StringIO() with contextlib.redirect_stdout(out): result = fn(*args) return result, out.getvalue() @case def a_late_prompt_is_cancelled_and_the_home_screen_is_reached(page): page.set_content(_PROMPT % 700) ok, log = _quiet(login.wait_for_home, page, 8_000) assert ok is True, log assert page.evaluate("window.okClicked") is False, "OK was clicked" assert page.evaluate("window.otherCancel") is False, \ "clicked a Cancel outside the box" assert "expires in 14 day(s)" in log, log assert "Answered the password-expiry box with Cancel" in log, log @case def input_buttons_work_too(page): page.set_content(_INPUT_BUTTONS) ok, log = _quiet(login.wait_for_home, page, 5_000) assert ok is True, log assert page.evaluate("window.okClicked") is False @case def no_prompt_means_nothing_is_clicked(page): page.set_content('Home' '' '') result, _ = _quiet(login.dismiss_password_expiry_prompt, page) assert result is None ok, _ = _quiet(login.wait_for_home, page, 2_000) assert ok is True and page.evaluate("window.c") is False @case def a_hidden_prompt_is_ignored(page): page.set_content(_PROMPT % 60_000) # never shown within the test result, _ = _quiet(login.dismiss_password_expiry_prompt, page) assert result is None @case def an_expired_password_stops_the_login_with_the_reason(page): page.set_content(_EXPIRED) try: _quiet(login.dismiss_password_expiry_prompt, page) except RuntimeError as exc: assert "password has expired" in str(exc), exc assert "never sets a password" in str(exc), exc else: raise AssertionError("an expired password did not raise") @case def a_prompt_without_cancel_is_reported_and_ok_is_never_clicked(page): page.set_content(_NO_CANCEL) ok, log = _quiet(login.wait_for_home, page, 1_500) assert ok is False assert page.evaluate("window.okClicked") is False, "OK was clicked" assert log.count("no Cancel button") == 3, log # capped, not every poll @case def the_patterns_match_the_live_wording(): import re live = ("This password expires in 14 days. Do you want to change the " "password now?") m = re.search(sel.TXT_PASSWORD_EXPIRES, live, re.I) assert m and m.group(1) == "14" assert not re.search(sel.TXT_PASSWORD_EXPIRED, live, re.I) def main() -> int: try: from playwright.sync_api import sync_playwright except ImportError: print(" SKIP playwright is not installed") return 0 import inspect failures = 0 with tempfile.TemporaryDirectory() as tmp: cfg.SCREENSHOTS = pathlib.Path(tmp) # never the real folder 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 inspect.signature(fn).parameters: fn(page) else: fn() 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())