"""Playwright helpers for Phase-2 recipe tests (plan §4.2). Centralizes the `page.goto(...)` retry loop that absorbs transient network errors (F2-3 / F2-5): Playwright's `page.goto` raises `PlaywrightError` on transport-level failures (`net::ERR_*`, connection resets, CDP target gone) — those escape a naive loop that only retries on status mismatches. Wrap every install-overlay `page.goto` in this helper so transient errors retry without weakening the underlying assertion (same pattern as F1e-1's `exec_in_app` poll+raise hardening). """ from __future__ import annotations import time def goto_with_retry(page, url, *, deadline_seconds: int = 120, accept_statuses=(200, 304), goto_timeout_ms: int = 30_000, wait_until: str = "domcontentloaded"): """Poll `page.goto(url)` until status is in `accept_statuses` OR the deadline expires. Returns the final Playwright response. Raises AssertionError if the deadline expires without a successful status. Each iteration catches `PlaywrightError` (and any other exception) so transient network failures retry rather than fail the test. Use case: recipe install overlays where the app's HTTP layer may be up (status 200 to /healthz or generic readiness) but the requested route is still registering (404), or Playwright's CDP connection transiently flakes (`net::ERR_NETWORK_CHANGED`). """ # Imported lazily so this module can be imported without playwright at unit-test time. try: from playwright.sync_api import Error as PlaywrightError except ImportError: # pragma: no cover — playwright is always installed in cc-ci-run PlaywrightError = Exception # noqa: N806 deadline = time.time() + deadline_seconds resp = None last_status = 0 last_err = "" attempts = 0 while time.time() < deadline: attempts += 1 try: resp = page.goto(url, wait_until=wait_until, timeout=goto_timeout_ms) except PlaywrightError as e: last_err = str(e) resp = None last_status = 0 else: last_status = resp.status if resp is not None else 0 if last_status in accept_statuses: return resp time.sleep(3) raise AssertionError( f"page.goto({url}) never returned a status in {accept_statuses} after " f"{attempts} attempts ({deadline_seconds}s); last status={last_status}, " f"last error={last_err or 'none'}" )