1515# specific language governing permissions and limitations
1616# under the License.
1717
18+ import datetime
19+ import math
1820from dataclasses import dataclass
1921from typing import Any , Optional
2022
23+ from selenium .common .exceptions import WebDriverException
2124from selenium .webdriver .common .bidi .common import command_builder
2225
2326from .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 ,
0 commit comments