Stabilize Internal Docker Browser rendering

Return WebUI navigation after the initial document commit and align the socket timeout with browser command deadlines. Remove forced viewport nudges and reject cropped or mismatched screencast frames so tab changes and pointer movement do not stretch the viewport.

Alessandro committed Jul 21, 2026 at 18:55 UTC 23af25e52ace7c8afba8c7aa1ecbb3346d121c39
4 files changed +120 -198
plugins/_browser/api/ws_browser.py
+27 -11
@@ -230,13 +230,18 @@ class WsBrowser(WsHandler):
230 if command == "open":
231 result = await runtime.call("open", data.get("url") or "")
232 elif command == "navigate":
233 - result = await runtime.call("navigate", browser_id, data.get("url") or "")
233 + result = await runtime.call(
234 + "navigate",
235 + browser_id,
236 + data.get("url") or "",
237 + wait_until="commit",
238 + )
239 elif command == "back":
235 - result = await runtime.call("back", browser_id)
240 + result = await runtime.call("back", browser_id, wait_until="commit")
241 elif command == "forward":
237 - result = await runtime.call("forward", browser_id)
242 + result = await runtime.call("forward", browser_id, wait_until="commit")
243 elif command == "reload":
239 - result = await runtime.call("reload", browser_id)
244 + result = await runtime.call("reload", browser_id, wait_until="commit")
245 elif command == "close":
246 result = await runtime.call("close_browser", browser_id)
247 elif command == "list":
@@ -717,18 +722,29 @@ class WsBrowser(WsHandler):
722 def _frame_dimensions(metadata: Any) -> dict[str, int]:
723 if not isinstance(metadata, dict):
724 return {}
720 - for width_key, height_key in (
721 - ("expectedWidth", "expectedHeight"),
722 - ("deviceWidth", "deviceHeight"),
723 - ("jpegWidth", "jpegHeight"),
724 - ):
725 +
726 + def dimensions(width_key: str, height_key: str) -> tuple[int, int] | None:
727 try:
728 width = int(metadata.get(width_key) or 0)
729 height = int(metadata.get(height_key) or 0)
730 except (TypeError, ValueError):
729 - continue
731 + return None
732 if width > 0 and height > 0:
731 - return {"width": width, "height": height}
733 + return width, height
734 + return None
735 +
736 + expected = dimensions("expectedWidth", "expectedHeight")
737 + jpeg = dimensions("jpegWidth", "jpegHeight")
738 + if expected and jpeg:
739 + width_scale = jpeg[0] / expected[0]
740 + height_scale = jpeg[1] / expected[1]
741 + if abs(width_scale - height_scale) <= 0.01:
742 + return {"width": expected[0], "height": expected[1]}
743 + return {"width": jpeg[0], "height": jpeg[1]}
744 +
745 + for fallback in (jpeg, dimensions("deviceWidth", "deviceHeight"), expected):
746 + if fallback:
747 + return {"width": fallback[0], "height": fallback[1]}
748 return {}
749
750 async def _emit_viewer_state(
plugins/_browser/helpers/runtime.py
+47 -58
@@ -40,7 +40,6 @@ CHROME_SINGLETON_FILES = ("SingletonLock", "SingletonCookie", "SingletonSocket")
40 SCREENCAST_MAX_WIDTH = 4096
41 SCREENCAST_MAX_HEIGHT = 4096
42 VIEWPORT_SIZE_TOLERANCE = 4
43 -VIEWPORT_REMOUNT_PAUSE_SECONDS = 0.05
43 CLIPBOARD_BRIDGE_SCRIPT = r"""
44 (payload) => {
45 const action = String(payload?.action || "").trim().toLowerCase();
@@ -282,18 +281,6 @@ CLIPBOARD_BRIDGE_SCRIPT = r"""
281 _SAFE_CONTEXT_RE = re.compile(r"[^a-zA-Z0-9_.-]+")
282
283
285 -def _nudged_viewport(viewport: dict[str, int]) -> dict[str, int]:
286 - width = int(viewport["width"])
287 - height = int(viewport["height"])
288 - if width < 4096:
289 - return {"width": width + 1, "height": height}
290 - if width > 320:
291 - return {"width": width - 1, "height": height}
292 - if height < 4096:
293 - return {"width": width, "height": height + 1}
294 - return {"width": width, "height": height - 1}
295 -
296 -
284 def _safe_context_id(context_id: str) -> str:
285 return _SAFE_CONTEXT_RE.sub("_", str(context_id or "default")).strip("._") or "default"
286
@@ -344,7 +331,7 @@ class _BrowserScreencast:
331 self._expected_height = height
332 with contextlib.suppress(Exception):
333 await self.session.send("Page.enable")
347 - await self._apply_cdp_viewport_with_remount({"width": width, "height": height})
334 + await self._apply_cdp_viewport({"width": width, "height": height})
335 await self.session.send(
336 "Page.startScreencast",
337 {
@@ -356,14 +343,6 @@ class _BrowserScreencast:
343 },
344 )
345
359 - async def _apply_cdp_viewport_with_remount(self, viewport: dict[str, int]) -> None:
360 - await self._apply_cdp_viewport(viewport)
361 - await asyncio.sleep(VIEWPORT_REMOUNT_PAUSE_SECONDS)
362 - await self._apply_cdp_viewport(_nudged_viewport(viewport))
363 - await asyncio.sleep(VIEWPORT_REMOUNT_PAUSE_SECONDS)
364 - await self._apply_cdp_viewport(viewport)
365 - await asyncio.sleep(VIEWPORT_REMOUNT_PAUSE_SECONDS)
366 -
346 async def _apply_cdp_viewport(self, viewport: dict[str, int]) -> None:
347 width = max(320, min(4096, int(viewport.get("width") or DEFAULT_VIEWPORT["width"])))
348 height = max(200, min(4096, int(viewport.get("height") or DEFAULT_VIEWPORT["height"])))
@@ -1247,38 +1226,59 @@ class _BrowserRuntimeCore:
1226 await self.ensure_started()
1227 return await self._state(self._resolve_browser_id(browser_id))
1228
1250 - async def navigate(self, browser_id: int | str | None, url: str) -> dict[str, Any]:
1229 + async def navigate(
1230 + self,
1231 + browser_id: int | str | None,
1232 + url: str,
1233 + *,
1234 + wait_until: str = "domcontentloaded",
1235 + ) -> dict[str, Any]:
1236 await self.ensure_started()
1237 resolved_id = self._resolve_browser_id(browser_id)
1238 page = self._page(resolved_id)
1254 - await self._goto(page, normalize_url(url))
1239 + await self._goto(page, normalize_url(url), wait_until=wait_until)
1240 self._maybe_promote(resolved_id)
1241 return await self._state(resolved_id)
1242
1258 - async def back(self, browser_id: int | str | None = None) -> dict[str, Any]:
1243 + async def back(
1244 + self,
1245 + browser_id: int | str | None = None,
1246 + *,
1247 + wait_until: str = "domcontentloaded",
1248 + ) -> dict[str, Any]:
1249 await self.ensure_started()
1250 resolved_id = self._resolve_browser_id(browser_id)
1251 page = self._page(resolved_id)
1262 - await page.go_back(wait_until="domcontentloaded", timeout=10000)
1263 - await self._settle(page)
1252 + await page.go_back(wait_until=wait_until, timeout=10000)
1253 + await self._settle(page, short=wait_until == "commit")
1254 self._maybe_promote(resolved_id)
1255 return await self._state(resolved_id)
1256
1267 - async def forward(self, browser_id: int | str | None = None) -> dict[str, Any]:
1257 + async def forward(
1258 + self,
1259 + browser_id: int | str | None = None,
1260 + *,
1261 + wait_until: str = "domcontentloaded",
1262 + ) -> dict[str, Any]:
1263 await self.ensure_started()
1264 resolved_id = self._resolve_browser_id(browser_id)
1265 page = self._page(resolved_id)
1271 - await page.go_forward(wait_until="domcontentloaded", timeout=10000)
1272 - await self._settle(page)
1266 + await page.go_forward(wait_until=wait_until, timeout=10000)
1267 + await self._settle(page, short=wait_until == "commit")
1268 self._maybe_promote(resolved_id)
1269 return await self._state(resolved_id)
1270
1276 - async def reload(self, browser_id: int | str | None = None) -> dict[str, Any]:
1271 + async def reload(
1272 + self,
1273 + browser_id: int | str | None = None,
1274 + *,
1275 + wait_until: str = "domcontentloaded",
1276 + ) -> dict[str, Any]:
1277 await self.ensure_started()
1278 resolved_id = self._resolve_browser_id(browser_id)
1279 page = self._page(resolved_id)
1280 - await page.reload(wait_until="domcontentloaded", timeout=15000)
1281 - await self._settle(page)
1280 + await page.reload(wait_until=wait_until, timeout=15000)
1281 + await self._settle(page, short=wait_until == "commit")
1282 self._maybe_promote(resolved_id)
1283 return await self._state(resolved_id)
1284
@@ -1748,33 +1748,16 @@ class _BrowserRuntimeCore:
1748 or abs(int(current_viewport.get("height") or 0) - viewport["height"])
1749 > VIEWPORT_SIZE_TOLERANCE
1750 )
1751 - should_remount_viewport = changed or restart_screencast
1752 - if should_remount_viewport:
1751 + should_restart_screencast = changed or restart_screencast
1752 + if should_restart_screencast:
1753 await self._stop_screencasts_for_browser(resolved_id)
1754 if changed:
1755 - await self._apply_viewport_with_remount(page, viewport)
1756 - elif restart_screencast:
1757 - await self._remount_viewport(page, viewport)
1758 - if should_remount_viewport:
1755 + await page.set_viewport_size(viewport)
1756 + if should_restart_screencast:
1757 await self._settle(page, short=True)
1758 self._maybe_promote(resolved_id)
1759 return {"state": await self._state(resolved_id), "viewport": viewport}
1760
1763 - async def _apply_viewport_with_remount(self, page: Any, viewport: dict[str, int]) -> None:
1764 - await page.set_viewport_size(viewport)
1765 - await asyncio.sleep(VIEWPORT_REMOUNT_PAUSE_SECONDS)
1766 - await self._remount_viewport(page, viewport)
1767 -
1768 - async def _remount_viewport(self, page: Any, viewport: dict[str, int]) -> None:
1769 - nudged_viewport = self._nudged_viewport(viewport)
1770 - await page.set_viewport_size(nudged_viewport)
1771 - await asyncio.sleep(VIEWPORT_REMOUNT_PAUSE_SECONDS)
1772 - await page.set_viewport_size(viewport)
1773 -
1774 - @staticmethod
1775 - def _nudged_viewport(viewport: dict[str, int]) -> dict[str, int]:
1776 - return _nudged_viewport(viewport)
1777 -
1761 async def _point_for(
1762 self,
1763 page: Any,
@@ -2223,17 +2206,23 @@ class _BrowserRuntimeCore:
2206 self._maybe_promote(resolved_id)
2207 return {"action": action or {}, "state": await self._state(resolved_id)}
2208
2226 - async def _goto(self, page: Any, url: str) -> None:
2209 + async def _goto(
2210 + self,
2211 + page: Any,
2212 + url: str,
2213 + *,
2214 + wait_until: str = "domcontentloaded",
2215 + ) -> None:
2216 from playwright.async_api import Error as PlaywrightError
2217 from playwright.async_api import TimeoutError as PlaywrightTimeoutError
2218
2219 try:
2231 - await page.goto(url, wait_until="domcontentloaded", timeout=30000)
2220 + await page.goto(url, wait_until=wait_until, timeout=30000)
2221 except PlaywrightTimeoutError:
2233 - PrintStyle.warning(f"Browser navigation timed out after DOM handoff: {url}")
2222 + PrintStyle.warning(f"Browser navigation timed out waiting for {wait_until}: {url}")
2223 except PlaywrightError as exc:
2224 PrintStyle.warning(f"Browser navigation showed a native error page for {url}: {exc}")
2236 - await self._settle(page)
2225 + await self._settle(page, short=wait_until == "commit")
2226
2227 async def _settle(self, page: Any, short: bool = False) -> None:
2228 from playwright.async_api import Error as PlaywrightError
plugins/_browser/webui/browser-store.js
+5 -71
@@ -19,6 +19,7 @@ websocket.addHandlers(["ws_webui"]);
19 const EXTENSIONS_ROOT = "/a0/usr/_browser/extensions";
20 const BROWSER_SUBSCRIBE_TIMEOUT_MS = 60000;
21 const BROWSER_FIRST_INSTALL_TIMEOUT_MS = 300000;
22 +const BROWSER_COMMAND_TIMEOUT_MS = 45000;
23 const BROWSER_CONFIG_REFRESH_MS = 15000;
24 const BROWSER_VIEWER_TRANSPORT_SNAPSHOT = "snapshot";
25 const BROWSER_VIEWER_TRANSPORT_SCREENCAST = "screencast";
@@ -211,9 +212,6 @@ const model = {
212 _surfaceHandoffTimer: null,
213 _surfaceOpenedAt: 0,
214 _surfaceOpenSequence: 0,
214 - _canvasSurfaceReadySequence: 0,
215 - _canvasFirstFrameAcceptedSequence: 0,
216 - _canvasFirstFrameNudgeSequence: 0,
215 _openPromise: null,
216 _openSignature: "",
217 _connectSequence: 0,
@@ -751,10 +749,6 @@ const model = {
749 } finally {
750 if (this.isCurrentSurfaceOpen(surfaceSequence)) {
751 this.loading = false;
754 - if (this._mode === "canvas") {
755 - this._canvasSurfaceReadySequence = surfaceSequence;
756 - this.scheduleCanvasWidthNudgeAfterFirstFrame();
757 - }
752 }
753 }
754 },
@@ -1298,11 +1292,9 @@ const model = {
1292 const viewport = this.currentViewportSize() || this._lastViewport;
1293 if (!this.frameMatchesViewport(dimensions, viewport)) {
1294 this.requestViewportSyncAfterRejectedFrame();
1301 - if (!this.shouldAcceptMismatchedFrame(dimensions)) {
1302 - bitmap?.close?.();
1303 - options?.cleanup?.();
1304 - return;
1305 - }
1295 + bitmap?.close?.();
1296 + options?.cleanup?.();
1297 + return;
1298 }
1299 if (bitmap && this.paintFrameBitmap(bitmap)) {
1300 this.clearFrameSrc();
@@ -1317,64 +1309,6 @@ const model = {
1309 this._lastFrameDimensions = dimensions;
1310 this._lastFrameAt = Date.now();
1311 options?.onAccepted?.();
1320 - this._canvasFirstFrameAcceptedSequence = surfaceSequence;
1321 - this.scheduleCanvasWidthNudgeAfterFirstFrame();
1322 - },
1323 -
1324 - shouldAcceptMismatchedFrame(dimensions = null) {
1325 - return Boolean(
1326 - dimensions?.width
1327 - && dimensions?.height
1328 - && (!this.hasFrame() || this._surfaceSwitching || this.isSwitchingBrowser())
1329 - );
1330 - },
1331 -
1332 - scheduleCanvasWidthNudgeAfterFirstFrame() {
1333 - const surfaceSequence = this._surfaceOpenSequence;
1334 - if (this._mode !== "canvas" || !this.isCurrentSurfaceOpen(surfaceSequence) || !this.activeBrowserId) {
1335 - return;
1336 - }
1337 - if (this._canvasFirstFrameNudgeSequence === surfaceSequence) {
1338 - return;
1339 - }
1340 - if (
1341 - this._canvasSurfaceReadySequence !== surfaceSequence
1342 - || this._canvasFirstFrameAcceptedSequence !== surfaceSequence
1343 - ) {
1344 - return;
1345 - }
1346 - this._canvasFirstFrameNudgeSequence = surfaceSequence;
1347 -
1348 - void (async () => {
1349 - await nextAnimationFrame();
1350 - await nextAnimationFrame();
1351 - if (!this.isCurrentSurfaceOpen(surfaceSequence) || this._mode !== "canvas") {
1352 - return;
1353 - }
1354 - this.forceRightCanvasWidthNudge();
1355 - })();
1356 - },
1357 -
1358 - forceRightCanvasWidthNudge() {
1359 - const canvas = rightCanvasStore;
1360 - if (!canvas || canvas.isMobileMode || !canvas.isOpen || canvas.activeSurfaceId !== "browser") {
1361 - return;
1362 - }
1363 -
1364 - const currentWidth = Number(canvas.width || 0);
1365 - if (!Number.isFinite(currentWidth) || currentWidth <= 0) {
1366 - return;
1367 - }
1368 - const maxWidth = Number(canvas.maxWidth?.() || currentWidth);
1369 - const minWidth = Number(canvas.minWidth || 420);
1370 - const direction = currentWidth < maxWidth ? 1 : -1;
1371 - const nudgedWidth = currentWidth + direction;
1372 - if (nudgedWidth < minWidth || nudgedWidth > maxWidth || nudgedWidth === currentWidth) {
1373 - return;
1374 - }
1375 -
1376 - canvas.setWidth?.(nudgedWidth, { persist: false });
1377 - this.queueViewportSync(true);
1312 },
1313
1314 frameMatchesViewport(dimensions = null, viewport = null) {
@@ -1525,7 +1459,7 @@ const model = {
1459 viewer_transport: this.requestedViewerTransport(),
1460 command,
1461 },
1528 - { timeoutMs: 20000 },
1462 + { timeoutMs: BROWSER_COMMAND_TIMEOUT_MS },
1463 );
1464 const data = firstOk(response);
1465 this.applyTabScope(data);
tests/test_browser_agent_regressions.py
+41 -58
@@ -707,6 +707,7 @@ def test_browser_viewer_allows_slow_extension_startup():
707
708 assert "const BROWSER_SUBSCRIBE_TIMEOUT_MS = 60000;" in js
709 assert "const BROWSER_FIRST_INSTALL_TIMEOUT_MS = 300000;" in js
710 + assert "const BROWSER_COMMAND_TIMEOUT_MS = 45000;" in js
711 assert "? BROWSER_FIRST_INSTALL_TIMEOUT_MS" in js
712 assert ": BROWSER_SUBSCRIBE_TIMEOUT_MS" in js
713 assert "Installing Chromium for the first Browser run" in js
@@ -796,32 +797,17 @@ def test_browser_canvas_surface_open_waits_for_visible_panel():
797 assert "forceCanvasWidthNudgeAfterBrowserMount" not in js
798
799
799 -def test_browser_canvas_nudges_width_after_first_accepted_frame():
800 +def test_browser_canvas_does_not_resize_after_first_accepted_frame():
801 js = (PROJECT_ROOT / "plugins" / "_browser" / "webui" / "browser-store.js").read_text(
802 encoding="utf-8"
803 )
804
804 - assert "_canvasSurfaceReadySequence" in js
805 - assert "_canvasFirstFrameAcceptedSequence" in js
806 - assert "_canvasFirstFrameNudgeSequence" in js
807 - assert "scheduleCanvasWidthNudgeAfterFirstFrame()" in js
808 - assert "this._canvasSurfaceReadySequence = surfaceSequence;" in js
809 - assert "const surfaceSequence = this._surfaceOpenSequence;" in js
810 - assert "this._canvasFirstFrameAcceptedSequence = surfaceSequence;" in js
811 - assert "forceRightCanvasWidthNudge()" in js
812 - assert "await nextAnimationFrame();" in js
813 - assert "globalThis.Alpine" not in js
814 - assert 'import { store as rightCanvasStore } from "/components/canvas/right-canvas-store.js";' in js
815 - assert "const canvas = rightCanvasStore;" in js
816 - assert 'canvas.activeSurfaceId !== "browser"' in js
817 - assert "canvas.setWidth?.(nudgedWidth, { persist: false })" in js
818 - assert "this.queueViewportSync(true)" in js
819 - frame_accept_index = js.index("this._lastFrameDimensions = dimensions;")
820 - frame_nudge_schedule_index = js.index(
821 - "this.scheduleCanvasWidthNudgeAfterFirstFrame();",
822 - frame_accept_index,
823 - )
824 - assert frame_accept_index < frame_nudge_schedule_index
805 + assert "_canvasSurfaceReadySequence" not in js
806 + assert "_canvasFirstFrameAcceptedSequence" not in js
807 + assert "_canvasFirstFrameNudgeSequence" not in js
808 + assert "scheduleCanvasWidthNudgeAfterFirstFrame" not in js
809 + assert "forceRightCanvasWidthNudge" not in js
810 + assert "canvas.setWidth?.(nudgedWidth" not in js
811
812
813 def test_browser_canvas_restarts_stream_after_page_navigation():
@@ -1423,18 +1409,17 @@ def test_browser_viewer_defaults_to_live_screencast_with_snapshot_fallback():
1409 assert "restart_stream: restartStream && this.usesScreencastTransport()" in browser_store
1410 assert 'restart_screencast=bool(data.get("restart_stream"))' in ws_browser
1411 assert "restart_screencast: bool = False" in runtime
1426 - assert "should_remount_viewport = changed or restart_screencast" in runtime
1427 - assert "VIEWPORT_REMOUNT_PAUSE_SECONDS = 0.05" in runtime
1428 - assert "await self._apply_cdp_viewport_with_remount" in runtime
1429 - assert "await self._apply_viewport_with_remount(page, viewport)" in runtime
1430 - assert "await self._remount_viewport(page, viewport)" in runtime
1431 - assert "await asyncio.sleep(VIEWPORT_REMOUNT_PAUSE_SECONDS)" in runtime
1432 - assert "def _nudged_viewport(viewport: dict[str, int])" in runtime
1412 + assert "should_restart_screencast = changed or restart_screencast" in runtime
1413 + assert "await self._apply_cdp_viewport({\"width\": width, \"height\": height})" in runtime
1414 + assert "await page.set_viewport_size(viewport)" in runtime
1415 + assert "VIEWPORT_REMOUNT_PAUSE_SECONDS" not in runtime
1416 + assert "_nudged_viewport" not in runtime
1417 + assert 'wait_until="commit"' in ws_browser
1418 assert 'restartStream: this._mode === "canvas" && this.usesScreencastTransport()' in browser_store
1419 assert "this.frameState = data.state || null" not in browser_store
1420 assert "function loadFrameDimensions(src)" in browser_store
1421 assert "frameMatchesViewport(dimensions = null, viewport = null)" in browser_store
1437 - assert "shouldAcceptMismatchedFrame(dimensions = null)" in browser_store
1422 + assert "shouldAcceptMismatchedFrame" not in browser_store
1423 assert "requestViewportSyncAfterRejectedFrame()" in browser_store
1424 assert "this.applySnapshot(data.snapshot);" in browser_store
1425 assert "if (!this.frameCanvasReady || !this.usesScreencastTransport())" in browser_store
@@ -1495,6 +1480,27 @@ def test_browser_viewer_frame_payload_supports_binary_slim_frames():
1480 assert "state" not in payload
1481
1482
1483 +def test_browser_viewer_frame_dimensions_reject_crops_but_allow_uniform_scaling():
1484 + dimensions = ws_browser_module.WsBrowser._frame_dimensions
1485 +
1486 + assert dimensions(
1487 + {
1488 + "expectedWidth": 900,
1489 + "expectedHeight": 600,
1490 + "jpegWidth": 1800,
1491 + "jpegHeight": 1200,
1492 + }
1493 + ) == {"width": 900, "height": 600}
1494 + assert dimensions(
1495 + {
1496 + "expectedWidth": 900,
1497 + "expectedHeight": 600,
1498 + "jpegWidth": 900,
1499 + "jpegHeight": 500,
1500 + }
1501 + ) == {"width": 900, "height": 500}
1502 +
1503 +
1504 def test_browser_navigation_errors_stay_inside_native_browser_page():
1505 runtime = (
1506 PROJECT_ROOT / "plugins" / "_browser" / "helpers" / "runtime.py"
@@ -1796,7 +1802,7 @@ async def test_browser_screencast_acknowledges_and_drops_stale_frames():
1802 for method, params in session.sent
1803 if method == "Emulation.setVisibleSize"
1804 ]
1799 - assert metrics_calls[:3] == [
1805 + assert metrics_calls == [
1806 {
1807 "width": 1118,
1808 "height": 662,
@@ -1804,26 +1810,8 @@ async def test_browser_screencast_acknowledges_and_drops_stale_frames():
1810 "mobile": False,
1811 "dontSetVisibleSize": True,
1812 },
1807 - {
1808 - "width": 1119,
1809 - "height": 662,
1810 - "deviceScaleFactor": 1,
1811 - "mobile": False,
1812 - "dontSetVisibleSize": True,
1813 - },
1814 - {
1815 - "width": 1118,
1816 - "height": 662,
1817 - "deviceScaleFactor": 1,
1818 - "mobile": False,
1819 - "dontSetVisibleSize": True,
1820 - },
1821 - ]
1822 - assert visible_calls[:3] == [
1823 - {"width": 1118, "height": 662},
1824 - {"width": 1119, "height": 662},
1825 - {"width": 1118, "height": 662},
1813 ]
1814 + assert visible_calls == [{"width": 1118, "height": 662}]
1815 start_index = next(
1816 index
1817 for index, (method, _params) in enumerate(session.sent)
@@ -3072,7 +3060,7 @@ async def test_browser_viewer_viewport_input_dispatches_resize(monkeypatch):
3060
3061
3062 @pytest.mark.anyio
3075 -async def test_browser_runtime_remounts_same_viewport_when_restarting_screencast():
3063 +async def test_browser_runtime_restarts_screencast_without_resizing_same_viewport():
3064 viewport_calls = []
3065 stopped = []
3066 settled = []
@@ -3106,16 +3094,13 @@ async def test_browser_runtime_remounts_same_viewport_when_restarting_screencast
3094 "state": {"id": 7},
3095 "viewport": {"width": 1280, "height": 720},
3096 }
3109 - assert viewport_calls == [
3110 - {"width": 1281, "height": 720},
3111 - {"width": 1280, "height": 720},
3112 - ]
3097 + assert viewport_calls == []
3098 assert stopped == [7]
3099 assert settled == [True]
3100
3101
3102 @pytest.mark.anyio
3118 -async def test_browser_runtime_remounts_initial_changed_viewport():
3103 +async def test_browser_runtime_applies_changed_viewport_once():
3104 calls = []
3105 stopped = []
3106 settled = []
@@ -3151,8 +3136,6 @@ async def test_browser_runtime_remounts_initial_changed_viewport():
3136 }
3137 assert calls == [
3138 ("viewport", {"width": 672, "height": 789}),
3154 - ("viewport", {"width": 673, "height": 789}),
3155 - ("viewport", {"width": 672, "height": 789}),
3139 ]
3140 assert stopped == [7]
3141 assert settled == [True]