Phase 2 lesson from F2-3 (n8n install Playwright flake on net::ERR_NETWORK_CHANGED): every install overlay that does page.goto needs the same try/except PlaywrightError + status retry. Centralize in runner/harness/browser.py::goto_with_retry; apply to ALL install overlays. - runner/harness/browser.py: shared helper. Polls page.goto until status in accept_statuses; catches PlaywrightError (net::ERR_*) as a retryable signal, not a failure. Raises AssertionError with last_status + last_err diagnostic only on deadline expiry. - tests/custom-html/test_install.py: now uses goto_with_retry (200 only, wait_until=load). - tests/custom-html/playwright/test_browser_smoke.py: same. - tests/n8n/test_install.py: replaced inline retry loop with goto_with_retry (200, 304). - tests/keycloak/test_install.py: goto_with_retry for admin console (200, 302, 303; 45s goto). - tests/cryptpad/test_install.py: goto_with_retry (200, 304; 60s goto, wait_until=load). - tests/lasuite-docs/test_install.py: goto_with_retry (200, 301, 302; 60s goto). Cold-verifiable: ssh cc-ci 'RECIPE=custom-html cc-ci-run runner/run_recipe_ci.py' all 5 stages PASS (including the install overlay that flaked in the deps_smoke run), deploy-count=1, head_ref=8a026066==chaos-version=8a026066 (HC1 non-vacuous). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
57 lines
2.4 KiB
Python
57 lines
2.4 KiB
Python
"""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'}"
|
|
)
|