diff --git a/scripts/feedback_api.py b/scripts/feedback_api.py index 19ac09c..93cfd0a 100644 --- a/scripts/feedback_api.py +++ b/scripts/feedback_api.py @@ -13,6 +13,7 @@ from pathlib import Path import requests import yaml +from playwright.sync_api import sync_playwright _ROOT = Path(__file__).parent.parent _EMAIL_RE = re.compile(r"[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}") @@ -192,3 +193,24 @@ def upload_attachment( ) resp.raise_for_status() return resp.json().get("browser_download_url", "") + + +def screenshot_page(port: int | None = None) -> bytes | None: + """ + Capture a screenshot of the running Peregrine UI using Playwright. + Returns PNG bytes, or None if Playwright is not installed or if capture fails. + """ + if port is None: + port = int(os.environ.get("STREAMLIT_PORT", os.environ.get("STREAMLIT_SERVER_PORT", "8502"))) + + try: + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page(viewport={"width": 1280, "height": 800}) + page.goto(f"http://localhost:{port}", timeout=10_000) + page.wait_for_load_state("networkidle", timeout=10_000) + png = page.screenshot(full_page=False) + browser.close() + return png + except Exception: + return None diff --git a/tests/test_feedback_api.py b/tests/test_feedback_api.py index 8413e8b..e5cd3e8 100644 --- a/tests/test_feedback_api.py +++ b/tests/test_feedback_api.py @@ -242,3 +242,32 @@ def test_upload_attachment_returns_url(mock_post, monkeypatch): } url = upload_attachment(42, b"\x89PNG", "screenshot.png") assert url == "https://example.com/assets/abc" + + +# ── screenshot_page ─────────────────────────────────────────────────────────── + +def test_screenshot_page_returns_none_on_failure(monkeypatch): + """screenshot_page returns None gracefully when capture fails.""" + from scripts.feedback_api import screenshot_page + # Patch sync_playwright to raise an exception (simulates any failure) + import scripts.feedback_api as fapi + def bad_playwright(): + raise RuntimeError("browser unavailable") + monkeypatch.setattr(fapi, "sync_playwright", bad_playwright) + result = screenshot_page(port=9999) + assert result is None + + +@patch("scripts.feedback_api.sync_playwright") +def test_screenshot_page_returns_bytes(mock_pw): + """screenshot_page returns PNG bytes when playwright is available.""" + from scripts.feedback_api import screenshot_page + fake_png = b"\x89PNG\r\n\x1a\n" + mock_context = MagicMock() + mock_pw.return_value.__enter__ = lambda s: mock_context + mock_pw.return_value.__exit__ = MagicMock(return_value=False) + mock_browser = mock_context.chromium.launch.return_value + mock_page = mock_browser.new_page.return_value + mock_page.screenshot.return_value = fake_png + result = screenshot_page(port=8502) + assert result == fake_png