15
15
# specific language governing permissions and limitations
16
16
# under the License.
17
17
18
+ import datetime
19
+ import math
18
20
from dataclasses import dataclass
19
21
from typing import Any , Optional
20
22
23
+ from selenium .common .exceptions import WebDriverException
21
24
from selenium .webdriver .common .bidi .common import command_builder
22
25
23
26
from .log import LogEntryAdded
@@ -238,12 +241,15 @@ class Script:
238
241
"realm_destroyed" : "script.realmDestroyed" ,
239
242
}
240
243
241
- def __init__ (self , conn ):
244
+ def __init__ (self , conn , driver = None ):
242
245
self .conn = conn
246
+ self .driver = driver
243
247
self .log_entry_subscribed = False
244
248
self .subscriptions = {}
245
249
self .callbacks = {}
246
250
251
+ # High-level APIs for SCRIPT module
252
+
247
253
def add_console_message_handler (self , handler ):
248
254
self ._subscribe_to_log_entries ()
249
255
return self .conn .add_callback (LogEntryAdded , self ._handle_log_entry ("console" , handler ))
@@ -258,6 +264,122 @@ def remove_console_message_handler(self, id):
258
264
259
265
remove_javascript_error_handler = remove_console_message_handler
260
266
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
+
261
383
# low-level APIs for script module
262
384
def _add_preload_script (
263
385
self ,
0 commit comments