Skip to content

Commit 75c8c97

Browse files
authored
Merge pull request #63 from cleitonleonel/main
Handle case where Browser.WindowID is not found and improve error handling.
2 parents d2d5d80 + ba0dd58 commit 75c8c97

File tree

4 files changed

+178
-6
lines changed

4 files changed

+178
-6
lines changed

pydoll/browser/base.py

+50-6
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
7777
exc_val: The exception value, if raised.
7878
exc_tb: The traceback, if an exception was raised.
7979
"""
80-
await self.stop()
80+
if await self._is_browser_running():
81+
await self.stop()
82+
8183
await self._connection_handler.close()
8284

8385
async def start(self) -> None:
@@ -137,6 +139,7 @@ async def get_page(self) -> Page:
137139
page_id = (
138140
await self.new_page() if not self._pages else self._pages.pop()
139141
)
142+
140143
return Page(self._connection_port, page_id)
141144

142145
async def delete_all_cookies(self):
@@ -243,10 +246,25 @@ async def get_window_id(self):
243246
Retrieves the ID of the current browser window.
244247
245248
Returns:
246-
str: The ID of the current browser window.
249+
int: The ID of the current browser window.
250+
251+
Raises:
252+
RuntimeError: If unable to retrieve the window ID.
247253
"""
248-
response = await self._execute_command(BrowserCommands.get_window_id())
249-
return response['result']['windowId']
254+
command = BrowserCommands.get_window_id()
255+
response = await self._execute_command(command)
256+
257+
if response.get('error'):
258+
pages = await self.get_targets()
259+
target_id = await self._get_valid_target_id(pages)
260+
response = await self._execute_command(
261+
BrowserCommands.get_window_id_by_target(target_id)
262+
)
263+
264+
if window_id := response.get('result', {}).get('windowId'):
265+
return window_id
266+
267+
raise RuntimeError(response.get('error', {}))
250268

251269
async def set_window_bounds(self, bounds: dict):
252270
"""
@@ -519,7 +537,7 @@ def _is_valid_page(page: dict) -> bool:
519537
'url', ''
520538
)
521539

522-
async def _get_valid_page(self, pages) -> str:
540+
async def _get_valid_page(self, pages: list) -> str:
523541
"""
524542
Gets the ID of a valid page or creates a new one.
525543
@@ -541,6 +559,31 @@ async def _get_valid_page(self, pages) -> str:
541559

542560
return await self.new_page()
543561

562+
@staticmethod
563+
async def _get_valid_target_id(pages: list) -> str:
564+
"""
565+
Retrieves the target ID of a valid attached browser page.
566+
567+
Returns:
568+
str: The target ID of a valid page.
569+
570+
"""
571+
572+
valid_page = next(
573+
(page for page in pages
574+
if page.get('type') == 'page' and page.get('attached')),
575+
None
576+
)
577+
578+
if not valid_page:
579+
raise RuntimeError("No valid attached browser page found.")
580+
581+
target_id = valid_page.get('targetId')
582+
if not target_id:
583+
raise RuntimeError("Valid page found but missing 'targetId'.")
584+
585+
return target_id
586+
544587
async def _is_browser_running(self, timeout: int = 10) -> bool:
545588
"""
546589
Checks if the browser process is currently running.
@@ -553,9 +596,10 @@ async def _is_browser_running(self, timeout: int = 10) -> bool:
553596
if await self._connection_handler.ping():
554597
return True
555598
await asyncio.sleep(1)
599+
556600
return False
557601

558-
async def _execute_command(self, command: str):
602+
async def _execute_command(self, command: dict):
559603
"""
560604
Executes a command through the connection handler.
561605

pydoll/browser/page.py

+1
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ async def has_dialog(self) -> bool:
157157
"""
158158
if self._connection_handler.dialog:
159159
return True
160+
160161
return False
161162

162163
async def get_dialog_message(self) -> str:

pydoll/commands/browser.py

+19
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ class BrowserCommands:
1717

1818
CLOSE = {'method': 'Browser.close'}
1919
GET_WINDOW_ID = {'method': 'Browser.WindowID'}
20+
GET_WINDOW_ID_BY_TARGET = {
21+
'method': 'Browser.getWindowForTarget',
22+
'params': {},
23+
}
2024
SET_WINDOW_BOUNDS_TEMPLATE = {
2125
'method': 'Browser.setWindowBounds',
2226
'params': {},
@@ -62,6 +66,21 @@ def get_window_id(cls) -> dict:
6266
"""
6367
return cls.GET_WINDOW_ID
6468

