Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/py/private/bidi_enhancements_manifest.py
11809 views
1
# Licensed to the Software Freedom Conservancy (SFC) under one
2
# or more contributor license agreements. See the NOTICE file
3
# distributed with this work for additional information
4
# regarding copyright ownership. The SFC licenses this file
5
# to you under the Apache License, Version 2.0 (the
6
# "License"); you may not use this file except in compliance
7
# with the License. You may obtain a copy of the License at
8
#
9
# http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing,
12
# software distributed under the License is distributed on an
13
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
# KIND, either express or implied. See the License for the
15
# specific language governing permissions and limitations
16
# under the License.
17
18
19
"""
20
Enhancement manifest for BiDi code generation.
21
22
This file defines custom enhancements applied to generated BiDi modules,
23
including custom dataclass methods, parameter validation/transformation,
24
response deserialization, and field extraction.
25
26
All code must be compatible with Python 3.10+.
27
"""
28
29
from __future__ import annotations
30
31
from typing import Any
32
33
# ============================================================================
34
# Format Guide
35
# ============================================================================
36
# Each module in ENHANCEMENTS specifies enhancement rules for methods:
37
#
38
# 'module_name': {
39
# 'method_name': {
40
# 'dataclass_methods': { # For dataclass enhancements
41
# 'ClassName': ['method1', 'method2', ...]
42
# },
43
# 'preprocess': { # Pre-processing on parameters
44
# 'param_name': 'check_serialize_method'
45
# },
46
# 'deserialize': { # Deserialize response to typed objects
47
# 'response_field': 'TypeName',
48
# },
49
# 'extract_field': str, # Extract nested field from response
50
# 'extract_property': str, # Extract property from extracted items
51
# 'validate': str, # Validation function name
52
# 'transform': str, # Transformation function name
53
# }
54
# }
55
# ============================================================================
56
57
ENHANCEMENTS: dict[str, dict[str, Any]] = {
58
"browser": {
59
# Dataclass custom methods
60
"__dataclass_methods__": {
61
"ClientWindowInfo": [
62
"get_client_window",
63
"get_state",
64
"get_width",
65
"get_height",
66
"is_active",
67
"get_x",
68
"get_y",
69
],
70
},
71
# Method enhancements
72
"create_user_context": {
73
"preprocess": {
74
"proxy": "check_serialize_method",
75
"unhandled_prompt_behavior": "check_serialize_method",
76
},
77
"extract_field": "userContext",
78
},
79
"get_client_windows": {
80
"deserialize": {
81
"clientWindows": "ClientWindowInfo",
82
},
83
},
84
"get_user_contexts": {
85
"extract_field": "userContexts",
86
"extract_property": "userContext",
87
},
88
"set_download_behavior": {
89
"params_override": {
90
"allowed": "bool",
91
"destination_folder": "str",
92
"userContexts": "[*browser.UserContext]",
93
},
94
"validate": "validate_download_behavior",
95
"transform": {
96
"allowed": "allowed",
97
"destination_folder": "destination_folder",
98
"func": "transform_download_params",
99
"result_param": "download_behavior",
100
},
101
},
102
# Replace the auto-generated ClientWindowNamedState so we can add the
103
# convenience NORMAL constant. In the BiDi spec "normal" is the state
104
# represented by ClientWindowRectState, but exposing it here keeps the
105
# Python API consistent with the old ClientWindowState enum.
106
"exclude_types": ["ClientWindowNamedState", "SetClientWindowStateParameters"],
107
"extra_dataclasses": [
108
'''class ClientWindowNamedState:
109
"""Named states for a browser client window."""
110
111
FULLSCREEN = "fullscreen"
112
MAXIMIZED = "maximized"
113
MINIMIZED = "minimized"
114
NORMAL = "normal"''',
115
'''@dataclass
116
class SetClientWindowStateParameters:
117
"""SetClientWindowStateParameters.
118
119
The ``state`` field is required and must be either a named-state string
120
(e.g. ``ClientWindowNamedState.MAXIMIZED``) or a
121
:class:`ClientWindowRectState` instance. ``client_window`` is the ID of
122
the window to affect.
123
"""
124
125
client_window: Any | None = None
126
state: Any | None = None''',
127
],
128
# Override the generator-produced set_download_behavior so that
129
# downloadBehavior is never stripped by the generic None filter.
130
# The BiDi spec marks it as required (can be null, but must be present).
131
"extra_methods": [
132
''' def set_download_behavior(
133
self,
134
allowed: bool | None = None,
135
destination_folder: str | None = None,
136
user_contexts: list[Any] | None = None,
137
):
138
"""Set the download behavior for the browser.
139
140
Args:
141
allowed: ``True`` to allow downloads, ``False`` to deny, or ``None``
142
to reset to browser default (sends ``null`` to the protocol).
143
destination_folder: Destination folder for downloads. Required when
144
``allowed=True``. Accepts a string or :class:`pathlib.Path`.
145
user_contexts: Optional list of user context IDs.
146
147
Raises:
148
ValueError: If *allowed* is ``True`` and *destination_folder* is
149
omitted, or ``False`` and *destination_folder* is provided.
150
"""
151
validate_download_behavior(
152
allowed=allowed,
153
destination_folder=destination_folder,
154
user_contexts=user_contexts,
155
)
156
download_behavior = transform_download_params(allowed, destination_folder)
157
# downloadBehavior is a REQUIRED field in the BiDi spec (can be null but
158
# must be present). Do NOT use a generic None-filter on it.
159
params: dict = {"downloadBehavior": download_behavior}
160
if user_contexts is not None:
161
params["userContexts"] = user_contexts
162
cmd = command_builder("browser.setDownloadBehavior", params)
163
return self._conn.execute(cmd)''',
164
''' def set_client_window_state(
165
self,
166
client_window: Any | None = None,
167
state: Any | None = None,
168
):
169
"""Set the client window state.
170
171
Args:
172
client_window: The client window ID to apply the state to.
173
state: The window state to set. Can be one of:
174
- A string: "fullscreen", "maximized", "minimized", "normal"
175
- A ClientWindowRectState object with width, height, x, y
176
- A dict representing the state
177
178
Raises:
179
ValueError: If client_window is not provided or state is invalid.
180
"""
181
if client_window is None:
182
raise ValueError("client_window is required")
183
if state is None:
184
raise ValueError("state is required")
185
186
# Serialize ClientWindowRectState if needed
187
state_param = state
188
if hasattr(state, '__dataclass_fields__'):
189
# It's a dataclass, convert to dict
190
state_param = {
191
k: v for k, v in state.__dict__.items()
192
if v is not None
193
}
194
195
params = {
196
"clientWindow": client_window,
197
"state": state_param,
198
}
199
cmd = command_builder("browser.setClientWindowState", params)
200
return self._conn.execute(cmd)''',
201
],
202
},
203
"browsingContext": {
204
# Method enhancements
205
"exclude_methods": ["set_viewport"],
206
"create": {
207
"extract_field": "context",
208
},
209
"get_tree": {
210
"extract_field": "contexts",
211
"deserialize": {
212
"contexts": "Info",
213
},
214
},
215
"capture_screenshot": {
216
"extract_field": "data",
217
"params_override": {
218
"context": "str",
219
"format": "ImageFormat",
220
"clip": "BoxClipRectangle",
221
"origin": "str",
222
},
223
},
224
"print": {
225
"extract_field": "data",
226
},
227
"locate_nodes": {
228
"extract_field": "nodes",
229
"params_override": {
230
"context": "str",
231
"locator": "dict",
232
"serializationOptions": "dict",
233
"startNodes": "list",
234
"maxNodeCount": "int",
235
},
236
},
237
"set_viewport": {
238
"params_override": {
239
"context": "str",
240
"viewport": "dict",
241
"userContexts": "list",
242
"devicePixelRatio": "float",
243
},
244
},
245
"extra_methods": [
246
''' def set_viewport(
247
self,
248
context: str | None = None,
249
viewport: Any = ...,
250
user_contexts: Any | None = None,
251
device_pixel_ratio: Any = ...,
252
):
253
"""Execute browsingContext.setViewport.
254
255
Uses sentinel defaults so explicit None is serialized for viewport/devicePixelRatio,
256
while omitted arguments are not sent.
257
"""
258
params = {}
259
if context is not None:
260
params["context"] = context
261
if user_contexts is not None:
262
params["userContexts"] = user_contexts
263
if viewport is not ...:
264
params["viewport"] = viewport
265
if device_pixel_ratio is not ...:
266
params["devicePixelRatio"] = device_pixel_ratio
267
268
cmd = command_builder("browsingContext.setViewport", params)
269
result = self._conn.execute(cmd)
270
return result''',
271
],
272
# Non-CDDL download event dataclasses (Chromium-specific)
273
"extra_dataclasses": [
274
'''@dataclass
275
class DownloadWillBeginParams:
276
"""DownloadWillBeginParams."""
277
278
suggested_filename: str | None = None''',
279
'''@dataclass
280
class DownloadCanceledParams:
281
"""DownloadCanceledParams."""
282
283
status: Any | None = None''',
284
'''@dataclass
285
class DownloadParams:
286
"""DownloadParams - fields shared by all download end event variants."""
287
288
status: str | None = None
289
context: Any | None = None
290
navigation: Any | None = None
291
timestamp: Any | None = None
292
url: str | None = None
293
filepath: str | None = None''',
294
'''@dataclass
295
class DownloadEndParams:
296
"""DownloadEndParams - params for browsingContext.downloadEnd event."""
297
298
download_params: DownloadParams | None = None
299
300
@classmethod
301
def from_json(cls, params: dict) -> DownloadEndParams:
302
"""Deserialize from BiDi wire-level params dict."""
303
dp = DownloadParams(
304
status=params.get("status"),
305
context=params.get("context"),
306
navigation=params.get("navigation"),
307
timestamp=params.get("timestamp"),
308
url=params.get("url"),
309
filepath=params.get("filepath"),
310
)
311
return cls(download_params=dp)''',
312
],
313
# Download events are now in the CDDL spec, so no extra_events needed
314
},
315
"log": {
316
# Make LogLevel an alias for Level so existing code using LogLevel works
317
"aliases": {"LogLevel": "Level"},
318
# Replace the minimal CDDL-generated versions with richer ones that have from_json
319
"exclude_types": ["JavascriptLogEntry"],
320
"extra_dataclasses": [
321
'''@dataclass
322
class ConsoleLogEntry:
323
"""ConsoleLogEntry - a console log entry from the browser."""
324
325
type_: str | None = None
326
method: str | None = None
327
args: list | None = None
328
level: Any | None = None
329
text: Any | None = None
330
source: Any | None = None
331
timestamp: Any | None = None
332
stack_trace: Any | None = None
333
334
@classmethod
335
def from_json(cls, params: dict) -> ConsoleLogEntry:
336
"""Deserialize from BiDi params dict."""
337
return cls(
338
type_=params.get("type"),
339
method=params.get("method"),
340
args=params.get("args"),
341
level=params.get("level"),
342
text=params.get("text"),
343
source=params.get("source"),
344
timestamp=params.get("timestamp"),
345
stack_trace=params.get("stackTrace"),
346
)''',
347
'''@dataclass
348
class JavascriptLogEntry:
349
"""JavascriptLogEntry - a JavaScript error log entry from the browser."""
350
351
type_: str | None = None
352
level: Any | None = None
353
text: Any | None = None
354
source: Any | None = None
355
timestamp: Any | None = None
356
stacktrace: Any | None = None
357
358
@classmethod
359
def from_json(cls, params: dict) -> JavascriptLogEntry:
360
"""Deserialize from BiDi params dict."""
361
return cls(
362
type_=params.get("type"),
363
level=params.get("level"),
364
text=params.get("text"),
365
source=params.get("source"),
366
timestamp=params.get("timestamp"),
367
stacktrace=params.get("stackTrace"),
368
)''',
369
],
370
# Define Entry union type for log.entryAdded event deserialization
371
"extra_type_aliases": [
372
"Entry = GenericLogEntry | ConsoleLogEntry | JavascriptLogEntry",
373
],
374
"event_type_aliases": {
375
"entry_added": "Entry",
376
},
377
},
378
"emulation": {
379
"exclude_types": ["setNetworkConditionsParameters"],
380
"extra_dataclasses": [
381
'''@dataclass
382
class SetNetworkConditionsParameters:
383
"""SetNetworkConditionsParameters."""
384
385
network_conditions: Any | None = None
386
contexts: list[Any] = field(default_factory=list)
387
user_contexts: list[Any] = field(default_factory=list)
388
389
390
# Backward-compatible alias for existing imports
391
setNetworkConditionsParameters = SetNetworkConditionsParameters''',
392
],
393
"extra_methods": [
394
''' def set_geolocation_override(
395
self,
396
coordinates=None,
397
error=None,
398
contexts: list[Any] | None = None,
399
user_contexts: list[Any] | None = None,
400
):
401
"""Execute emulation.setGeolocationOverride.
402
403
Sets or clears the geolocation override for specified browsing or user contexts.
404
405
Args:
406
coordinates: A GeolocationCoordinates instance (or dict) to override the
407
position, or ``None`` to clear a previously-set override.
408
error: A GeolocationPositionError instance (or dict) to simulate a
409
position-unavailable error. Mutually exclusive with *coordinates*.
410
contexts: List of browsing context IDs to target.
411
user_contexts: List of user context IDs to target.
412
"""
413
params: dict[str, Any] = {}
414
if coordinates is not None:
415
if isinstance(coordinates, dict):
416
coords_dict = coordinates
417
else:
418
coords_dict = {}
419
if coordinates.latitude is not None:
420
coords_dict["latitude"] = coordinates.latitude
421
if coordinates.longitude is not None:
422
coords_dict["longitude"] = coordinates.longitude
423
if coordinates.accuracy is not None:
424
coords_dict["accuracy"] = coordinates.accuracy
425
if coordinates.altitude is not None:
426
coords_dict["altitude"] = coordinates.altitude
427
if coordinates.altitude_accuracy is not None:
428
coords_dict["altitudeAccuracy"] = coordinates.altitude_accuracy
429
if coordinates.heading is not None:
430
coords_dict["heading"] = coordinates.heading
431
if coordinates.speed is not None:
432
coords_dict["speed"] = coordinates.speed
433
params["coordinates"] = coords_dict
434
if error is not None:
435
if isinstance(error, dict):
436
params["error"] = error
437
else:
438
params["error"] = {
439
"type": error.type if error.type is not None else "positionUnavailable"
440
}
441
if contexts is not None:
442
params["contexts"] = contexts
443
if user_contexts is not None:
444
params["userContexts"] = user_contexts
445
cmd = command_builder("emulation.setGeolocationOverride", params)
446
result = self._conn.execute(cmd)
447
return result''',
448
''' def set_timezone_override(
449
self,
450
timezone=None,
451
contexts: list[Any] | None = None,
452
user_contexts: list[Any] | None = None,
453
):
454
"""Execute emulation.setTimezoneOverride.
455
456
Sets or clears the timezone override for specified browsing or user contexts.
457
Pass ``timezone=None`` (or omit it) to clear a previously-set override.
458
459
Args:
460
timezone: IANA timezone string (e.g. ``"America/New_York"``) or ``None``
461
to clear the override.
462
contexts: List of browsing context IDs to target.
463
user_contexts: List of user context IDs to target.
464
"""
465
params: dict[str, Any] = {"timezone": timezone}
466
if contexts is not None:
467
params["contexts"] = contexts
468
if user_contexts is not None:
469
params["userContexts"] = user_contexts
470
cmd = command_builder("emulation.setTimezoneOverride", params)
471
return self._conn.execute(cmd)''',
472
''' def set_scripting_enabled(
473
self,
474
enabled=None,
475
contexts: list[Any] | None = None,
476
user_contexts: list[Any] | None = None,
477
):
478
"""Execute emulation.setScriptingEnabled.
479
480
Enables or disables scripting for specified browsing or user contexts.
481
Pass ``enabled=None`` to restore the default behaviour.
482
483
Args:
484
enabled: ``True`` to enable scripting, ``False`` to disable it, or
485
``None`` to clear the override.
486
contexts: List of browsing context IDs to target.
487
user_contexts: List of user context IDs to target.
488
"""
489
params: dict[str, Any] = {"enabled": enabled}
490
if contexts is not None:
491
params["contexts"] = contexts
492
if user_contexts is not None:
493
params["userContexts"] = user_contexts
494
cmd = command_builder("emulation.setScriptingEnabled", params)
495
return self._conn.execute(cmd)''',
496
''' def set_user_agent_override(
497
self,
498
user_agent=None,
499
contexts: list[Any] | None = None,
500
user_contexts: list[Any] | None = None,
501
):
502
"""Execute emulation.setUserAgentOverride.
503
504
Overrides the User-Agent string for specified browsing or user contexts.
505
Pass ``user_agent=None`` to clear a previously-set override.
506
507
Args:
508
user_agent: Custom User-Agent string, or ``None`` to clear the override.
509
contexts: List of browsing context IDs to target.
510
user_contexts: List of user context IDs to target.
511
"""
512
params: dict[str, Any] = {"userAgent": user_agent}
513
if contexts is not None:
514
params["contexts"] = contexts
515
if user_contexts is not None:
516
params["userContexts"] = user_contexts
517
cmd = command_builder("emulation.setUserAgentOverride", params)
518
return self._conn.execute(cmd)''',
519
''' def set_screen_orientation_override(
520
self,
521
screen_orientation=None,
522
contexts: list[Any] | None = None,
523
user_contexts: list[Any] | None = None,
524
):
525
"""Execute emulation.setScreenOrientationOverride.
526
527
Sets or clears the screen orientation override for specified browsing or
528
user contexts.
529
530
Args:
531
screen_orientation: A :class:`ScreenOrientation` instance (or dict with
532
``natural`` and ``type`` keys) to lock the orientation, or ``None``
533
to clear a previously-set override.
534
contexts: List of browsing context IDs to target.
535
user_contexts: List of user context IDs to target.
536
"""
537
if screen_orientation is None:
538
so_value = None
539
elif isinstance(screen_orientation, dict):
540
so_value = screen_orientation
541
else:
542
natural = screen_orientation.natural
543
orientation_type = screen_orientation.type
544
so_value = {
545
"natural": natural.lower() if isinstance(natural, str) else natural,
546
"type": orientation_type.lower() if isinstance(orientation_type, str) else orientation_type,
547
}
548
params: dict[str, Any] = {"screenOrientation": so_value}
549
if contexts is not None:
550
params["contexts"] = contexts
551
if user_contexts is not None:
552
params["userContexts"] = user_contexts
553
cmd = command_builder("emulation.setScreenOrientationOverride", params)
554
return self._conn.execute(cmd)''',
555
''' def set_network_conditions(
556
self,
557
network_conditions=None,
558
offline: bool | None = None,
559
contexts: list[Any] | None = None,
560
user_contexts: list[Any] | None = None,
561
):
562
"""Execute emulation.setNetworkConditions.
563
564
Sets or clears network condition emulation for specified browsing or user
565
contexts.
566
567
Args:
568
network_conditions: A dict with the raw ``networkConditions`` value
569
(e.g. ``{"type": "offline"}``), or ``None`` to clear the override.
570
Mutually exclusive with *offline*.
571
offline: Convenience bool — ``True`` sets offline conditions,
572
``False`` clears them (sends ``null``). When provided, this takes
573
precedence over *network_conditions*.
574
contexts: List of browsing context IDs to target.
575
user_contexts: List of user context IDs to target.
576
"""
577
if offline is not None:
578
nc_value = {"type": "offline"} if offline else None
579
else:
580
nc_value = network_conditions
581
params: dict[str, Any] = {"networkConditions": nc_value}
582
if contexts is not None:
583
params["contexts"] = contexts
584
if user_contexts is not None:
585
params["userContexts"] = user_contexts
586
cmd = command_builder("emulation.setNetworkConditions", params)
587
return self._conn.execute(cmd)''',
588
''' def set_screen_settings_override(
589
self,
590
width: int | None = None,
591
height: int | None = None,
592
contexts: list[Any] | None = None,
593
user_contexts: list[Any] | None = None,
594
):
595
"""Execute emulation.setScreenSettingsOverride.
596
597
Sets or clears the screen settings override for specified browsing or user
598
contexts.
599
600
Args:
601
width: The screen width in pixels, or ``None`` to clear the override.
602
height: The screen height in pixels, or ``None`` to clear the override.
603
contexts: List of browsing context IDs to target.
604
user_contexts: List of user context IDs to target.
605
"""
606
screen_area = None
607
if width is not None or height is not None:
608
screen_area = {}
609
if width is not None:
610
screen_area["width"] = width
611
if height is not None:
612
screen_area["height"] = height
613
params: dict[str, Any] = {"screenArea": screen_area}
614
if contexts is not None:
615
params["contexts"] = contexts
616
if user_contexts is not None:
617
params["userContexts"] = user_contexts
618
cmd = command_builder("emulation.setScreenSettingsOverride", params)
619
return self._conn.execute(cmd)''',
620
],
621
},
622
"script": {
623
"extra_init_code": [
624
"self._log_handlers = LogHandlerRegistry(self)",
625
"self._dom_mutation_handlers = DomMutationRegistry(self)",
626
],
627
"extra_dataclasses": [
628
# The handler registries and payload classes live in the static
629
# helper module _script_handlers.py (staged via create-bidi-src
630
# extra_srcs) so the implementation is lintable and unit-testable
631
# as real code. The import also re-exports the payload classes
632
# to keep selenium.webdriver.common.bidi.script.<name> importable
633
# (DomMutation in particular predates the helper module).
634
"""from selenium.webdriver.common.bidi._script_handlers import (
635
ConsoleMessage,
636
DomMutation,
637
DomMutationRegistry,
638
LogHandlerRegistry,
639
PinnedScript,
640
ScriptError,
641
ScriptResult,
642
execute_pinned,
643
)""",
644
],
645
"extra_methods": [
646
''' def execute(
647
self, function_declaration: str | PinnedScript, *args, context_id: str | None = None
648
) -> Any:
649
"""Execute a function declaration in the browser context.
650
651
Args:
652
function_declaration: The function as a string, e.g. ``"() => document.title"``,
653
or a ``PinnedScript`` returned by ``pin()``.
654
*args: Optional Python values to pass as arguments to the function.
655
Each value is serialised to a BiDi ``LocalValue`` automatically.
656
Supported types: ``None``, ``bool``, ``int``, ``float``
657
(including ``NaN`` and ``Infinity``), ``str``, ``list``,
658
``dict``, and ``datetime.datetime``.
659
When a ``PinnedScript`` is given, the single argument is the
660
code to execute with the pinned source in scope, e.g.
661
``script.execute(pinned, "return helper();")``.
662
context_id: The browsing context ID to run in. Defaults to the
663
driver\'s current window handle when a driver was provided.
664
665
Returns:
666
The inner RemoteValue result dict, or raises WebDriverException on
667
exception. When a ``PinnedScript`` is given, returns a
668
``ScriptResult`` instead and does not raise: failures are reported
669
through ``ScriptResult.error``.
670
"""
671
if isinstance(function_declaration, PinnedScript):
672
code = args[0] if args else ""
673
return execute_pinned(self, function_declaration, code, context_id=context_id)
674
import math as _math
675
import datetime as _datetime
676
from selenium.common.exceptions import WebDriverException as _WebDriverException
677
678
def _serialize_arg(value):
679
"""Serialise a Python value to a BiDi LocalValue dict."""
680
if value is None:
681
return {"type": "null"}
682
if isinstance(value, bool):
683
return {"type": "boolean", "value": value}
684
if isinstance(value, _datetime.datetime):
685
return {"type": "date", "value": value.isoformat()}
686
if isinstance(value, float):
687
if _math.isnan(value):
688
return {"type": "number", "value": "NaN"}
689
if _math.isinf(value):
690
return {"type": "number", "value": "Infinity" if value > 0 else "-Infinity"}
691
return {"type": "number", "value": value}
692
if isinstance(value, int):
693
_MAX_SAFE_INT = 9007199254740991
694
if abs(value) > _MAX_SAFE_INT:
695
return {"type": "bigint", "value": str(value)}
696
return {"type": "number", "value": value}
697
if isinstance(value, str):
698
return {"type": "string", "value": value}
699
if isinstance(value, list):
700
return {"type": "array", "value": [_serialize_arg(v) for v in value]}
701
if isinstance(value, dict):
702
return {"type": "object", "value": [[str(k), _serialize_arg(v)] for k, v in value.items()]}
703
return value
704
705
if context_id is None and self._driver is not None:
706
try:
707
context_id = self._driver.current_window_handle
708
except Exception:
709
pass
710
target = {"context": context_id} if context_id else {}
711
serialized_args = [_serialize_arg(a) for a in args] if args else None
712
raw = self.call_function(
713
function_declaration=function_declaration,
714
await_promise=True,
715
target=target,
716
arguments=serialized_args,
717
)
718
if isinstance(raw, dict):
719
if raw.get("type") == "exception":
720
exc = raw.get("exceptionDetails", {})
721
msg = exc.get("text", str(exc)) if isinstance(exc, dict) else str(exc)
722
raise _WebDriverException(msg)
723
if raw.get("type") == "success":
724
return raw.get("result")
725
return raw''',
726
''' def _add_preload_script(
727
self,
728
function_declaration,
729
arguments=None,
730
contexts=None,
731
user_contexts=None,
732
sandbox=None,
733
):
734
"""Add a preload script with validation.
735
736
Args:
737
function_declaration: The JS function to run on page load.
738
arguments: Optional list of BiDi arguments.
739
contexts: Optional list of browsing context IDs.
740
user_contexts: Optional list of user context IDs.
741
sandbox: Optional sandbox name.
742
743
Returns:
744
script_id: The ID of the added preload script (str).
745
746
Raises:
747
ValueError: If both contexts and user_contexts are specified.
748
"""
749
if contexts is not None and user_contexts is not None:
750
raise ValueError("Cannot specify both contexts and user_contexts")
751
result = self.add_preload_script(
752
function_declaration=function_declaration,
753
arguments=arguments,
754
contexts=contexts,
755
user_contexts=user_contexts,
756
sandbox=sandbox,
757
)
758
if isinstance(result, dict):
759
return result.get("script")
760
return result''',
761
''' def _remove_preload_script(self, script_id):
762
"""Remove a preload script by ID.
763
764
Args:
765
script_id: The ID of the preload script to remove.
766
"""
767
return self.remove_preload_script(script=script_id)''',
768
''' def pin(self, function_declaration) -> PinnedScript:
769
"""Pin (add) a preload script that runs on every page load.
770
771
Args:
772
function_declaration: The JS function to execute on page load.
773
774
Returns:
775
A ``PinnedScript`` carrying the pinned source. It subclasses
776
``str`` (the script ID), so it can be used anywhere a script ID
777
string is expected, and can be passed to ``execute()`` to run
778
code with the pinned source in scope.
779
"""
780
script_id = self._add_preload_script(function_declaration)
781
return PinnedScript(script_id, source=function_declaration)''',
782
''' def unpin(self, script_id):
783
"""Unpin (remove) a previously pinned preload script.
784
785
Args:
786
script_id: The ID returned by pin().
787
"""
788
return self._remove_preload_script(script_id=script_id)''',
789
''' def _evaluate(
790
self,
791
expression,
792
target,
793
await_promise,
794
result_ownership=None,
795
serialization_options=None,
796
user_activation=None,
797
):
798
"""Evaluate a script expression and return a structured result.
799
800
Args:
801
expression: The JavaScript expression to evaluate.
802
target: A dict like {"context": <id>} or {"realm": <id>}.
803
await_promise: Whether to await a returned promise.
804
result_ownership: Optional result ownership setting.
805
serialization_options: Optional serialization options dict.
806
user_activation: Optional user activation flag.
807
808
Returns:
809
An object with .realm, .result (dict or None), and .exception_details (or None).
810
"""
811
class _EvalResult:
812
def __init__(self2, realm, result, exception_details):
813
self2.realm = realm
814
self2.result = result
815
self2.exception_details = exception_details
816
817
raw = self.evaluate(
818
expression=expression,
819
target=target,
820
await_promise=await_promise,
821
result_ownership=result_ownership,
822
serialization_options=serialization_options,
823
user_activation=user_activation,
824
)
825
if isinstance(raw, dict):
826
realm = raw.get("realm")
827
if raw.get("type") == "exception":
828
exc = raw.get("exceptionDetails")
829
return _EvalResult(realm=realm, result=None, exception_details=exc)
830
return _EvalResult(realm=realm, result=raw.get("result"), exception_details=None)
831
return _EvalResult(realm=None, result=raw, exception_details=None)''',
832
''' def _call_function(
833
self,
834
function_declaration,
835
await_promise,
836
target,
837
arguments=None,
838
result_ownership=None,
839
this=None,
840
user_activation=None,
841
serialization_options=None,
842
):
843
"""Call a function and return a structured result.
844
845
Args:
846
function_declaration: The JS function string.
847
await_promise: Whether to await the return value.
848
target: A dict like {"context": <id>}.
849
arguments: Optional list of BiDi arguments.
850
result_ownership: Optional result ownership.
851
this: Optional \'this\' binding.
852
user_activation: Optional user activation flag.
853
serialization_options: Optional serialization options dict.
854
855
Returns:
856
An object with .result (dict or None) and .exception_details (or None).
857
"""
858
class _CallResult:
859
def __init__(self2, result, exception_details):
860
self2.result = result
861
self2.exception_details = exception_details
862
863
raw = self.call_function(
864
function_declaration=function_declaration,
865
await_promise=await_promise,
866
target=target,
867
arguments=arguments,
868
result_ownership=result_ownership,
869
this=this,
870
user_activation=user_activation,
871
serialization_options=serialization_options,
872
)
873
if isinstance(raw, dict):
874
if raw.get("type") == "exception":
875
exc = raw.get("exceptionDetails")
876
return _CallResult(result=None, exception_details=exc)
877
if raw.get("type") == "success":
878
return _CallResult(result=raw.get("result"), exception_details=None)
879
return _CallResult(result=raw, exception_details=None)''',
880
''' def _get_realms(self, context=None, type=None):
881
"""Get all realms, optionally filtered by context and type.
882
883
Args:
884
context: Optional browsing context ID to filter by.
885
type: Optional realm type string to filter by (e.g. RealmType.WINDOW).
886
887
Returns:
888
List of realm info objects with .realm, .origin, .type, .context attributes.
889
"""
890
class _RealmInfo:
891
def __init__(self2, realm, origin, type_, context):
892
self2.realm = realm
893
self2.origin = origin
894
self2.type = type_
895
self2.context = context
896
897
raw = self.get_realms(context=context, type=type)
898
realms_list = raw.get("realms", []) if isinstance(raw, dict) else []
899
result = []
900
for r in realms_list:
901
if isinstance(r, dict):
902
result.append(_RealmInfo(
903
realm=r.get("realm"),
904
origin=r.get("origin"),
905
type_=r.get("type"),
906
context=r.get("context"),
907
))
908
return result''',
909
''' def _disown(self, handles, target):
910
"""Disown handles in a browsing context.
911
912
Args:
913
handles: List of handle strings to disown.
914
target: A dict like {"context": <id>}.
915
"""
916
return self.disown(handles=handles, target=target)''',
917
''' def add_console_message_handler(self, callback: Callable) -> int:
918
"""Add a handler for console log messages (log.entryAdded type=console).
919
920
The callback receives the generated ``ConsoleLogEntry`` dataclass.
921
For payloads carrying source URL and line/column numbers, see
922
``add_console_handler``.
923
924
Args:
925
callback: Function called with a ConsoleLogEntry on each console message.
926
927
Returns:
928
callback_id for use with remove_console_message_handler.
929
"""
930
return self._log_handlers.add_handler(callback, category="console", legacy=True)''',
931
''' def remove_console_message_handler(self, callback_id: int) -> None:
932
"""Remove a console message handler by callback ID."""
933
self._log_handlers.remove_handler(callback_id)''',
934
''' def add_console_handler(self, callback: Callable) -> int:
935
"""Add a handler for console messages (log.entryAdded type=console).
936
937
Args:
938
callback: Function called with a ``ConsoleMessage`` carrying
939
level, text, source URL, line/column numbers and stack trace.
940
941
Returns:
942
callback_id for use with remove_console_handler.
943
"""
944
return self._log_handlers.add_handler(callback, category="console")''',
945
''' def remove_console_handler(self, callback_id: int) -> None:
946
"""Remove a console handler by callback ID."""
947
self._log_handlers.remove_handler(callback_id)''',
948
''' def clear_console_handlers(self) -> None:
949
"""Remove all console handlers.
950
951
Clears handlers registered through both ``add_console_handler``
952
and ``add_console_message_handler``.
953
"""
954
self._log_handlers.clear_handlers("console")''',
955
''' def add_javascript_error_handler(self, callback: Callable) -> int:
956
"""Add a handler for JavaScript error log messages (log.entryAdded type=javascript).
957
958
The callback receives the generated ``JavascriptLogEntry`` dataclass.
959
For payloads carrying source URL and line/column numbers, see
960
``add_error_handler``.
961
962
Args:
963
callback: Function called with a JavascriptLogEntry on each JS error.
964
965
Returns:
966
callback_id for use with remove_javascript_error_handler.
967
"""
968
return self._log_handlers.add_handler(callback, category="error", legacy=True)''',
969
''' def remove_javascript_error_handler(self, callback_id: int) -> None:
970
"""Remove a JavaScript error handler by callback ID."""
971
self._log_handlers.remove_handler(callback_id)''',
972
''' def add_error_handler(self, callback: Callable) -> int:
973
"""Add a handler for JavaScript errors (log.entryAdded type=javascript).
974
975
Args:
976
callback: Function called with a ``ScriptError`` carrying message,
977
source URL, line/column numbers and stack trace.
978
979
Returns:
980
callback_id for use with remove_error_handler.
981
"""
982
return self._log_handlers.add_handler(callback, category="error")''',
983
''' def remove_error_handler(self, callback_id: int) -> None:
984
"""Remove a JavaScript error handler by callback ID."""
985
self._log_handlers.remove_handler(callback_id)''',
986
''' def clear_error_handlers(self) -> None:
987
"""Remove all JavaScript error handlers.
988
989
Clears handlers registered through both ``add_error_handler``
990
and ``add_javascript_error_handler``.
991
"""
992
self._log_handlers.clear_handlers("error")''',
993
''' def add_dom_mutation_handler(self, callback: Callable, mutation_types=None) -> int:
994
"""Add a handler for DOM mutation events.
995
996
Uses a BiDi preload script and channel to observe DOM mutations on
997
the page. The callback is invoked with a ``DomMutation`` object
998
describing each observed mutation.
999
1000
Args:
1001
callback: Function called with a ``DomMutation`` on each mutation.
1002
mutation_types: The mutation types to observe: any of
1003
``attributes``, ``childList`` and ``characterData``, given as
1004
a string or an iterable of strings. Defaults to
1005
``("attributes",)``.
1006
1007
Returns:
1008
callback_id for use with remove_dom_mutation_handler.
1009
"""
1010
return self._dom_mutation_handlers.add_handler(callback, mutation_types)''',
1011
''' def remove_dom_mutation_handler(self, callback_id: int) -> None:
1012
"""Remove a DOM mutation handler by callback ID."""
1013
self._dom_mutation_handlers.remove_handler(callback_id)''',
1014
''' def clear_dom_mutation_handlers(self) -> None:
1015
"""Remove all DOM mutation handlers."""
1016
self._dom_mutation_handlers.clear_handlers()''',
1017
],
1018
},
1019
"network": {
1020
"exclude_types": ["disownDataParameters"],
1021
# Initialize intercepts tracking list and per-handler intercept map
1022
"extra_init_code": [
1023
"self.intercepts: list[Any] = []",
1024
"self._handler_intercepts: dict[str, Any] = {}",
1025
"self._request_handlers = RequestHandlerRegistry(self)",
1026
"self._response_handlers = ResponseHandlerRegistry(self)",
1027
"self._auth_handlers = AuthHandlerRegistry(self)",
1028
],
1029
# Request class wraps a beforeRequestSent event params and provides actions
1030
"extra_dataclasses": [
1031
'''@dataclass
1032
class DisownDataParameters:
1033
"""DisownDataParameters."""
1034
1035
data_type: Any | None = None
1036
collector: Any | None = None
1037
request: Any | None = None
1038
1039
1040
# Backward-compatible alias for existing imports
1041
disownDataParameters = DisownDataParameters''',
1042
'''class BytesValue:
1043
"""A string or base64-encoded bytes value used in cookie operations.
1044
1045
This corresponds to network.BytesValue in the WebDriver BiDi specification,
1046
wrapping either a plain string or a base64-encoded binary value.
1047
"""
1048
1049
TYPE_STRING = "string"
1050
TYPE_BASE64 = "base64"
1051
1052
def __init__(self, type: Any | None, value: Any | None) -> None:
1053
self.type = type
1054
self.value = value
1055
1056
def to_bidi_dict(self) -> dict:
1057
return {"type": self.type, "value": self.value}''',
1058
# Request/Response and the handler registries live in the static
1059
# helper module _network_handlers.py (staged via create-bidi-src
1060
# extra_srcs) so the implementation is lintable and unit-testable
1061
# as real code. The import also re-exports Request and Response to
1062
# keep them importable from selenium.webdriver.common.bidi.network.
1063
"""from selenium.webdriver.common.bidi._network_handlers import (
1064
LEGACY_REQUEST_HANDLER_EVENTS,
1065
AuthenticationRequest,
1066
AuthHandlerRegistry,
1067
Request,
1068
RequestHandlerRegistry,
1069
Response,
1070
ResponseHandlerRegistry,
1071
looks_like_url_glob,
1072
)""",
1073
],
1074
# Override auth_required to use raw dict so _auth_callback receives all
1075
# fields (including "request") from the BiDi event params. The
1076
# generated AuthRequiredParameters dataclass only contains "response",
1077
# losing the "request" field that holds the request ID required to call
1078
# network.continueWithAuth. extra_events entries appear last in the
1079
# EVENT_CONFIGS dict literal, so this duplicate key overrides the
1080
# CDDL-generated entry.
1081
# Add before_request event (maps to network.beforeRequestSent)
1082
# Override response_started to use raw dict for the same reason: the
1083
# generated ResponseStartedParameters dataclass only contains
1084
# "response", losing "request", "isBlocked" and "intercepts" which the
1085
# response handler registry needs to reconcile blocked responses.
1086
"extra_events": [
1087
{
1088
"event_key": "auth_required",
1089
"bidi_event": "network.authRequired",
1090
"event_class": "dict",
1091
},
1092
{
1093
"event_key": "before_request",
1094
"bidi_event": "network.beforeRequestSent",
1095
"event_class": "dict",
1096
},
1097
{
1098
"event_key": "response_started",
1099
"bidi_event": "network.responseStarted",
1100
"event_class": "dict",
1101
},
1102
],
1103
"extra_methods": [
1104
''' def _add_intercept(self, phases=None, url_patterns=None):
1105
"""Add a low-level network intercept.
1106
1107
Args:
1108
phases: list of intercept phases (default: ["beforeRequestSent"])
1109
url_patterns: optional URL patterns to filter
1110
1111
Returns:
1112
dict with "intercept" key containing the intercept ID
1113
"""
1114
from selenium.webdriver.common.bidi.common import command_builder as _cb
1115
1116
if phases is None:
1117
phases = ["beforeRequestSent"]
1118
params = {"phases": phases}
1119
if url_patterns:
1120
params["urlPatterns"] = url_patterns
1121
result = self._conn.execute(_cb("network.addIntercept", params))
1122
if result:
1123
intercept_id = result.get("intercept")
1124
if intercept_id and intercept_id not in self.intercepts:
1125
self.intercepts.append(intercept_id)
1126
return result''',
1127
''' def _remove_intercept(self, intercept_id):
1128
"""Remove a low-level network intercept."""
1129
from selenium.webdriver.common.bidi.common import command_builder as _cb
1130
1131
self._conn.execute(_cb("network.removeIntercept", {"intercept": intercept_id}))
1132
if intercept_id in self.intercepts:
1133
self.intercepts.remove(intercept_id)''',
1134
''' def _canonical_request_handler_event(self, event):
1135
"""Map public request-handler aliases to supported event keys."""
1136
event_aliases = {
1137
"auth_required": "auth_required",
1138
"before_request": "before_request",
1139
"before_request_sent": "before_request",
1140
}
1141
canonical_event = event_aliases.get(event)
1142
if canonical_event is None:
1143
available_events = ", ".join(sorted(event_aliases))
1144
raise ValueError(
1145
f"Unsupported request handler event '{event}'. Available events: {available_events}"
1146
)
1147
return canonical_event''',
1148
''' def add_request_handler(self, event=None, callback=None, url_patterns=None):
1149
"""Add a handler for network requests.
1150
1151
Two calling styles are supported.
1152
1153
High-level (recommended)::
1154
1155
driver.network.add_request_handler(handler)
1156
driver.network.add_request_handler(["**/api/**"], handler)
1157
1158
The handler receives a :class:`Request` and may observe it, mutate it
1159
via ``set_url``/``set_method``/``set_headers``/``set_cookies``/``set_body``,
1160
call ``fail()``, or call ``provide_response(...)``. After all matching
1161
handlers run, Selenium reconciles the outcome (fail > provide_response >
1162
continue with mutations > continue) and continues the request
1163
automatically — observers never stall the page. URL patterns are glob
1164
strings supporting ``*``, ``**`` and ``?`` (default: match everything).
1165
Returns a string handler ID for ``remove_request_handler(handler_id)``.
1166
1167
Legacy (phase-based)::
1168
1169
driver.network.add_request_handler("before_request", handler, url_patterns=[...])
1170
1171
The callback must call ``request.continue_request()`` itself and
1172
url_patterns are wire-level UrlPattern dicts. Returns an int callback
1173
ID for ``remove_request_handler(event, callback_id)``.
1174
"""
1175
if callable(event) and callback is None:
1176
return self._request_handlers.add_handler(url_patterns, event)
1177
if callable(callback) and event not in LEGACY_REQUEST_HANDLER_EVENTS:
1178
if not isinstance(event, str) or looks_like_url_glob(event):
1179
return self._request_handlers.add_handler(event, callback)
1180
canonical_event = self._canonical_request_handler_event(event)
1181
phase_map = {
1182
"before_request": "beforeRequestSent",
1183
"auth_required": "authRequired",
1184
}
1185
phase = phase_map[canonical_event]
1186
intercept_result = self._add_intercept(phases=[phase], url_patterns=url_patterns)
1187
intercept_id = intercept_result.get("intercept") if intercept_result else None
1188
1189
def _request_callback(params):
1190
raw = (
1191
params
1192
if isinstance(params, dict)
1193
else (params.__dict__ if hasattr(params, "__dict__") else {})
1194
)
1195
request = Request(self._conn, raw)
1196
callback(request)
1197
1198
callback_id = self.add_event_handler(canonical_event, _request_callback)
1199
if intercept_id:
1200
self._handler_intercepts[callback_id] = intercept_id
1201
return callback_id''',
1202
''' def remove_request_handler(self, event, callback_id=None):
1203
"""Remove a network request handler and its associated network intercept.
1204
1205
Args:
1206
event: The handler ID string returned by the high-level
1207
``add_request_handler(callback)`` form, or the event name used
1208
with the legacy phase-based form.
1209
callback_id: The int returned by the legacy form. Omit when
1210
removing a high-level handler by its ID.
1211
"""
1212
if callback_id is None:
1213
self._request_handlers.remove_handler(event)
1214
return
1215
canonical_event = self._canonical_request_handler_event(event)
1216
self.remove_event_handler(canonical_event, callback_id)
1217
intercept_id = self._handler_intercepts.pop(callback_id, None)
1218
if intercept_id:
1219
self._remove_intercept(intercept_id)''',
1220
''' def clear_request_handlers(self):
1221
"""Clear all request handlers and remove all tracked intercepts.
1222
1223
Response handlers registered via ``add_response_handler``,
1224
authentication handlers registered via ``add_authentication_handler``
1225
and extra headers registered via ``add_extra_header`` are preserved;
1226
use ``clear_response_handlers`` / ``clear_authentication_handlers`` /
1227
``clear_extra_headers`` to remove those.
1228
"""
1229
self._request_handlers.clear()
1230
self.clear_event_handlers()
1231
# After clear() the request registry's intercept_ids() only contains
1232
# the extra-headers intercept, which survives like the other
1233
# registries' intercepts.
1234
preserved_intercepts = (
1235
self._request_handlers.intercept_ids()
1236
| self._response_handlers.intercept_ids()
1237
| self._auth_handlers.intercept_ids()
1238
)
1239
for intercept_id in list(self.intercepts):
1240
if intercept_id not in preserved_intercepts:
1241
self._remove_intercept(intercept_id)
1242
# clear_event_handlers dropped every subscription, including the
1243
# other registries'; restore them so their handlers keep working.
1244
self._request_handlers.resubscribe()
1245
self._response_handlers.resubscribe()
1246
self._auth_handlers.resubscribe()''',
1247
''' def add_response_handler(self, url_patterns=None, callback=None):
1248
"""Add a handler for network responses.
1249
1250
Usage::
1251
1252
driver.network.add_response_handler(handler)
1253
driver.network.add_response_handler(["**/api/**"], handler)
1254
1255
The handler receives a :class:`Response` at the ``responseStarted``
1256
phase and may observe it or mutate it via
1257
``set_status``/``set_headers``/``set_cookies``/``set_body``. After all
1258
matching handlers run, Selenium reconciles the outcome — a mutated body
1259
is delivered via ``network.provideResponse``, other mutations via
1260
``network.continueResponse`` — and continues the response
1261
automatically, so observers never stall the page. URL patterns are
1262
glob strings supporting ``*``, ``**`` and ``?`` (default: match
1263
everything).
1264
1265
Returns:
1266
A string handler ID for ``remove_response_handler(handler_id)``.
1267
"""
1268
if callable(url_patterns) and callback is None:
1269
return self._response_handlers.add_handler(None, url_patterns)
1270
if not callable(callback):
1271
raise TypeError("add_response_handler requires a callable handler")
1272
return self._response_handlers.add_handler(url_patterns, callback)''',
1273
''' def remove_response_handler(self, handler_id):
1274
"""Remove a response handler and its intercept by handler ID.
1275
1276
Args:
1277
handler_id: The ID returned by ``add_response_handler``.
1278
"""
1279
self._response_handlers.remove_handler(handler_id)''',
1280
''' def clear_response_handlers(self):
1281
"""Clear all response handlers and their intercepts."""
1282
self._response_handlers.clear()''',
1283
''' def add_authentication_handler(self, url_patterns=None, callback=None):
1284
"""Add a handler for authentication challenges.
1285
1286
Usage::
1287
1288
driver.network.add_authentication_handler(handler)
1289
driver.network.add_authentication_handler(
1290
["https://secure-api.example.com/**"], handler
1291
)
1292
1293
The handler receives an :class:`AuthenticationRequest` at the
1294
``authRequired`` phase and may respond with
1295
``provide_credentials(username, password)`` or ``cancel()``. After all
1296
matching handlers run, Selenium reconciles the outcome (cancel >
1297
provide_credentials > browser default) and continues the challenge
1298
automatically, so observers never stall the page. URL patterns are
1299
glob strings supporting ``*``, ``**`` and ``?`` (default: match
1300
everything).
1301
1302
Do not combine with the credentials-only ``add_auth_handler``: both
1303
would answer the same challenge and the second response fails.
1304
1305
Returns:
1306
A string handler ID for ``remove_authentication_handler(handler_id)``.
1307
"""
1308
if callable(url_patterns) and callback is None:
1309
return self._auth_handlers.add_handler(None, url_patterns)
1310
if not callable(callback):
1311
raise TypeError("add_authentication_handler requires a callable handler")
1312
return self._auth_handlers.add_handler(url_patterns, callback)''',
1313
''' def remove_authentication_handler(self, handler_id):
1314
"""Remove an authentication handler and its intercept by handler ID.
1315
1316
Args:
1317
handler_id: The ID returned by ``add_authentication_handler``.
1318
"""
1319
self._auth_handlers.remove_handler(handler_id)''',
1320
''' def clear_authentication_handlers(self):
1321
"""Clear all authentication handlers and their intercepts."""
1322
self._auth_handlers.clear()''',
1323
''' def add_extra_header(self, name, value):
1324
"""Add a header that is merged into every subsequent request.
1325
1326
Usage::
1327
1328
driver.network.add_extra_header("x-test", "value")
1329
1330
BiDi has no dedicated command for extra headers, so while any extra
1331
header is set every request is paused at the ``beforeRequestSent``
1332
phase and continued with the merged headers — this adds a round trip
1333
per request, so remove the headers when no longer needed. Header
1334
names are case-insensitive; adding a header replaces any existing
1335
request header of the same name.
1336
1337
Args:
1338
name: The header name.
1339
value: The header value.
1340
"""
1341
self._request_handlers.set_extra_header(name, value)''',
1342
''' def remove_extra_header(self, name):
1343
"""Stop adding an extra header to subsequent requests.
1344
1345
Args:
1346
name: The (case-insensitive) header name passed to
1347
``add_extra_header``.
1348
"""
1349
self._request_handlers.remove_extra_header(name)''',
1350
''' def clear_extra_headers(self):
1351
"""Stop adding all extra headers to subsequent requests."""
1352
self._request_handlers.clear_extra_headers()''',
1353
''' def add_auth_handler(self, username, password):
1354
"""Add an auth handler that automatically provides credentials.
1355
1356
For callback-based handling with URL scoping and the ability to cancel
1357
a challenge, prefer ``add_authentication_handler``. Do not combine the
1358
two: both would answer the same challenge and the second response
1359
fails.
1360
1361
Args:
1362
username: The username for basic authentication.
1363
password: The password for basic authentication.
1364
1365
Returns:
1366
callback_id int for later removal via remove_auth_handler.
1367
"""
1368
from selenium.webdriver.common.bidi.common import command_builder as _cb
1369
1370
# Set up network intercept for authRequired phase
1371
intercept_result = self._add_intercept(phases=["authRequired"])
1372
intercept_id = intercept_result.get("intercept") if intercept_result else None
1373
1374
def _auth_callback(params):
1375
raw = (
1376
params
1377
if isinstance(params, dict)
1378
else (params.__dict__ if hasattr(params, "__dict__") else {})
1379
)
1380
request_id = (
1381
raw.get("request", {}).get("request")
1382
if isinstance(raw, dict)
1383
else None
1384
)
1385
if request_id:
1386
self._conn.execute(
1387
_cb(
1388
"network.continueWithAuth",
1389
{
1390
"request": request_id,
1391
"action": "provideCredentials",
1392
"credentials": {
1393
"type": "password",
1394
"username": username,
1395
"password": password,
1396
},
1397
},
1398
)
1399
)
1400
1401
callback_id = self.add_event_handler("auth_required", _auth_callback)
1402
if intercept_id:
1403
self._handler_intercepts[callback_id] = intercept_id
1404
return callback_id''',
1405
''' def remove_auth_handler(self, callback_id):
1406
"""Remove an auth handler by callback ID and its associated network intercept.
1407
1408
Args:
1409
callback_id: The handler ID returned by add_auth_handler.
1410
"""
1411
self.remove_event_handler("auth_required", callback_id)
1412
intercept_id = self._handler_intercepts.pop(callback_id, None)
1413
if intercept_id:
1414
self._remove_intercept(intercept_id)''',
1415
],
1416
},
1417
"storage": {
1418
# Exclude auto-generated dataclasses that need custom to_bidi_dict()
1419
# for JSON-over-WebSocket serialization, or custom constructors.
1420
"exclude_types": [
1421
"CookieFilter",
1422
"PartialCookie",
1423
"BrowsingContextPartitionDescriptor",
1424
"StorageKeyPartitionDescriptor",
1425
],
1426
"extra_dataclasses": [
1427
# Re-export network types used in cookie operations so they can be
1428
# imported from selenium.webdriver.common.bidi.storage alongside
1429
# the storage-specific classes.
1430
'''class BytesValue:
1431
"""A string or base64-encoded bytes value used in cookie operations.
1432
1433
This corresponds to network.BytesValue in the WebDriver BiDi specification,
1434
wrapping either a plain string or a base64-encoded binary value.
1435
"""
1436
1437
TYPE_STRING = "string"
1438
TYPE_BASE64 = "base64"
1439
1440
def __init__(self, type: Any | None, value: Any | None) -> None:
1441
self.type = type
1442
self.value = value
1443
1444
def to_bidi_dict(self) -> dict:
1445
return {"type": self.type, "value": self.value}
1446
1447
def to_dict(self) -> dict:
1448
"""Backward-compatible alias for to_bidi_dict()."""
1449
return self.to_bidi_dict()''',
1450
'''class SameSite:
1451
"""SameSite cookie attribute values."""
1452
1453
STRICT = "strict"
1454
LAX = "lax"
1455
NONE = "none"
1456
DEFAULT = "default"''',
1457
# Helper: cookie object returned inside a GetCookiesResult response
1458
'''@dataclass
1459
class StorageCookie:
1460
"""A cookie object returned by storage.getCookies."""
1461
1462
name: str | None = None
1463
value: Any | None = None
1464
domain: str | None = None
1465
path: str | None = None
1466
size: Any | None = None
1467
http_only: bool | None = None
1468
secure: bool | None = None
1469
same_site: Any | None = None
1470
expiry: Any | None = None
1471
1472
@classmethod
1473
def from_bidi_dict(cls, raw: dict) -> StorageCookie:
1474
"""Deserialize a wire-level cookie dict to a StorageCookie."""
1475
value_raw = raw.get("value")
1476
if isinstance(value_raw, dict):
1477
value: Any = BytesValue(value_raw.get("type"), value_raw.get("value"))
1478
else:
1479
value = value_raw
1480
return cls(
1481
name=raw.get("name"),
1482
value=value,
1483
domain=raw.get("domain"),
1484
path=raw.get("path"),
1485
size=raw.get("size"),
1486
http_only=raw.get("httpOnly"),
1487
secure=raw.get("secure"),
1488
same_site=raw.get("sameSite"),
1489
expiry=raw.get("expiry"),
1490
)''',
1491
# Custom CookieFilter with camelCase serialization
1492
'''@dataclass
1493
class CookieFilter:
1494
"""CookieFilter."""
1495
1496
name: str | None = None
1497
value: Any | None = None
1498
domain: str | None = None
1499
path: str | None = None
1500
size: Any | None = None
1501
http_only: bool | None = None
1502
secure: bool | None = None
1503
same_site: Any | None = None
1504
expiry: Any | None = None
1505
1506
def to_bidi_dict(self) -> dict:
1507
"""Serialize to the BiDi wire-protocol dict."""
1508
result: dict = {}
1509
if self.name is not None:
1510
result["name"] = self.name
1511
if self.value is not None:
1512
result["value"] = self.value.to_bidi_dict() if hasattr(self.value, "to_bidi_dict") else self.value
1513
if self.domain is not None:
1514
result["domain"] = self.domain
1515
if self.path is not None:
1516
result["path"] = self.path
1517
if self.size is not None:
1518
result["size"] = self.size
1519
if self.http_only is not None:
1520
result["httpOnly"] = self.http_only
1521
if self.secure is not None:
1522
result["secure"] = self.secure
1523
if self.same_site is not None:
1524
result["sameSite"] = self.same_site
1525
if self.expiry is not None:
1526
result["expiry"] = self.expiry
1527
return result
1528
1529
def to_dict(self) -> dict:
1530
"""Backward-compatible alias for to_bidi_dict()."""
1531
return self.to_bidi_dict()''',
1532
# Custom PartialCookie with camelCase serialization
1533
'''@dataclass
1534
class PartialCookie:
1535
"""PartialCookie."""
1536
1537
name: str | None = None
1538
value: Any | None = None
1539
domain: str | None = None
1540
path: str | None = None
1541
http_only: bool | None = None
1542
secure: bool | None = None
1543
same_site: Any | None = None
1544
expiry: Any | None = None
1545
1546
def to_bidi_dict(self) -> dict:
1547
"""Serialize to the BiDi wire-protocol dict."""
1548
result: dict = {}
1549
if self.name is not None:
1550
result["name"] = self.name
1551
if self.value is not None:
1552
result["value"] = self.value.to_bidi_dict() if hasattr(self.value, "to_bidi_dict") else self.value
1553
if self.domain is not None:
1554
result["domain"] = self.domain
1555
if self.path is not None:
1556
result["path"] = self.path
1557
if self.http_only is not None:
1558
result["httpOnly"] = self.http_only
1559
if self.secure is not None:
1560
result["secure"] = self.secure
1561
if self.same_site is not None:
1562
result["sameSite"] = self.same_site
1563
if self.expiry is not None:
1564
result["expiry"] = self.expiry
1565
return result
1566
1567
def to_dict(self) -> dict:
1568
"""Backward-compatible alias for to_bidi_dict()."""
1569
return self.to_bidi_dict()''',
1570
# BrowsingContextPartitionDescriptor: first positional arg is *context*
1571
# (the auto-generated dataclass had `type` first, breaking positional
1572
# usage like BrowsingContextPartitionDescriptor(driver.current_window_handle))
1573
'''class BrowsingContextPartitionDescriptor:
1574
"""BrowsingContextPartitionDescriptor.
1575
1576
The first positional argument is *context* (a browsing-context ID / window
1577
handle), mirroring how the class is used throughout the test suite:
1578
``BrowsingContextPartitionDescriptor(driver.current_window_handle)``.
1579
"""
1580
1581
def __init__(self, context: Any = None, type: str = "context") -> None:
1582
self.context = context
1583
self.type = type
1584
1585
def to_bidi_dict(self) -> dict:
1586
return {"type": "context", "context": self.context}
1587
1588
def to_dict(self) -> dict:
1589
"""Backward-compatible alias for to_bidi_dict()."""
1590
return self.to_bidi_dict()''',
1591
# StorageKeyPartitionDescriptor with camelCase serialization
1592
'''@dataclass
1593
class StorageKeyPartitionDescriptor:
1594
"""StorageKeyPartitionDescriptor."""
1595
1596
type: Any | None = "storageKey"
1597
user_context: str | None = None
1598
source_origin: str | None = None
1599
1600
def to_bidi_dict(self) -> dict:
1601
"""Serialize to the BiDi wire-protocol dict."""
1602
result: dict = {"type": "storageKey"}
1603
if self.user_context is not None:
1604
result["userContext"] = self.user_context
1605
if self.source_origin is not None:
1606
result["sourceOrigin"] = self.source_origin
1607
return result
1608
1609
def to_dict(self) -> dict:
1610
"""Backward-compatible alias for to_bidi_dict()."""
1611
return self.to_bidi_dict()''',
1612
],
1613
# Override the generated Storage class methods (Python's last-definition-
1614
# wins semantics means these extra_methods shadow the generated ones).
1615
"extra_methods": [
1616
''' def get_cookies(self, filter=None, partition=None):
1617
"""Execute storage.getCookies and return a GetCookiesResult."""
1618
if filter and hasattr(filter, "to_bidi_dict"):
1619
filter = filter.to_bidi_dict()
1620
if partition and hasattr(partition, "to_bidi_dict"):
1621
partition = partition.to_bidi_dict()
1622
params = {
1623
"filter": filter,
1624
"partition": partition,
1625
}
1626
params = {k: v for k, v in params.items() if v is not None}
1627
cmd = command_builder("storage.getCookies", params)
1628
result = self._conn.execute(cmd)
1629
if result and "cookies" in result:
1630
cookies = [
1631
StorageCookie.from_bidi_dict(c)
1632
for c in result.get("cookies", [])
1633
if isinstance(c, dict)
1634
]
1635
pk_raw = result.get("partitionKey")
1636
pk = (
1637
PartitionKey(
1638
user_context=pk_raw.get("userContext"),
1639
source_origin=pk_raw.get("sourceOrigin"),
1640
)
1641
if isinstance(pk_raw, dict)
1642
else None
1643
)
1644
return GetCookiesResult(cookies=cookies, partition_key=pk)
1645
return GetCookiesResult(cookies=[], partition_key=None)''',
1646
''' def set_cookie(self, cookie=None, partition=None):
1647
"""Execute storage.setCookie."""
1648
if cookie and hasattr(cookie, "to_bidi_dict"):
1649
cookie = cookie.to_bidi_dict()
1650
if partition and hasattr(partition, "to_bidi_dict"):
1651
partition = partition.to_bidi_dict()
1652
params = {
1653
"cookie": cookie,
1654
"partition": partition,
1655
}
1656
params = {k: v for k, v in params.items() if v is not None}
1657
cmd = command_builder("storage.setCookie", params)
1658
result = self._conn.execute(cmd)
1659
if isinstance(result, dict):
1660
pk_raw = result.get("partitionKey")
1661
pk = (
1662
PartitionKey(
1663
user_context=pk_raw.get("userContext"),
1664
source_origin=pk_raw.get("sourceOrigin"),
1665
)
1666
if isinstance(pk_raw, dict)
1667
else None
1668
)
1669
return SetCookieResult(partition_key=pk)
1670
return result''',
1671
''' def delete_cookies(self, filter=None, partition=None):
1672
"""Execute storage.deleteCookies."""
1673
if filter and hasattr(filter, "to_bidi_dict"):
1674
filter = filter.to_bidi_dict()
1675
if partition and hasattr(partition, "to_bidi_dict"):
1676
partition = partition.to_bidi_dict()
1677
params = {
1678
"filter": filter,
1679
"partition": partition,
1680
}
1681
params = {k: v for k, v in params.items() if v is not None}
1682
cmd = command_builder("storage.deleteCookies", params)
1683
result = self._conn.execute(cmd)
1684
if isinstance(result, dict):
1685
pk_raw = result.get("partitionKey")
1686
pk = (
1687
PartitionKey(
1688
user_context=pk_raw.get("userContext"),
1689
source_origin=pk_raw.get("sourceOrigin"),
1690
)
1691
if isinstance(pk_raw, dict)
1692
else None
1693
)
1694
return DeleteCookiesResult(partition_key=pk)
1695
return result''',
1696
],
1697
},
1698
"session": {
1699
# Override UserPromptHandler to add to_bidi_dict() for JSON serialization
1700
"exclude_types": ["UserPromptHandler"],
1701
"extra_dataclasses": [
1702
'''@dataclass
1703
class UserPromptHandler:
1704
"""UserPromptHandler."""
1705
1706
alert: Any | None = None
1707
before_unload: Any | None = None
1708
confirm: Any | None = None
1709
default: Any | None = None
1710
file: Any | None = None
1711
prompt: Any | None = None
1712
1713
def to_bidi_dict(self) -> dict:
1714
"""Convert to BiDi protocol dict with camelCase keys."""
1715
result = {}
1716
if self.alert is not None:
1717
result["alert"] = self.alert
1718
if self.before_unload is not None:
1719
result["beforeUnload"] = self.before_unload
1720
if self.confirm is not None:
1721
result["confirm"] = self.confirm
1722
if self.default is not None:
1723
result["default"] = self.default
1724
if self.file is not None:
1725
result["file"] = self.file
1726
if self.prompt is not None:
1727
result["prompt"] = self.prompt
1728
return result
1729
1730
def to_dict(self) -> dict:
1731
"""Backward-compatible alias for to_bidi_dict()."""
1732
return self.to_bidi_dict()''',
1733
],
1734
},
1735
"webExtension": {
1736
# Suppress the raw generated stubs; hand-written versions follow below
1737
"exclude_methods": ["install", "uninstall"],
1738
"extra_methods": [
1739
''' def install(
1740
self,
1741
path: str | None = None,
1742
archive_path: str | None = None,
1743
base64_value: str | None = None,
1744
):
1745
"""Install a web extension.
1746
1747
Exactly one of the three keyword arguments must be provided.
1748
1749
Args:
1750
path: Directory path to an unpacked extension (also accepted for
1751
signed ``.xpi`` / ``.crx`` archive files on Firefox).
1752
archive_path: File-system path to a packed extension archive.
1753
base64_value: Base64-encoded extension archive string.
1754
1755
Returns:
1756
The raw result dict from the BiDi ``webExtension.install`` command
1757
(contains at least an ``"extension"`` key with the extension ID).
1758
1759
Raises:
1760
ValueError: If more than one, or none, of the arguments is provided.
1761
"""
1762
provided = [
1763
k for k, v in {
1764
"path": path, "archive_path": archive_path, "base64_value": base64_value,
1765
}.items() if v is not None
1766
]
1767
if len(provided) != 1:
1768
raise ValueError(
1769
f"Exactly one of path, archive_path, or base64_value must be provided; got: {provided}"
1770
)
1771
if path is not None:
1772
extension_data = {"type": "path", "path": path}
1773
elif archive_path is not None:
1774
extension_data = {"type": "archivePath", "path": archive_path}
1775
else:
1776
assert base64_value is not None
1777
extension_data = {"type": "base64", "value": base64_value}
1778
params = {"extensionData": extension_data}
1779
cmd = command_builder("webExtension.install", params)
1780
try:
1781
return self._conn.execute(cmd)
1782
except Exception as e:
1783
if "Method not available" in str(e):
1784
raise RuntimeError(
1785
"webExtension.install failed with 'Method not available'. "
1786
"This likely means that web extension support is disabled. "
1787
"Enable unsafe extension debugging and/or set options.enable_webextensions "
1788
"in your WebDriver configuration."
1789
) from e
1790
raise''',
1791
''' def uninstall(self, extension: str | dict):
1792
"""Uninstall a web extension.
1793
1794
Args:
1795
extension: Either the extension ID string returned by ``install``,
1796
or the full result dict returned by ``install`` (the
1797
``"extension"`` value is extracted automatically).
1798
1799
Raises:
1800
ValueError: If extension is not provided or is None.
1801
"""
1802
if isinstance(extension, dict):
1803
extension_id: Any = extension.get("extension")
1804
else:
1805
extension_id = extension
1806
1807
if extension_id is None:
1808
raise ValueError("extension parameter is required")
1809
1810
params = {"extension": extension_id}
1811
cmd = command_builder("webExtension.uninstall", params)
1812
return self._conn.execute(cmd)''',
1813
],
1814
},
1815
"input": {
1816
# FileDialogInfo needs from_json for event deserialization
1817
"exclude_types": ["FileDialogInfo", "PointerMoveAction", "PointerDownAction"],
1818
"extra_dataclasses": [
1819
'''@dataclass
1820
class FileDialogInfo:
1821
"""FileDialogInfo - parameters for the input.fileDialogOpened event."""
1822
1823
context: Any | None = None
1824
element: Any | None = None
1825
multiple: bool | None = None
1826
1827
@classmethod
1828
def from_json(cls, params: dict) -> FileDialogInfo:
1829
"""Deserialize event params into FileDialogInfo."""
1830
return cls(
1831
context=params.get("context"),
1832
element=params.get("element"),
1833
multiple=params.get("multiple"),
1834
)''',
1835
'''@dataclass
1836
class PointerMoveAction:
1837
"""PointerMoveAction."""
1838
1839
type: str = field(default="pointerMove", init=False)
1840
x: Any | None = None
1841
y: Any | None = None
1842
duration: Any | None = None
1843
origin: Any | None = None
1844
properties: Any | None = None''',
1845
'''@dataclass
1846
class PointerDownAction:
1847
"""PointerDownAction."""
1848
1849
type: str = field(default="pointerDown", init=False)
1850
button: Any | None = None
1851
properties: Any | None = None''',
1852
],
1853
"extra_methods": [
1854
''' def add_file_dialog_handler(self, callback) -> int:
1855
"""Subscribe to the input.fileDialogOpened event.
1856
1857
Args:
1858
callback: Callable invoked with a FileDialogInfo when a file dialog opens.
1859
1860
Returns:
1861
A handler ID that can be passed to remove_file_dialog_handler.
1862
"""
1863
return self._event_manager.add_event_handler("file_dialog_opened", callback)
1864
1865
def remove_file_dialog_handler(self, handler_id: int) -> None:
1866
"""Unsubscribe a previously registered file dialog event handler.
1867
1868
Args:
1869
handler_id: The handler ID returned by add_file_dialog_handler.
1870
"""
1871
return self._event_manager.remove_event_handler("file_dialog_opened", handler_id)''',
1872
],
1873
},
1874
"permissions": {
1875
"module_docstring": (
1876
"WebDriver BiDi permissions module.\n\n"
1877
"Provides control over browser permission grants during automated tests,\n"
1878
"as specified by the W3C Permissions specification.\n\n"
1879
"Typical usage::\n\n"
1880
" driver.permissions.set_permission('geolocation', 'granted', origin)\n"
1881
),
1882
"class_docstrings": {
1883
"PermissionState": (
1884
"Permission state constants.\n\n"
1885
"GRANTED: The permission is granted — the browser will not prompt the user.\n"
1886
"DENIED: The permission is denied — the browser will block the request.\n"
1887
"PROMPT: The browser will show a permission prompt (default browser behaviour)."
1888
),
1889
"Permissions": (
1890
"BiDi interface for controlling browser permissions.\n\nAccess via ``driver.permissions``."
1891
),
1892
},
1893
"extra_dataclasses": [
1894
'''class PermissionDescriptor:
1895
"""Descriptor identifying a permission by name.
1896
1897
Args:
1898
name: The permission name (e.g. 'geolocation', 'microphone', 'camera').
1899
"""
1900
1901
def __init__(self, name: str) -> None:
1902
self.name = name
1903
1904
def __repr__(self) -> str:
1905
return f"PermissionDescriptor(name={self.name!r})"''',
1906
],
1907
"extra_methods": [
1908
''' def set_permission(
1909
self,
1910
descriptor: "PermissionDescriptor | str",
1911
state: "PermissionState | str",
1912
origin: str | None = None,
1913
user_context: str | None = None,
1914
*,
1915
embedded_origin: str | None = None,
1916
) -> None:
1917
"""Set a browser permission.
1918
1919
Args:
1920
descriptor: The permission descriptor or permission name as a string.
1921
state: The desired permission state (granted, denied, or prompt).
1922
origin: The origin to scope the permission to.
1923
user_context: Optional user context ID to scope the permission.
1924
embedded_origin: Keyword-only. Embedded origin for cross-origin
1925
iframes; scopes the permission to that iframe's origin.
1926
1927
Raises:
1928
ValueError: If *state* is not a valid permission state.
1929
"""
1930
state_value = state.value if isinstance(state, PermissionState) else state
1931
valid_states = {"granted", "denied", "prompt"}
1932
if state_value not in valid_states:
1933
raise ValueError(
1934
f"Invalid permission state: {state_value!r}. "
1935
f"Must be one of {sorted(valid_states)}"
1936
)
1937
1938
descriptor_dict = {"name": descriptor} if isinstance(descriptor, str) else {"name": descriptor.name}
1939
1940
params: dict = {
1941
"descriptor": descriptor_dict,
1942
"state": state_value,
1943
}
1944
if origin is not None:
1945
params["origin"] = origin
1946
if embedded_origin is not None:
1947
params["embeddedOrigin"] = embedded_origin
1948
if user_context is not None:
1949
params["userContext"] = user_context
1950
1951
cmd = command_builder("permissions.setPermission", params)
1952
self._conn.execute(cmd)''',
1953
],
1954
},
1955
"bluetooth": {
1956
"module_docstring": (
1957
"WebDriver BiDi bluetooth module.\n\n"
1958
"Provides a simulation API for Web Bluetooth, allowing tests to fake\n"
1959
"Bluetooth adapters, nearby peripherals, GATT services, characteristics,\n"
1960
"and descriptors without physical hardware.\n"
1961
),
1962
"class_docstrings": {
1963
"Bluetooth": (
1964
"BiDi interface for simulating Web Bluetooth hardware.\n\n"
1965
"Simulate adapters, peripherals, GATT services, characteristics,\n"
1966
"and descriptors without physical hardware."
1967
),
1968
"RequestDeviceInfo": (
1969
"Identifies a simulated Bluetooth device returned in a device-request prompt.\n\n"
1970
"Attributes:\n"
1971
" id: The internal device identifier.\n"
1972
" name: The human-readable device name shown in the prompt."
1973
),
1974
"SimulateAdapterParameters": (
1975
"Parameters for simulating a Bluetooth adapter state.\n\n"
1976
"Attributes:\n"
1977
" context: The browsing context ID to target.\n"
1978
" le_supported: Whether the adapter supports Bluetooth Low Energy.\n"
1979
" state: Adapter power state (e.g. 'powered-on', 'powered-off', 'absent')."
1980
),
1981
"SimulatePreconnectedPeripheralParameters": (
1982
"Parameters for adding a pre-connected simulated peripheral.\n\n"
1983
"Attributes:\n"
1984
" context: The browsing context ID to target.\n"
1985
" address: The Bluetooth device address (e.g. '09:09:09:09:09:09').\n"
1986
" name: The device name advertised to the page.\n"
1987
" manufacturer_data: List of manufacturer-specific data records.\n"
1988
" known_service_uuids: UUIDs of GATT services the device exposes."
1989
),
1990
"SimulateAdvertisementParameters": (
1991
"Parameters for injecting a simulated advertisement packet.\n\n"
1992
"Attributes:\n"
1993
" context: The browsing context ID to target.\n"
1994
" scan_entry: The advertisement scan record to inject."
1995
),
1996
"SimulateGattConnectionResponseParameters": (
1997
"Parameters for simulating a GATT connection response.\n\n"
1998
"Attributes:\n"
1999
" context: The browsing context ID to target.\n"
2000
" address: The address of the peripheral.\n"
2001
" code: The ATT error code (0 = success)."
2002
),
2003
"SimulateCharacteristicParameters": (
2004
"Parameters for adding a simulated GATT characteristic to a service.\n\n"
2005
"Attributes:\n"
2006
" context: The browsing context ID to target.\n"
2007
" address: The peripheral address.\n"
2008
" service: The service UUID the characteristic belongs to.\n"
2009
" characteristic: UUID of the characteristic.\n"
2010
" properties: Supported operations (read, write, notify, etc.)."
2011
),
2012
},
2013
"command_docstrings": {
2014
"handle_request_device_prompt": (
2015
"Dismiss or accept a Bluetooth device-chooser prompt.\n\n"
2016
"Args:\n"
2017
" context: The browsing context containing the prompt.\n"
2018
" prompt: The prompt ID returned in the prompt-opened event."
2019
),
2020
"simulate_adapter": (
2021
"Simulate a Bluetooth adapter in the given browsing context.\n\n"
2022
"Args:\n"
2023
" context: The browsing context ID to target.\n"
2024
" le_supported: Whether Low Energy is supported.\n"
2025
" state: Adapter state ('powered-on', 'powered-off', 'absent')."
2026
),
2027
"disable_simulation": (
2028
"Disable all Bluetooth simulation in the given context, restoring real behaviour.\n\n"
2029
"Args:\n"
2030
" context: The browsing context ID to stop simulating."
2031
),
2032
"simulate_preconnected_peripheral": (
2033
"Register a simulated peripheral as already connected to the adapter.\n\n"
2034
"Args:\n"
2035
" context: The browsing context ID to target.\n"
2036
" address: The Bluetooth device address.\n"
2037
" name: The device name.\n"
2038
" manufacturer_data: Manufacturer-specific advertisement data.\n"
2039
" known_service_uuids: List of GATT service UUIDs the device exposes."
2040
),
2041
"simulate_advertisement": (
2042
"Inject a simulated Bluetooth advertisement packet.\n\n"
2043
"Args:\n"
2044
" context: The browsing context ID to target.\n"
2045
" scan_entry: The advertisement scan record to inject."
2046
),
2047
"simulate_gatt_connection_response": (
2048
"Respond to a pending GATT connection attempt from the page.\n\n"
2049
"Args:\n"
2050
" context: The browsing context ID.\n"
2051
" address: The peripheral address.\n"
2052
" code: ATT error code (0 = success; non-zero signals failure)."
2053
),
2054
"simulate_gatt_disconnection": (
2055
"Simulate a GATT disconnection for the given peripheral.\n\n"
2056
"Args:\n"
2057
" context: The browsing context ID.\n"
2058
" address: The address of the peripheral to disconnect."
2059
),
2060
"simulate_service": (
2061
"Add a simulated GATT service to a peripheral.\n\n"
2062
"Args:\n"
2063
" context: The browsing context ID.\n"
2064
" address: The peripheral address.\n"
2065
" uuid: The service UUID."
2066
),
2067
"simulate_characteristic": (
2068
"Add a simulated GATT characteristic to a service.\n\n"
2069
"Args:\n"
2070
" context: The browsing context ID.\n"
2071
" address: The peripheral address.\n"
2072
" service: The service UUID.\n"
2073
" characteristic: The characteristic UUID.\n"
2074
" properties: Supported operations bitmap."
2075
),
2076
"simulate_characteristic_response": (
2077
"Respond to a pending read or write on a simulated characteristic.\n\n"
2078
"Args:\n"
2079
" context: The browsing context ID.\n"
2080
" address: The peripheral address.\n"
2081
" service: The service UUID.\n"
2082
" characteristic: The characteristic UUID.\n"
2083
" code: ATT error code (0 = success).\n"
2084
" body: The characteristic value bytes (for reads)."
2085
),
2086
"simulate_descriptor": (
2087
"Add a simulated GATT descriptor to a characteristic.\n\n"
2088
"Args:\n"
2089
" context: The browsing context ID.\n"
2090
" address: The peripheral address.\n"
2091
" service: The service UUID.\n"
2092
" characteristic: The characteristic UUID.\n"
2093
" descriptor: The descriptor UUID."
2094
),
2095
"simulate_descriptor_response": (
2096
"Respond to a pending read or write on a simulated descriptor.\n\n"
2097
"Args:\n"
2098
" context: The browsing context ID.\n"
2099
" address: The peripheral address.\n"
2100
" service: The service UUID.\n"
2101
" characteristic: The characteristic UUID.\n"
2102
" descriptor: The descriptor UUID.\n"
2103
" code: ATT error code (0 = success).\n"
2104
" body: The descriptor value bytes (for reads)."
2105
),
2106
},
2107
},
2108
"speculation": {
2109
"module_docstring": (
2110
"WebDriver BiDi speculation module.\n\n"
2111
"Provides events for observing the status of Speculation Rules prefetch\n"
2112
"requests initiated by the browser (e.g. via <script type='speculationrules'>).\n"
2113
),
2114
"class_docstrings": {
2115
"Speculation": ("BiDi interface for observing Speculation Rules prefetch activity."),
2116
"PreloadingStatus": (
2117
"Status values for a speculation-rules prefetch operation.\n\n"
2118
"PENDING: The prefetch has been queued but not yet attempted.\n"
2119
"READY: The prefetch succeeded and the resource is cached.\n"
2120
"SUCCESS: The prefetched navigation was used successfully.\n"
2121
"FAILURE: The prefetch failed or was cancelled."
2122
),
2123
"PrefetchStatusUpdatedParameters": (
2124
"Event payload emitted when a prefetch status changes.\n\n"
2125
"Attributes:\n"
2126
" context: The browsing context ID that owns the speculation rule.\n"
2127
" url: The URL being prefetched.\n"
2128
" status: The new prefetch status (see PreloadingStatus)."
2129
),
2130
},
2131
},
2132
"userAgentClientHints": {
2133
"module_docstring": (
2134
"WebDriver BiDi userAgentClientHints module.\n\n"
2135
"Provides an API for overriding the User-Agent Client Hints reported\n"
2136
"by the browser, enabling tests to simulate different devices, platforms,\n"
2137
"and browser brands without changing the actual browser binary.\n"
2138
),
2139
"class_docstrings": {
2140
"UserAgentClientHints": ("BiDi interface for overriding User-Agent Client Hints."),
2141
"ClientHintsMetadata": (
2142
"Full set of User-Agent Client Hint values to override.\n\n"
2143
"Attributes:\n"
2144
" brands: List of browser brand/version pairs (e.g. [BrandVersion('Chrome', '120')]).\n"
2145
" full_version_list: Brands with full version strings.\n"
2146
" platform: Operating system name (e.g. 'Windows', 'macOS').\n"
2147
" platform_version: OS version string.\n"
2148
" architecture: CPU architecture (e.g. 'x86', 'arm').\n"
2149
" model: Device model (primarily for mobile).\n"
2150
" mobile: True if the UA should appear to be a mobile device.\n"
2151
" bitness: Pointer-size bitness string ('32' or '64').\n"
2152
" wow64: True if running a 32-bit process on 64-bit Windows.\n"
2153
" form_factors: Device form factors (e.g. 'Desktop', 'Phone')."
2154
),
2155
"BrandVersion": (
2156
"A single browser brand entry used in Client Hints brand lists.\n\n"
2157
"Attributes:\n"
2158
" brand: The browser/engine brand name (e.g. 'Google Chrome').\n"
2159
" version: The major or full version string (e.g. '120')."
2160
),
2161
},
2162
},
2163
}
2164
2165
2166
# ============================================================================
2167
# Pre-processing Functions
2168
# ============================================================================
2169
2170
2171
def check_serialize_method(obj: Any) -> Any:
2172
"""Check if object has to_bidi_dict() method and use it for serialization."""
2173
if obj and hasattr(obj, "to_bidi_dict"):
2174
return obj.to_bidi_dict()
2175
return obj
2176
2177
2178
# ============================================================================
2179
# Validation Functions
2180
# ============================================================================
2181
2182
2183
def validate_download_behavior(
2184
allowed: bool | None,
2185
destination_folder: str | None,
2186
user_contexts: Any | None = None,
2187
) -> None:
2188
"""Validate download behavior parameters.
2189
2190
Args:
2191
allowed: Whether downloads are allowed
2192
destination_folder: Destination folder for downloads
2193
user_contexts: Optional list of user contexts (ignored for validation)
2194
2195
Raises:
2196
ValueError: If parameters are invalid
2197
"""
2198
if allowed is True and not destination_folder:
2199
raise ValueError("destination_folder is required when allowed=True")
2200
if allowed is False and destination_folder:
2201
raise ValueError("destination_folder should not be provided when allowed=False")
2202
2203
2204
# ============================================================================
2205
# Transformation Functions
2206
# ============================================================================
2207
2208
2209
def transform_download_params(
2210
allowed: bool | None,
2211
destination_folder: str | None,
2212
) -> dict[str, Any]:
2213
"""Transform download parameters into download_behavior object.
2214
2215
Args:
2216
allowed: Whether downloads are allowed
2217
destination_folder: Destination folder for downloads
2218
2219
Returns:
2220
Dictionary representing the download_behavior object, or None if allowed is None
2221
"""
2222
if allowed is True:
2223
return {
2224
"type": "allowed",
2225
# Convert pathlib.Path (or any path-like) to str so the BiDi
2226
# protocol always receives a plain JSON string.
2227
"destinationFolder": (str(destination_folder) if destination_folder is not None else None),
2228
}
2229
elif allowed is False:
2230
return {"type": "denied"}
2231
else: # None — reset to browser default (sent as JSON null)
2232
return None
2233
2234
2235
# ============================================================================
2236
# Dataclass Method Templates
2237
# ============================================================================
2238
2239
DATACLASS_METHOD_TEMPLATES: dict[str, dict[str, str]] = {
2240
"ClientWindowInfo": {
2241
"get_client_window": "return self.client_window",
2242
"get_state": "return self.state",
2243
"get_width": "return self.width",
2244
"get_height": "return self.height",
2245
"is_active": "return self.active",
2246
"get_x": "return self.x",
2247
"get_y": "return self.y",
2248
},
2249
"BrowsingContext": {
2250
"add_event_handler": "_add_event_handler_impl",
2251
"remove_event_handler": "_remove_event_handler_impl",
2252
},
2253
}
2254
2255
DATACLASS_METHOD_DOCSTRINGS: dict[str, dict[str, str]] = {
2256
"ClientWindowInfo": {
2257
"get_client_window": "Get the client window ID.",
2258
"get_state": "Get the client window state.",
2259
"get_width": "Get the client window width.",
2260
"get_height": "Get the client window height.",
2261
"is_active": "Check if the client window is active.",
2262
"get_x": "Get the client window X position.",
2263
"get_y": "Get the client window Y position.",
2264
},
2265
"BrowsingContext": {
2266
"add_event_handler": "Add an event handler for browsing context events.",
2267
"remove_event_handler": "Remove an event handler by callback ID.",
2268
},
2269
}
2270
2271
# ============================================================================
2272
# Event Handler Support for BrowsingContext
2273
# ============================================================================
2274
2275
2276
def _add_event_handler(
2277
self,
2278
event_name: str,
2279
callback: callable,
2280
contexts: list[str] | None = None,
2281
) -> str:
2282
"""Add an event handler for a browsing context event.
2283
2284
Supported events:
2285
- 'context_created'
2286
- 'context_destroyed'
2287
- 'navigation_started'
2288
- 'navigation_committed'
2289
- 'navigation_failed'
2290
- 'dom_content_loaded'
2291
- 'load'
2292
- 'fragment_navigated'
2293
- 'user_prompt_opened'
2294
- 'user_prompt_closed'
2295
- 'download_will_begin'
2296
- 'download_end'
2297
- 'history_updated'
2298
2299
Args:
2300
self: The module instance this handler is bound to.
2301
event_name: The name of the event to subscribe to
2302
callback: Callback function to invoke when event occurs
2303
contexts: Optional list of context IDs to limit event subscription
2304
2305
Returns:
2306
A callback ID that can be used to unsubscribe the handler
2307
"""
2308
if not hasattr(self, "_event_handlers"):
2309
self._event_handlers = {}
2310
self._event_callback_id_counter = 0
2311
2312
# Generate unique callback ID
2313
self._event_callback_id_counter += 1
2314
callback_id = f"callback_{self._event_callback_id_counter}"
2315
2316
# Store the handler
2317
self._event_handlers[callback_id] = {
2318
"event": event_name,
2319
"callback": callback,
2320
"contexts": contexts,
2321
}
2322
2323
# Subscribe via the driver's event listening mechanism
2324
if hasattr(self._driver, "_subscribe_event"):
2325
self._driver._subscribe_event(event_name, callback, contexts)
2326
2327
return callback_id
2328
2329
2330
def _remove_event_handler(
2331
self,
2332
callback_id: str,
2333
) -> None:
2334
"""Remove an event handler by its callback ID.
2335
2336
Args:
2337
self: The module instance this handler is bound to.
2338
callback_id: The callback ID returned from add_event_handler
2339
"""
2340
if not hasattr(self, "_event_handlers"):
2341
return
2342
2343
if callback_id in self._event_handlers:
2344
handler_info = self._event_handlers[callback_id]
2345
2346
# Unsubscribe from the driver
2347
if hasattr(self._driver, "_unsubscribe_event"):
2348
self._driver._unsubscribe_event(
2349
handler_info["event"],
2350
handler_info["callback"],
2351
handler_info["contexts"],
2352
)
2353
2354
del self._event_handlers[callback_id]
2355
2356