From 80e5713c5c6c2dfb4e7b7be0f202b3b3b91f1d41 Mon Sep 17 00:00:00 2001 From: autonomic-bot Date: Thu, 11 Jun 2026 06:19:39 +0000 Subject: [PATCH] =?UTF-8?q?feat(shot):=20mattermost-lts=20SCREENSHOT=20hoo?= =?UTF-8?q?k=20=E2=86=92=20/login=20(default=20lands=20the=20desktop-or-br?= =?UTF-8?q?owser=20interstitial;=20watch-list=20wants=20the=20real=20sign-?= =?UTF-8?q?in=20form)=20+=20public=20screenshot.settle()=20for=20hooks;=20?= =?UTF-8?q?unit=20test=20via=20real=20loader;=20206=20unit=20tests=20pass,?= =?UTF-8?q?=20lint=20PASS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- runner/harness/screenshot.py | 6 ++++++ tests/mattermost-lts/recipe_meta.py | 19 +++++++++++++++++++ tests/unit/test_screenshot.py | 27 +++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/runner/harness/screenshot.py b/runner/harness/screenshot.py index 7527764..f7936ef 100644 --- a/runner/harness/screenshot.py +++ b/runner/harness/screenshot.py @@ -58,6 +58,12 @@ def _settle(page, idle_timeout_ms: int) -> None: page.wait_for_timeout(RENDER_GRACE_MS) +def settle(page, idle_timeout_ms: int = SETTLE_TIMEOUT_MS) -> None: + """Public settle for recipe SCREENSHOT hooks: after the hook navigates to its safe view, call + this so the snap happens post-paint. Same bounded best-effort contract as the default path.""" + _settle(page, idle_timeout_ms) + + def _snap_with_blank_retry(page, out_path: str) -> None: """Screenshot the page; if the PNG is blank/spinner-sized, retry ONCE after a longer settle. The retry overwrites the tiny frame with a strictly-later one (same page, more paint time).""" diff --git a/tests/mattermost-lts/recipe_meta.py b/tests/mattermost-lts/recipe_meta.py index 50f49df..5ccbf57 100644 --- a/tests/mattermost-lts/recipe_meta.py +++ b/tests/mattermost-lts/recipe_meta.py @@ -18,3 +18,22 @@ HEALTH_OK = (200, 302) DEPLOY_TIMEOUT = 900 HTTP_TIMEOUT = 600 EXTRA_ENV = {"TIMEOUT": "600"} + + +def SCREENSHOT(page, ctx): + """Land the real sign-in form for the CI card (phase-shot). The default landing capture gets + mattermost's "view in desktop app or browser?" interstitial at `/` — a real page but not + representative of the app. `/login` renders the standard login form directly. Credential-free + (empty fields, R7 secret-safety: never a page showing generated secrets); the harness snaps + the PNG after this returns.""" + from harness import browser as harness_browser + from harness import screenshot as screenshot_mod + + harness_browser.goto_with_retry( + page, + f"{ctx.base_url}/login", + accept_statuses=(200,), + deadline_seconds=screenshot_mod.NAV_DEADLINE_S, + wait_until="domcontentloaded", + ) + screenshot_mod.settle(page) diff --git a/tests/unit/test_screenshot.py b/tests/unit/test_screenshot.py index 43968bc..a25ca5a 100644 --- a/tests/unit/test_screenshot.py +++ b/tests/unit/test_screenshot.py @@ -116,6 +116,33 @@ def test_wait_budget_within_step_cap(): assert total_ms <= 60_000, f"screenshot wait budget {total_ms}ms exceeds the ~60s step cap" +def test_mattermost_screenshot_hook_lands_login(): + """phase-shot: mattermost-lts ships the first real SCREENSHOT hook — `/` serves the + desktop-or-browser interstitial, so the hook must navigate to /login (the representative, + credential-free sign-in form) and settle; the harness then snaps the PNG.""" + + class _Resp: + status = 200 + + class _NavPage(_FakePage): + def __init__(self): + super().__init__([]) + self.urls = [] + + def goto(self, url, wait_until=None, timeout=None): + self.urls.append(url) + return _Resp() + + tests_dir = os.path.join(os.path.dirname(__file__), "..") + meta = meta_mod.load("mattermost-lts", tests_dir=tests_dir) + hook = S._load_screenshot_hook(meta) + assert callable(hook), "mattermost-lts SCREENSHOT hook missing from the real load path" + page = _NavPage() + hook(page, meta_mod.hook_ctx("mm.example.org", meta)) + assert page.urls == ["https://mm.example.org/login"] + assert page.idle_waits, "hook must settle before the harness snaps" + + 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