69+
@classmethod
70+
def get_window_id_by_target(cls, target_id: str) -> dict:
71+
"""
72+
Generates the command to get the ID of the current window.
73+
74+
Args:
75+
target_id (str): The target_id to set for the window.
76+
77+
Returns:
78+
dict: The command to be sent to the browser.
79+
"""
80+
command = cls.GET_WINDOW_ID_BY_TARGET.copy()
81+
command['params']['targetId'] = target_id
82+
return command
83+
6584
@classmethod
6685
def set_window_bounds(cls, window_id: int, bounds: dict) -> dict:
6786
"""

tests/test_browser_window.py

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import pytest
2+
import pytest_asyncio
3+
from unittest.mock import AsyncMock, MagicMock, patch
4+
from pydoll.browser.base import Browser
5+
from pydoll.commands.browser import BrowserCommands
6+
7+
8+
class ConcreteBrowser(Browser):
9+
def _get_default_binary_location(self) -> str:
10+
return '/fake/path/to/browser'
11+
12+
13+
@pytest_asyncio.fixture
14+
async def mock_browser():
15+
with patch.multiple(
16+
Browser,
17+
_get_default_binary_location=MagicMock(
18+
return_value='/fake/path/to/browser'
19+
),
20+
), patch(
21+
'pydoll.connection.connection.ConnectionHandler',
22+
autospec=True,
23+
) as mock_conn_handler:
24+
browser = ConcreteBrowser()
25+
browser._connection_handler = mock_conn_handler.return_value
26+
browser._connection_handler.execute_command = AsyncMock()
27+
browser._connection_handler.register_callback = AsyncMock()
28+
29+
browser._pages = ['page1']
30+
31+
yield browser
32+
33+
34+
@pytest.mark.asyncio
35+
async def test_get_window_id_success(mock_browser):
36+
mock_browser._connection_handler.execute_command.return_value = {
37+
'result': {'windowId': 123}
38+
}
39+
result = await mock_browser.get_window_id()
40+
assert result == 123
41+
42+
43+
@pytest.mark.asyncio
44+
async def test_get_window_id_with_error_and_retry(mock_browser):
45+
mock_browser._execute_command = AsyncMock(side_effect=[
46+
{'error': 'some error'},
47+
{'result': {
48+
'targetInfos': [{
49+
'type': 'page',
50+
'attached': True,
51+
'targetId': 'target1'
52+
}]
53+
}},
54+
{'result': {'windowId': 123}}
55+
])
56+
57+
result = await mock_browser.get_window_id()
58+
assert result == 123
59+
60+
61+
@pytest.mark.asyncio
62+
async def test_get_window_id_failure(mock_browser):
63+
mock_browser._connection_handler.execute_command.return_value = {
64+
'error': 'some error'
65+
}
66+
mock_browser.get_targets = AsyncMock(return_value=[])
67+
with pytest.raises(RuntimeError):
68+
await mock_browser.get_window_id()
69+
70+
71+
@pytest.mark.asyncio
72+
async def test_get_valid_target_id_success(mock_browser):
73+
pages = [{
74+
'type': 'page',
75+
'attached': True,
76+
'targetId': 'target1'
77+
}]
78+
result = await mock_browser._get_valid_target_id(pages)
79+
assert result == 'target1'
80+
81+
82+
@pytest.mark.asyncio
83+
async def test_get_valid_target_id_no_valid_page(mock_browser):
84+
pages = []
85+
with pytest.raises(RuntimeError, match="No valid attached browser page found."):
86+
await mock_browser._get_valid_target_id(pages)
87+
88+
89+
@pytest.mark.asyncio
90+
async def test_get_valid_target_id_missing_target_id(mock_browser):
91+
pages = [{
92+
'type': 'page',
93+
'attached': True,
94+
'targetId': None
95+
}]
96+
with pytest.raises(RuntimeError, match="Valid page found but missing 'targetId'."):
97+
await mock_browser._get_valid_target_id(pages)
98+
99+
100+
@pytest.mark.asyncio
101+
async def test_get_window_id_by_target(mock_browser):
102+
expected_command = {
103+
'method': 'Browser.getWindowForTarget',
104+
'params': {
105+
'targetId': 'target1',
106+
},
107+
}
108+
assert BrowserCommands.get_window_id_by_target('target1') == expected_command

0 commit comments

Comments
 (0)