Skip to content

Commit 134c0b7

Browse files
authored
[py][bidi]: add high level API for script module - pin, unpin and execute (#15936)
1 parent a9fb7e5 commit 134c0b7

File tree

3 files changed

+434
-3
lines changed

3 files changed

+434
-3
lines changed

py/selenium/webdriver/common/bidi/script.py

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18+
import datetime
19+
import math
1820
from dataclasses import dataclass
1921
from typing import Any, Optional
2022

23+
from selenium.common.exceptions import WebDriverException
2124
from selenium.webdriver.common.bidi.common import command_builder
2225

2326
from .log import LogEntryAdded
@@ -238,12 +241,15 @@ class Script:
238241
"realm_destroyed": "script.realmDestroyed",
239242
}
240243

241-
def __init__(self, conn):
244+
def __init__(self, conn, driver=None):
242245
self.conn = conn
246+
self.driver = driver
243247
self.log_entry_subscribed = False
244248
self.subscriptions = {}
245249
self.callbacks = {}
246250

251+
# High-level APIs for SCRIPT module
252+
247253
def add_console_message_handler(self, handler):
248254
self._subscribe_to_log_entries()
249255
return self.conn.add_callback(LogEntryAdded, self._handle_log_entry("console", handler))
@@ -258,6 +264,122 @@ def remove_console_message_handler(self, id):
258264

259265
remove_javascript_error_handler = remove_console_message_handler
260266

267+
def pin(self, script: str) -> str:
268+
"""Pins a script to the current browsing context.
269+
270+
Parameters:
271+
-----------
272+
script: The script to pin.
273+
274+
Returns:
275+
-------
276+
str: The ID of the pinned script.
277+
"""
278+
return self._add_preload_script(script)
279+
280+
def unpin(self, script_id: str) -> None:
281+
"""Unpins a script from the current browsing context.
282+
283+
Parameters:
284+
-----------
285+
script_id: The ID of the pinned script to unpin.
286+
"""
287+
self._remove_preload_script(script_id)
288+
289+
def execute(self, script: str, *args) -> dict:
290+
"""Executes a script in the current browsing context.
291+
292+
Parameters:
293+
-----------
294+
script: The script function to execute.
295+
*args: Arguments to pass to the script function.
296+
297+
Returns:
298+
-------
299+
dict: The result value from the script execution.
300+
301+
Raises:
302+
------
303+
WebDriverException: If the script execution fails.
304+
"""
305+
306+
if self.driver is None:
307+
raise WebDriverException("Driver reference is required for script execution")
308+
browsing_context_id = self.driver.current_window_handle
309+
310+
# Convert arguments to the format expected by BiDi call_function (LocalValue Type)
311+
arguments = []
312+
for arg in args:
313+
arguments.append(self.__convert_to_local_value(arg))
314+
315+
target = {"context": browsing_context_id}
316+
317+
result = self._call_function(
318+
function_declaration=script, await_promise=True, target=target, arguments=arguments if arguments else None
319+
)
320+
321+
if result.type == "success":
322+
return result.result
323+
else:
324+
error_message = "Error while executing script"
325+
if result.exception_details:
326+
if "text" in result.exception_details:
327+
error_message += f": {result.exception_details['text']}"
328+
elif "message" in result.exception_details:
329+
error_message += f": {result.exception_details['message']}"
330+
331+
raise WebDriverException(error_message)
332+
333+
def __convert_to_local_value(self, value) -> dict:
334+
"""
335+
Converts a Python value to BiDi LocalValue format.
336+
"""
337+
if value is None:
338+
return {"type": "null"}
339+
elif isinstance(value, bool):
340+
return {"type": "boolean", "value": value}
341+
elif isinstance(value, (int, float)):
342+
if isinstance(value, float):
343+
if math.isnan(value):
344+
return {"type": "number", "value": "NaN"}
345+
elif math.isinf(value):
346+
if value > 0:
347+
return {"type": "number", "value": "Infinity"}
348+
else:
349+
return {"type": "number", "value": "-Infinity"}
350+
elif value == 0.0 and math.copysign(1.0, value) < 0:
351+
return {"type": "number", "value": "-0"}
352+
353+
JS_MAX_SAFE_INTEGER = 9007199254740991
354+
if isinstance(value, int) and (value > JS_MAX_SAFE_INTEGER or value < -JS_MAX_SAFE_INTEGER):
355+
return {"type": "bigint", "value": str(value)}
356+
357+
return {"type": "number", "value": value}
358+
359+
elif isinstance(value, str):
360+
return {"type": "string", "value": value}
361+
elif isinstance(value, datetime.datetime):
362+
# Convert Python datetime to JavaScript Date (ISO 8601 format)
363+
return {"type": "date", "value": value.isoformat() + "Z" if value.tzinfo is None else value.isoformat()}
364+
elif isinstance(value, datetime.date):
365+
# Convert Python date to JavaScript Date
366+
dt = datetime.datetime.combine(value, datetime.time.min).replace(tzinfo=datetime.timezone.utc)
367+
return {"type": "date", "value": dt.isoformat()}
368+
elif isinstance(value, set):
369+
return {"type": "set", "value": [self.__convert_to_local_value(item) for item in value]}
370+
elif isinstance(value, (list, tuple)):
371+
return {"type": "array", "value": [self.__convert_to_local_value(item) for item in value]}
372+
elif isinstance(value, dict):
373+
return {
374+
"type": "object",
375+
"value": [
376+
[self.__convert_to_local_value(k), self.__convert_to_local_value(v)] for k, v in value.items()
377+
],
378+
}
379+
else:
380+
# For other types, convert to string
381+
return {"type": "string", "value": str(value)}
382+
261383
# low-level APIs for script module
262384
def _add_preload_script(
263385
self,

py/selenium/webdriver/remote/webdriver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1240,7 +1240,7 @@ def script(self):
12401240
self._start_bidi()
12411241

12421242
if not self._script:
1243-
self._script = Script(self._websocket_connection)
1243+
self._script = Script(self._websocket_connection, self)
12441244

12451245
return self._script
12461246

0 commit comments

Comments
 (0)