|
4 | 4 | import base64 as _b64 |
5 | 5 | import contextlib |
6 | 6 | import io |
| 7 | +import json |
7 | 8 | import logging |
8 | 9 | import shutil |
9 | 10 | import warnings |
|
49 | 50 | IFrameNotFound, |
50 | 51 | InvalidFileExtension, |
51 | 52 | InvalidIFrame, |
52 | | - InvalidScriptWithElement, |
53 | 53 | InvalidTabInitialization, |
54 | 54 | MissingScreenshotPath, |
55 | 55 | NavigationError, |
56 | 56 | NetworkEventsNotEnabled, |
57 | 57 | NoDialogPresent, |
58 | 58 | NotAnIFrame, |
59 | 59 | PageLoadTimeout, |
| 60 | + ScriptExecutionError, |
60 | 61 | TopLevelTargetRequired, |
61 | 62 | WaitElementTimeout, |
62 | 63 | WebSocketConnectionClosed, |
|
70 | 71 | from pydoll.protocol.cdp.page.events import PageEvent |
71 | 72 | from pydoll.protocol.cdp.page.types import FrameResourceTree, ScreenshotFormat |
72 | 73 | from pydoll.protocol.cdp.runtime.methods import ( |
73 | | - CallFunctionOnResponse, |
74 | 74 | EvaluateResponse, |
75 | | - SerializationOptions, |
76 | 75 | ) |
77 | | -from pydoll.protocol.cdp.runtime.types import CallArgument |
78 | 76 | from pydoll.protocol.cdp.target.types import TargetInfo |
79 | 77 | from pydoll.protocol.events import CDP_DOMAIN_MAP, CDP_EVENT_MAP, Event |
80 | 78 | from pydoll.utils import ( |
|
113 | 111 | from pydoll.protocol.cdp.network.methods import GetCookiesResponse as NetworkGetCookiesResponse |
114 | 112 | from pydoll.protocol.cdp.network.methods import GetResponseBodyResponse |
115 | 113 | from pydoll.protocol.cdp.network.types import ( |
116 | | - Cookie, |
117 | | - CookieParam, |
118 | 114 | ErrorReason, |
119 | 115 | RequestMethod, |
120 | 116 | ) |
|
126 | 122 | NavigateResponse, |
127 | 123 | PrintToPDFResponse, |
128 | 124 | ) |
129 | | - from pydoll.protocol.cdp.runtime.methods import CallFunctionOnResponse, EvaluateResponse |
| 125 | + from pydoll.protocol.cdp.runtime.methods import EvaluateResponse |
130 | 126 | from pydoll.protocol.cdp.storage.methods import GetCookiesResponse as StorageGetCookiesResponse |
131 | 127 | from pydoll.protocol.cdp.target.methods import AttachToTargetResponse, GetTargetsResponse |
| 128 | + from pydoll.protocol.types import Cookie, CookieParam |
132 | 129 |
|
133 | 130 | logger = logging.getLogger(__name__) |
134 | 131 |
|
@@ -1176,8 +1173,8 @@ async def _fetch_document_html(self, frame_tree: FrameResourceTree) -> str: |
1176 | 1173 | return html |
1177 | 1174 | except Exception: |
1178 | 1175 | logger.debug('getResourceContent failed for document, falling back to JS') |
1179 | | - response = await self.execute_script('return document.documentElement.outerHTML') |
1180 | | - return cast(str, response['result']['result']['value']) |
| 1176 | + html = await self.execute_script('return document.documentElement.outerHTML') |
| 1177 | + return cast(str, html) |
1181 | 1178 |
|
1182 | 1179 | async def _fetch_bundle_assets( |
1183 | 1180 | self, |
@@ -1258,175 +1255,53 @@ async def handle_dialog(self, accept: bool, prompt_text: Optional[str] = None): |
1258 | 1255 | PageCommands.handle_javascript_dialog(accept=accept, prompt_text=prompt_text) |
1259 | 1256 | ) |
1260 | 1257 |
|
1261 | | - @overload |
1262 | | - async def execute_script( |
1263 | | - self, |
1264 | | - script: str, |
1265 | | - *, |
1266 | | - object_group: Optional[str] = None, |
1267 | | - include_command_line_api: Optional[bool] = None, |
1268 | | - silent: Optional[bool] = None, |
1269 | | - context_id: Optional[int] = None, |
1270 | | - return_by_value: Optional[bool] = None, |
1271 | | - generate_preview: Optional[bool] = None, |
1272 | | - user_gesture: Optional[bool] = None, |
1273 | | - await_promise: Optional[bool] = None, |
1274 | | - throw_on_side_effect: Optional[bool] = None, |
1275 | | - timeout: Optional[float] = None, |
1276 | | - disable_breaks: Optional[bool] = None, |
1277 | | - repl_mode: Optional[bool] = None, |
1278 | | - allow_unsafe_eval_blocked_by_csp: Optional[bool] = None, |
1279 | | - unique_context_id: Optional[str] = None, |
1280 | | - serialization_options: Optional[SerializationOptions] = None, |
1281 | | - ) -> EvaluateResponse: ... |
1282 | | - |
1283 | | - @overload |
1284 | | - async def execute_script( |
1285 | | - self, |
1286 | | - script: str, |
1287 | | - element: WebElement, |
1288 | | - *, |
1289 | | - arguments: Optional[list[CallArgument]] = None, |
1290 | | - silent: Optional[bool] = None, |
1291 | | - return_by_value: Optional[bool] = None, |
1292 | | - generate_preview: Optional[bool] = None, |
1293 | | - user_gesture: Optional[bool] = None, |
1294 | | - await_promise: Optional[bool] = None, |
1295 | | - execution_context_id: Optional[int] = None, |
1296 | | - object_group: Optional[str] = None, |
1297 | | - throw_on_side_effect: Optional[bool] = None, |
1298 | | - unique_context_id: Optional[str] = None, |
1299 | | - serialization_options: Optional[SerializationOptions] = None, |
1300 | | - ) -> CallFunctionOnResponse: ... |
1301 | | - |
1302 | | - async def execute_script( |
1303 | | - self, |
1304 | | - script: str, |
1305 | | - element: Optional[WebElement] = None, |
1306 | | - *, |
1307 | | - arguments: Optional[list[CallArgument]] = None, |
1308 | | - object_group: Optional[str] = None, |
1309 | | - include_command_line_api: Optional[bool] = None, |
1310 | | - silent: Optional[bool] = None, |
1311 | | - context_id: Optional[int] = None, |
1312 | | - return_by_value: Optional[bool] = None, |
1313 | | - generate_preview: Optional[bool] = None, |
1314 | | - user_gesture: Optional[bool] = None, |
1315 | | - await_promise: Optional[bool] = None, |
1316 | | - execution_context_id: Optional[int] = None, |
1317 | | - throw_on_side_effect: Optional[bool] = None, |
1318 | | - timeout: Optional[float] = None, |
1319 | | - disable_breaks: Optional[bool] = None, |
1320 | | - repl_mode: Optional[bool] = None, |
1321 | | - allow_unsafe_eval_blocked_by_csp: Optional[bool] = None, |
1322 | | - unique_context_id: Optional[str] = None, |
1323 | | - serialization_options: Optional[SerializationOptions] = None, |
1324 | | - ) -> Union[EvaluateResponse, CallFunctionOnResponse]: |
1325 | | - """ |
1326 | | - Execute JavaScript in page context. |
| 1258 | + async def execute_script(self, script: str, *args: object) -> object: |
| 1259 | + """Execute JavaScript in the page and return its deserialized result. |
1327 | 1260 |
|
1328 | 1261 | Args: |
1329 | | - script (str): JavaScript code to execute. |
1330 | | - element (Optional[WebElement]): Optional WebElement to execute script on. |
1331 | | - arguments (Optional[list[CallArgument]]): Arguments to pass to the function. |
1332 | | - object_group (Optional[str]): Symbolic group name for the result (Runtime.evaluate). |
1333 | | - include_command_line_api (Optional[bool]): Whether to include command line API |
1334 | | - (Runtime.evaluate). |
1335 | | - silent (Optional[bool]): Whether to silence exceptions (Runtime.evaluate). |
1336 | | - context_id (Optional[int]): ID of the execution context to evaluate in |
1337 | | - (Runtime.evaluate). |
1338 | | - return_by_value (Optional[bool]): Whether to return the result by value instead of |
1339 | | - reference (Runtime.evaluate). |
1340 | | - generate_preview (Optional[bool]): Whether to generate a preview for the result |
1341 | | - (Runtime.evaluate). |
1342 | | - user_gesture (Optional[bool]): Whether to treat evaluation as initiated by user |
1343 | | - gesture (Runtime.evaluate). |
1344 | | - await_promise (Optional[bool]): Whether to await promise result (Runtime.evaluate). |
1345 | | - execution_context_id (Optional[int]): ID of the execution context to call the |
1346 | | - function in. |
1347 | | - throw_on_side_effect (Optional[bool]): Whether to throw if side effect cannot be |
1348 | | - ruled out (Runtime.evaluate). |
1349 | | - timeout (Optional[float]): Timeout in milliseconds (Runtime.evaluate). |
1350 | | - disable_breaks (Optional[bool]): Whether to disable breakpoints during evaluation |
1351 | | - (Runtime.evaluate). |
1352 | | - repl_mode (Optional[bool]): Whether to execute in REPL mode (Runtime.evaluate). |
1353 | | - allow_unsafe_eval_blocked_by_csp (Optional[bool]): Allow unsafe evaluation |
1354 | | - (Runtime.evaluate). |
1355 | | - unique_context_id (Optional[str]): Unique context ID for evaluation |
1356 | | - (Runtime.evaluate). |
1357 | | - serialization_options (Optional[SerializationOptions]): Serialization options for |
1358 | | - the result (Runtime.evaluate). |
| 1262 | + script: JavaScript to run. With no args, a bare ``return`` statement is |
| 1263 | + wrapped automatically. With args, pass a function expression |
| 1264 | + (e.g. ``(a, b) => a + b``) — the args are forwarded to it. |
| 1265 | + *args: JSON-serializable values forwarded to the script function. |
1359 | 1266 |
|
1360 | 1267 | Returns: |
1361 | | - Union[EvaluateResponse, CallFunctionOnResponse]: The result of the script execution. |
| 1268 | + The script's return value as a Python object (None when it returns nothing). |
1362 | 1269 |
|
1363 | 1270 | Raises: |
1364 | | - InvalidScriptWithElement: If script uses 'argument' keyword but no element is provided. |
| 1271 | + ScriptExecutionError: If the script throws. |
1365 | 1272 |
|
1366 | | - Examples: |
1367 | | - # Execute a simple script to log a message |
1368 | | - await page.execute_script('console.log("Hello World")') |
1369 | | -
|
1370 | | - # Execute a script that returns the page title |
1371 | | - await page.execute_script('return document.title') |
1372 | | -
|
1373 | | - # Execute a script on an element to click it |
1374 | | - await page.execute_script('argument.click()', element) |
1375 | | -
|
1376 | | - # Execute a script on an element to set its value |
1377 | | - await page.execute_script('argument.value = "Hello"', element) |
1378 | | - """ |
1379 | | - logger.debug(f'Executing script: with_element={bool(element)}, length={len(script)}') |
1380 | | - if element is not None: |
1381 | | - warnings.warn( |
1382 | | - 'Passing a WebElement to Tab.execute_script() is deprecated. ' |
1383 | | - 'Use WebElement.execute_script() instead.', |
1384 | | - DeprecationWarning, |
1385 | | - stacklevel=2, |
1386 | | - ) |
| 1273 | + Note: |
| 1274 | + For fine-grained CDP control (silent, contextId, raw RemoteObject, etc.) |
| 1275 | + use ``execute_protocol_command(RuntimeCommands.evaluate(...))``. |
1387 | 1276 |
|
1388 | | - return await element.execute_script( |
1389 | | - script, |
1390 | | - arguments=arguments, |
1391 | | - silent=silent, |
1392 | | - return_by_value=return_by_value, |
1393 | | - generate_preview=generate_preview, |
1394 | | - user_gesture=user_gesture, |
1395 | | - await_promise=await_promise, |
1396 | | - execution_context_id=execution_context_id, |
1397 | | - object_group=object_group, |
1398 | | - throw_on_side_effect=throw_on_side_effect, |
1399 | | - unique_context_id=unique_context_id, |
1400 | | - serialization_options=serialization_options, |
1401 | | - ) |
| 1277 | + Examples: |
| 1278 | + await tab.execute_script('return document.title') |
| 1279 | + await tab.execute_script('(a, b) => a + b', 2, 3) |
| 1280 | + """ |
| 1281 | + if args: |
| 1282 | + serialized = ', '.join(json.dumps(arg) for arg in args) |
| 1283 | + expression = f'({script})({serialized})' |
| 1284 | + elif has_return_outside_function(script): |
| 1285 | + expression = f'(function(){{ {script} }})()' |
| 1286 | + else: |
| 1287 | + expression = script |
1402 | 1288 |
|
1403 | | - if has_return_outside_function(script): |
1404 | | - script = f'(function(){{ {script} }})()' |
1405 | | - |
1406 | | - command = self._get_evaluate_command( |
1407 | | - script, |
1408 | | - object_group=object_group, |
1409 | | - include_command_line_api=include_command_line_api, |
1410 | | - silent=silent, |
1411 | | - context_id=context_id, |
1412 | | - return_by_value=return_by_value, |
1413 | | - generate_preview=generate_preview, |
1414 | | - user_gesture=user_gesture, |
1415 | | - await_promise=await_promise, |
1416 | | - throw_on_side_effect=throw_on_side_effect, |
1417 | | - timeout=timeout, |
1418 | | - disable_breaks=disable_breaks, |
1419 | | - repl_mode=repl_mode, |
1420 | | - allow_unsafe_eval_blocked_by_csp=allow_unsafe_eval_blocked_by_csp, |
1421 | | - unique_context_id=unique_context_id, |
1422 | | - serialization_options=serialization_options, |
1423 | | - ) |
1424 | | - logger.debug(f'Executing script without element: length={len(script)}') |
1425 | | - result: Union[EvaluateResponse, CallFunctionOnResponse] = await self._execute_command( |
1426 | | - command |
| 1289 | + logger.debug(f'Executing script: length={len(script)}, args={len(args)}') |
| 1290 | + response: EvaluateResponse = await self._execute_command( |
| 1291 | + RuntimeCommands.evaluate(expression, return_by_value=True, await_promise=True) |
1427 | 1292 | ) |
1428 | | - self._validate_argument_error(result) |
1429 | | - return result |
| 1293 | + return self._extract_script_value(response) |
| 1294 | + |
| 1295 | + @staticmethod |
| 1296 | + def _extract_script_value(response: EvaluateResponse) -> object: |
| 1297 | + """Extract the Python value from a Runtime.evaluate response, raising on JS errors.""" |
| 1298 | + evaluate_result = response.get('result', {}) |
| 1299 | + exception_details = evaluate_result.get('exceptionDetails') |
| 1300 | + if exception_details: |
| 1301 | + exception = exception_details.get('exception', {}) |
| 1302 | + message = exception.get('description') or exception_details.get('text', 'Script error') |
| 1303 | + raise ScriptExecutionError(message) |
| 1304 | + return evaluate_result.get('result', {}).get('value') |
1430 | 1305 |
|
1431 | 1306 | # TODO: think about how to remove these duplications with the base class |
1432 | 1307 | async def continue_request( |
@@ -1849,73 +1724,6 @@ def _get_connection_handler(self) -> ConnectionHandler: |
1849 | 1724 | ) |
1850 | 1725 | return ConnectionHandler(self._connection_port, self._target_id) |
1851 | 1726 |
|
1852 | | - @staticmethod |
1853 | | - def _get_evaluate_command( |
1854 | | - script: str, |
1855 | | - *, |
1856 | | - object_group: Optional[str] = None, |
1857 | | - include_command_line_api: Optional[bool] = None, |
1858 | | - silent: Optional[bool] = None, |
1859 | | - context_id: Optional[int] = None, |
1860 | | - return_by_value: Optional[bool] = None, |
1861 | | - generate_preview: Optional[bool] = None, |
1862 | | - user_gesture: Optional[bool] = None, |
1863 | | - await_promise: Optional[bool] = None, |
1864 | | - throw_on_side_effect: Optional[bool] = None, |
1865 | | - timeout: Optional[float] = None, |
1866 | | - disable_breaks: Optional[bool] = None, |
1867 | | - repl_mode: Optional[bool] = None, |
1868 | | - allow_unsafe_eval_blocked_by_csp: Optional[bool] = None, |
1869 | | - unique_context_id: Optional[str] = None, |
1870 | | - serialization_options: Optional[SerializationOptions] = None, |
1871 | | - ): |
1872 | | - """Create an evaluate command with the given parameters.""" |
1873 | | - return RuntimeCommands.evaluate( |
1874 | | - expression=script, |
1875 | | - object_group=object_group, |
1876 | | - include_command_line_api=include_command_line_api, |
1877 | | - silent=silent, |
1878 | | - context_id=context_id, |
1879 | | - return_by_value=return_by_value, |
1880 | | - generate_preview=generate_preview, |
1881 | | - user_gesture=user_gesture, |
1882 | | - await_promise=await_promise, |
1883 | | - throw_on_side_effect=throw_on_side_effect, |
1884 | | - timeout=timeout, |
1885 | | - disable_breaks=disable_breaks, |
1886 | | - repl_mode=repl_mode, |
1887 | | - allow_unsafe_eval_blocked_by_csp=allow_unsafe_eval_blocked_by_csp, |
1888 | | - unique_context_id=unique_context_id, |
1889 | | - serialization_options=serialization_options, |
1890 | | - ) |
1891 | | - |
1892 | | - @staticmethod |
1893 | | - def _validate_argument_error(response: EvaluateResponse) -> None: |
1894 | | - """ |
1895 | | - Validate that script didn't fail with ReferenceError about 'argument' being undefined. |
1896 | | -
|
1897 | | - Raises: |
1898 | | - InvalidScriptWithElement: If script uses 'argument' keyword but no element was provided. |
1899 | | - """ |
1900 | | - evaluate_result = response.get('result') |
1901 | | - if not isinstance(evaluate_result, dict): |
1902 | | - return |
1903 | | - |
1904 | | - remote_object = evaluate_result.get('result') |
1905 | | - if not isinstance(remote_object, dict): |
1906 | | - return |
1907 | | - |
1908 | | - if not ( |
1909 | | - remote_object.get('type') == 'object' |
1910 | | - and remote_object.get('subtype') == 'error' |
1911 | | - and remote_object.get('className') == 'ReferenceError' |
1912 | | - ): |
1913 | | - return |
1914 | | - |
1915 | | - description = remote_object.get('description', '') |
1916 | | - if 'argument is not defined' in description: |
1917 | | - raise InvalidScriptWithElement('Script contains "argument" but no element was provided') |
1918 | | - |
1919 | 1727 | _PAGE_LOAD_EVENT_MAP = { |
1920 | 1728 | PageLoadState.INTERACTIVE: PageEvent.DOM_CONTENT_EVENT_FIRED, |
1921 | 1729 | PageLoadState.COMPLETE: PageEvent.LOAD_EVENT_FIRED, |
|
0 commit comments