diff --git a/tests/test_cdp_browser.py b/tests/test_cdp_browser.py new file mode 100644 index 0000000..66a4b3f --- /dev/null +++ b/tests/test_cdp_browser.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +from unittest.mock import AsyncMock, MagicMock + +import pytest + +import config +from tools.cdp_browser import CDPBrowserManager + + +@pytest.mark.asyncio +async def test_existing_browser_connects_directly_to_devtools_browser(monkeypatch): + monkeypatch.setattr(config, "CDP_CONNECT_EXISTING", True) + monkeypatch.setattr(config, "BROWSER_LAUNCH_TIMEOUT", 60) + + manager = CDPBrowserManager() + manager.debug_port = 9222 + manager._get_browser_websocket_url = AsyncMock( # type: ignore[method-assign] + side_effect=AssertionError("existing browser mode must not call /json/version") + ) + + browser = MagicMock() + browser.is_connected.return_value = True + browser.contexts = [] + + playwright = MagicMock() + playwright.chromium.connect_over_cdp = AsyncMock(return_value=browser) + + await manager._connect_via_cdp(playwright) + + playwright.chromium.connect_over_cdp.assert_awaited_once_with( + "ws://localhost:9222/devtools/browser", + timeout=60000, + ) + + +@pytest.mark.asyncio +async def test_existing_browser_falls_back_to_discovered_websocket_url(monkeypatch): + monkeypatch.setattr(config, "CDP_CONNECT_EXISTING", True) + monkeypatch.setattr(config, "BROWSER_LAUNCH_TIMEOUT", 60) + + manager = CDPBrowserManager() + manager.debug_port = 9222 + manager._get_browser_websocket_url = AsyncMock( # type: ignore[method-assign] + return_value="ws://localhost:9222/devtools/browser/generated-id" + ) + + browser = MagicMock() + browser.is_connected.return_value = True + browser.contexts = [] + + playwright = MagicMock() + playwright.chromium.connect_over_cdp = AsyncMock( + side_effect=[RuntimeError("direct websocket failed"), browser] + ) + + await manager._connect_via_cdp(playwright) + + manager._get_browser_websocket_url.assert_awaited_once_with(9222) + assert playwright.chromium.connect_over_cdp.await_args_list[0].args == ( + "ws://localhost:9222/devtools/browser", + ) + assert playwright.chromium.connect_over_cdp.await_args_list[0].kwargs == { + "timeout": 60000, + } + assert playwright.chromium.connect_over_cdp.await_args_list[1].args == ( + "ws://localhost:9222/devtools/browser/generated-id", + ) + assert playwright.chromium.connect_over_cdp.await_args_list[1].kwargs == { + "timeout": 60000, + } + + +@pytest.mark.asyncio +async def test_launched_browser_uses_discovered_websocket_url(monkeypatch): + monkeypatch.setattr(config, "CDP_CONNECT_EXISTING", False) + + manager = CDPBrowserManager() + manager.debug_port = 9223 + manager._get_browser_websocket_url = AsyncMock( # type: ignore[method-assign] + return_value="ws://localhost:9223/devtools/browser/generated-id" + ) + + browser = MagicMock() + browser.is_connected.return_value = True + browser.contexts = [] + + playwright = MagicMock() + playwright.chromium.connect_over_cdp = AsyncMock(return_value=browser) + + await manager._connect_via_cdp(playwright) + + manager._get_browser_websocket_url.assert_awaited_once_with(9223) + playwright.chromium.connect_over_cdp.assert_awaited_once_with( + "ws://localhost:9223/devtools/browser/generated-id" + ) diff --git a/tools/cdp_browser.py b/tools/cdp_browser.py index c6d76b9..bc5a76c 100644 --- a/tools/cdp_browser.py +++ b/tools/cdp_browser.py @@ -316,16 +316,29 @@ class CDPBrowserManager: """ try: if config.CDP_CONNECT_EXISTING: - # Existing Chrome exposes the browser websocket URL from /json/version. - # Newer Chrome versions return a UUID path instead of plain /devtools/browser. - ws_url = await self._get_browser_websocket_url(self.debug_port) + # Existing browser remote debugging in Chrome 136+ does not expose + # /json/version. Connect directly and wait for user confirmation. + ws_url = f"ws://localhost:{self.debug_port}/devtools/browser" utils.logger.info(f"[CDPBrowserManager] Connecting to existing browser via CDP: {ws_url}") utils.logger.info( "[CDPBrowserManager] Please check your browser for a confirmation dialog and accept it" ) - self.browser = await playwright.chromium.connect_over_cdp( - ws_url, timeout=config.BROWSER_LAUNCH_TIMEOUT * 1000 - ) + try: + self.browser = await playwright.chromium.connect_over_cdp( + ws_url, timeout=config.BROWSER_LAUNCH_TIMEOUT * 1000 + ) + except Exception as direct_error: + utils.logger.warning( + "[CDPBrowserManager] Direct existing-browser CDP connection failed: " + f"{direct_error}. Trying /json/version discovery..." + ) + ws_url = await self._get_browser_websocket_url(self.debug_port) + utils.logger.info( + f"[CDPBrowserManager] Connecting to existing browser via discovered CDP: {ws_url}" + ) + self.browser = await playwright.chromium.connect_over_cdp( + ws_url, timeout=config.BROWSER_LAUNCH_TIMEOUT * 1000 + ) else: # For launched browser, get WebSocket URL first ws_url = await self._get_browser_websocket_url(self.debug_port)