feat(shot): harness default capture fix — bounded networkidle settle after domcontentloaded + blank-frame retry (≤60s wait budget, R7 best-effort preserved); 6 unit tests; lint PASS, 205 unit tests pass via cc-ci-run
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@ -32,6 +32,90 @@ def test_hook_returned_when_callable():
|
||||
assert S._load_screenshot_hook({"SCREENSHOT": hook}) is hook
|
||||
|
||||
|
||||
class _FakePage:
|
||||
"""Minimal Playwright-page stand-in for the settle/blank-retry helpers (no browser needed)."""
|
||||
|
||||
def __init__(self, shot_sizes, idle_raises=False):
|
||||
self._shot_sizes = list(shot_sizes) # bytes written per successive screenshot() call
|
||||
self._idle_raises = idle_raises
|
||||
self.idle_waits = [] # (state, timeout) per wait_for_load_state call
|
||||
self.timeout_waits = [] # ms per wait_for_timeout call
|
||||
self.shots = 0
|
||||
|
||||
def wait_for_load_state(self, state, timeout=None):
|
||||
self.idle_waits.append((state, timeout))
|
||||
if self._idle_raises:
|
||||
raise TimeoutError(f"page kept polling past {timeout}ms")
|
||||
|
||||
def wait_for_timeout(self, ms):
|
||||
self.timeout_waits.append(ms)
|
||||
|
||||
def screenshot(self, path, full_page=False):
|
||||
self.shots += 1
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"\x89PNG" + b"\0" * (self._shot_sizes.pop(0) - 4))
|
||||
|
||||
|
||||
def test_settle_swallows_never_idle_pages():
|
||||
"""R7: an app that never reaches network-idle (continuous polling) must not raise — the
|
||||
timeout cap IS the wait."""
|
||||
page = _FakePage([], idle_raises=True)
|
||||
S._settle(page, 1234) # must not raise
|
||||
assert page.idle_waits == [("networkidle", 1234)]
|
||||
assert page.timeout_waits == [S.RENDER_GRACE_MS]
|
||||
|
||||
|
||||
def test_snap_retries_blank_frame(tmp_path):
|
||||
"""A blank-sized first frame (audit fingerprint: 4801 B) triggers exactly one retry with a
|
||||
longer settle, overwriting the tiny frame with the later (painted) one."""
|
||||
out = str(tmp_path / "shot.png")
|
||||
page = _FakePage([4801, 30256])
|
||||
S._snap_with_blank_retry(page, out)
|
||||
assert page.shots == 2
|
||||
assert page.idle_waits == [("networkidle", S.BLANK_RETRY_SETTLE_MS)]
|
||||
assert os.path.getsize(out) == 30256
|
||||
|
||||
|
||||
def test_snap_no_retry_for_real_frame(tmp_path):
|
||||
"""A real-sized first frame is kept as-is — no second screenshot, no extra waiting."""
|
||||
out = str(tmp_path / "shot.png")
|
||||
page = _FakePage([35707])
|
||||
S._snap_with_blank_retry(page, out)
|
||||
assert page.shots == 1
|
||||
assert page.idle_waits == []
|
||||
assert os.path.getsize(out) == 35707
|
||||
|
||||
|
||||
def test_snap_retry_keeps_late_frame_even_if_still_blank(tmp_path):
|
||||
"""If the retry frame is still tiny we keep it (honest best-effort) — exactly one retry,
|
||||
never a loop."""
|
||||
out = str(tmp_path / "shot.png")
|
||||
page = _FakePage([4801, 4801])
|
||||
S._snap_with_blank_retry(page, out)
|
||||
assert page.shots == 2
|
||||
assert os.path.getsize(out) == 4801
|
||||
|
||||
|
||||
def test_blank_threshold_brackets_observed_sizes():
|
||||
"""Threshold sits between the audited defect sizes (blank 4801-2 B, lone spinners up to
|
||||
8764 B) and the smallest real page (custom-html-tiny, 12950 B)."""
|
||||
for defect in (4801, 4802, 5895, 6022, 7913, 8764):
|
||||
assert defect < S.BLANK_SIZE_BYTES
|
||||
assert S.BLANK_SIZE_BYTES < 12950
|
||||
|
||||
|
||||
def test_wait_budget_within_step_cap():
|
||||
"""plan-phase-shot §3 P3: the screenshot step's bounded waiting must stay ≤ ~60s worst case."""
|
||||
total_ms = (
|
||||
S.NAV_DEADLINE_S * 1000
|
||||
+ S.SETTLE_TIMEOUT_MS
|
||||
+ S.RENDER_GRACE_MS
|
||||
+ S.BLANK_RETRY_SETTLE_MS
|
||||
+ S.RENDER_GRACE_MS
|
||||
)
|
||||
assert total_ms <= 60_000, f"screenshot wait budget {total_ms}ms exceeds the ~60s step cap"
|
||||
|
||||
|
||||
def test_screenshot_reachable_through_real_load_path(tmp_path):
|
||||
"""R2 proof (rcust P1): a recipe SCREENSHOT hook declared in recipe_meta.py arrives at
|
||||
screenshot._load_screenshot_hook through the REAL orchestrator load path (meta.load — the
|
||||
|
||||
Reference in New Issue
Block a user