diff --git a/cloud/playwright_browser.py b/cloud/playwright_browser.py index 2d1a77e..03e00ab 100644 --- a/cloud/playwright_browser.py +++ b/cloud/playwright_browser.py @@ -120,6 +120,7 @@ class Browser: wait_until="domcontentloaded") self._current_url = self._page.url self._start_screencast() + self._eager_frame() except Exception as exc: # noqa: BLE001 raise BrowserError("navigate failed: %s" % exc) result = self._info() @@ -321,9 +322,35 @@ class Browser: self._cdp = None self._cdp_page = None + def _eager_frame(self): + """Push one current-viewport frame into the sink, best-effort. + + Screencast frames only arrive on repaint, and the page may + already have painted before startScreencast ran -- so a + static page would otherwise show nothing until its next + repaint. Capturing the viewport once right after navigate + guarantees the bridge file (and any open stream) has a + picture within milliseconds of the page loading. + """ + if self._cdp is None or self._frame_sink is None: + return + try: + result = self._cdp.send( + "Page.captureScreenshot", + {"format": "jpeg", "quality": 60}) + data = result.get("data") if isinstance(result, dict) \ + else None + if data: + # Empty session id: pump stores it but must not ack + # it (there is no screencast frame to acknowledge). + self._frame_sink.put(("", data)) + except Exception: # noqa: BLE001 - screencast still flows + pass + def pump_screencast(self): """Main-thread only: store the latest frame and ack every - queued frame (no ack = CDP stops sending after the first). + real screencast frame (no ack = CDP stops sending after the + first). Eager frames carry no session id and skip ack. Returns the latest frame data (base64 jpeg) or ''.""" if self._frame_sink is None: return "" @@ -335,7 +362,7 @@ class Browser: break if data: latest = data - if self._cdp is not None: + if self._cdp is not None and sid: try: self._cdp.send("Page.screencastFrameAck", {"sessionId": sid}) diff --git a/cloud/tests/test_playwright.py b/cloud/tests/test_playwright.py index 893865c..43867bd 100644 --- a/cloud/tests/test_playwright.py +++ b/cloud/tests/test_playwright.py @@ -59,12 +59,18 @@ class _FakeCDP: def __init__(self): self.sent = [] self.handlers = {} + self.fail_capture = False def on(self, event, handler): self.handlers[event] = handler def send(self, method, params=None): self.sent.append((method, params or {})) + if method == "Page.captureScreenshot": + if self.fail_capture: + raise RuntimeError("capture boom") + return {"data": "EAGERDATA"} + return None class _FakeContext: @@ -259,6 +265,33 @@ class BrowserTest(unittest.TestCase): self.assertIn(("Page.screencastFrameAck", {"sessionId": "s1"}), [(m, p) for m, p in self.page.cdp.sent]) + def test_navigate_pushes_eager_frame_without_ack(self): + from playwright_remote import SimpleQueue + sink = SimpleQueue() + self.browser.set_frame_sink(sink) + result = self.browser.navigate("https://example.com") + self.assertTrue(result["ok"]) + self.assertIn(("Page.captureScreenshot", + {"format": "jpeg", "quality": 60}), + [(m, p) for m, p in self.page.cdp.sent]) + frame = self.browser.pump_screencast() + self.assertEqual(frame, "EAGERDATA") + acks = [(m, p) for m, p in self.page.cdp.sent + if m == "Page.screencastFrameAck"] + self.assertEqual(acks, []) + + def test_navigate_survives_failed_eager_capture(self): + from playwright_remote import SimpleQueue + sink = SimpleQueue() + self.browser.set_frame_sink(sink) + self.page.cdp.fail_capture = True + try: + result = self.browser.navigate("https://example.com") + finally: + self.page.cdp.fail_capture = False + self.assertTrue(result["ok"]) + self.assertEqual(self.browser.pump_screencast(), "") + def test_live_shot_writes_fixed_file_and_status(self): self.browser.navigate("https://example.com") out = self.browser.live_shot()