Path: blob/trunk/py/private/bidi_enhancements_manifest.py
10192 views
# Licensed to the Software Freedom Conservancy (SFC) under one1# or more contributor license agreements. See the NOTICE file2# distributed with this work for additional information3# regarding copyright ownership. The SFC licenses this file4# to you under the Apache License, Version 2.0 (the5# "License"); you may not use this file except in compliance6# with the License. You may obtain a copy of the License at7#8# http://www.apache.org/licenses/LICENSE-2.09#10# Unless required by applicable law or agreed to in writing,11# software distributed under the License is distributed on an12# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13# KIND, either express or implied. See the License for the14# specific language governing permissions and limitations15# under the License.161718"""19Enhancement manifest for BiDi code generation.2021This file defines custom enhancements applied to generated BiDi modules,22including custom dataclass methods, parameter validation/transformation,23response deserialization, and field extraction.2425All code must be compatible with Python 3.10+.26"""2728from __future__ import annotations2930from typing import Any3132# ============================================================================33# Format Guide34# ============================================================================35# Each module in ENHANCEMENTS specifies enhancement rules for methods:36#37# 'module_name': {38# 'method_name': {39# 'dataclass_methods': { # For dataclass enhancements40# 'ClassName': ['method1', 'method2', ...]41# },42# 'preprocess': { # Pre-processing on parameters43# 'param_name': 'check_serialize_method'44# },45# 'deserialize': { # Deserialize response to typed objects46# 'response_field': 'TypeName',47# },48# 'extract_field': str, # Extract nested field from response49# 'extract_property': str, # Extract property from extracted items50# 'validate': str, # Validation function name51# 'transform': str, # Transformation function name52# }53# }54# ============================================================================5556ENHANCEMENTS: dict[str, dict[str, Any]] = {57"browser": {58# Dataclass custom methods59"__dataclass_methods__": {60"ClientWindowInfo": [61"get_client_window",62"get_state",63"get_width",64"get_height",65"is_active",66"get_x",67"get_y",68],69},70# Method enhancements71"create_user_context": {72"preprocess": {73"proxy": "check_serialize_method",74"unhandled_prompt_behavior": "check_serialize_method",75},76"extract_field": "userContext",77},78"get_client_windows": {79"deserialize": {80"clientWindows": "ClientWindowInfo",81},82},83"get_user_contexts": {84"extract_field": "userContexts",85"extract_property": "userContext",86},87"set_download_behavior": {88"params_override": {89"allowed": "bool",90"destination_folder": "str",91"userContexts": "[*browser.UserContext]",92},93"validate": "validate_download_behavior",94"transform": {95"allowed": "allowed",96"destination_folder": "destination_folder",97"func": "transform_download_params",98"result_param": "download_behavior",99},100},101# Replace the auto-generated ClientWindowNamedState so we can add the102# convenience NORMAL constant. In the BiDi spec "normal" is the state103# represented by ClientWindowRectState, but exposing it here keeps the104# Python API consistent with the old ClientWindowState enum.105"exclude_types": ["ClientWindowNamedState", "SetClientWindowStateParameters"],106"extra_dataclasses": [107'''class ClientWindowNamedState:108"""Named states for a browser client window."""109110FULLSCREEN = "fullscreen"111MAXIMIZED = "maximized"112MINIMIZED = "minimized"113NORMAL = "normal"''',114'''@dataclass115class SetClientWindowStateParameters:116"""SetClientWindowStateParameters.117118The ``state`` field is required and must be either a named-state string119(e.g. ``ClientWindowNamedState.MAXIMIZED``) or a120:class:`ClientWindowRectState` instance. ``client_window`` is the ID of121the window to affect.122"""123124client_window: Any | None = None125state: Any | None = None''',126],127# Override the generator-produced set_download_behavior so that128# downloadBehavior is never stripped by the generic None filter.129# The BiDi spec marks it as required (can be null, but must be present).130"extra_methods": [131''' def set_download_behavior(132self,133allowed: bool | None = None,134destination_folder: str | None = None,135user_contexts: list[Any] | None = None,136):137"""Set the download behavior for the browser.138139Args:140allowed: ``True`` to allow downloads, ``False`` to deny, or ``None``141to reset to browser default (sends ``null`` to the protocol).142destination_folder: Destination folder for downloads. Required when143``allowed=True``. Accepts a string or :class:`pathlib.Path`.144user_contexts: Optional list of user context IDs.145146Raises:147ValueError: If *allowed* is ``True`` and *destination_folder* is148omitted, or ``False`` and *destination_folder* is provided.149"""150validate_download_behavior(151allowed=allowed,152destination_folder=destination_folder,153user_contexts=user_contexts,154)155download_behavior = transform_download_params(allowed, destination_folder)156# downloadBehavior is a REQUIRED field in the BiDi spec (can be null but157# must be present). Do NOT use a generic None-filter on it.158params: dict = {"downloadBehavior": download_behavior}159if user_contexts is not None:160params["userContexts"] = user_contexts161cmd = command_builder("browser.setDownloadBehavior", params)162return self._conn.execute(cmd)''',163''' def set_client_window_state(164self,165client_window: Any | None = None,166state: Any | None = None,167):168"""Set the client window state.169170Args:171client_window: The client window ID to apply the state to.172state: The window state to set. Can be one of:173- A string: "fullscreen", "maximized", "minimized", "normal"174- A ClientWindowRectState object with width, height, x, y175- A dict representing the state176177Raises:178ValueError: If client_window is not provided or state is invalid.179"""180if client_window is None:181raise ValueError("client_window is required")182if state is None:183raise ValueError("state is required")184185# Serialize ClientWindowRectState if needed186state_param = state187if hasattr(state, '__dataclass_fields__'):188# It's a dataclass, convert to dict189state_param = {190k: v for k, v in state.__dict__.items()191if v is not None192}193194params = {195"clientWindow": client_window,196"state": state_param,197}198cmd = command_builder("browser.setClientWindowState", params)199return self._conn.execute(cmd)''',200],201},202"browsingContext": {203# Method enhancements204"exclude_methods": ["set_viewport"],205"create": {206"extract_field": "context",207},208"get_tree": {209"extract_field": "contexts",210"deserialize": {211"contexts": "Info",212},213},214"capture_screenshot": {215"extract_field": "data",216"params_override": {217"context": "str",218"format": "ImageFormat",219"clip": "BoxClipRectangle",220"origin": "str",221},222},223"print": {224"extract_field": "data",225},226"locate_nodes": {227"extract_field": "nodes",228"params_override": {229"context": "str",230"locator": "dict",231"serializationOptions": "dict",232"startNodes": "list",233"maxNodeCount": "int",234},235},236"set_viewport": {237"params_override": {238"context": "str",239"viewport": "dict",240"userContexts": "list",241"devicePixelRatio": "float",242},243},244"extra_methods": [245''' def set_viewport(246self,247context: str | None = None,248viewport: Any = ...,249user_contexts: Any | None = None,250device_pixel_ratio: Any = ...,251):252"""Execute browsingContext.setViewport.253254Uses sentinel defaults so explicit None is serialized for viewport/devicePixelRatio,255while omitted arguments are not sent.256"""257params = {}258if context is not None:259params["context"] = context260if user_contexts is not None:261params["userContexts"] = user_contexts262if viewport is not ...:263params["viewport"] = viewport264if device_pixel_ratio is not ...:265params["devicePixelRatio"] = device_pixel_ratio266267cmd = command_builder("browsingContext.setViewport", params)268result = self._conn.execute(cmd)269return result''',270],271# Non-CDDL download event dataclasses (Chromium-specific)272"extra_dataclasses": [273'''@dataclass274class DownloadWillBeginParams:275"""DownloadWillBeginParams."""276277suggested_filename: str | None = None''',278'''@dataclass279class DownloadCanceledParams:280"""DownloadCanceledParams."""281282status: Any | None = None''',283'''@dataclass284class DownloadParams:285"""DownloadParams - fields shared by all download end event variants."""286287status: str | None = None288context: Any | None = None289navigation: Any | None = None290timestamp: Any | None = None291url: str | None = None292filepath: str | None = None''',293'''@dataclass294class DownloadEndParams:295"""DownloadEndParams - params for browsingContext.downloadEnd event."""296297download_params: DownloadParams | None = None298299@classmethod300def from_json(cls, params: dict) -> DownloadEndParams:301"""Deserialize from BiDi wire-level params dict."""302dp = DownloadParams(303status=params.get("status"),304context=params.get("context"),305navigation=params.get("navigation"),306timestamp=params.get("timestamp"),307url=params.get("url"),308filepath=params.get("filepath"),309)310return cls(download_params=dp)''',311],312# Download events are now in the CDDL spec, so no extra_events needed313},314"log": {315# Make LogLevel an alias for Level so existing code using LogLevel works316"aliases": {"LogLevel": "Level"},317# Replace the minimal CDDL-generated versions with richer ones that have from_json318"exclude_types": ["JavascriptLogEntry"],319"extra_dataclasses": [320'''@dataclass321class ConsoleLogEntry:322"""ConsoleLogEntry - a console log entry from the browser."""323324type_: str | None = None325method: str | None = None326args: list | None = None327level: Any | None = None328text: Any | None = None329source: Any | None = None330timestamp: Any | None = None331stack_trace: Any | None = None332333@classmethod334def from_json(cls, params: dict) -> ConsoleLogEntry:335"""Deserialize from BiDi params dict."""336return cls(337type_=params.get("type"),338method=params.get("method"),339args=params.get("args"),340level=params.get("level"),341text=params.get("text"),342source=params.get("source"),343timestamp=params.get("timestamp"),344stack_trace=params.get("stackTrace"),345)''',346'''@dataclass347class JavascriptLogEntry:348"""JavascriptLogEntry - a JavaScript error log entry from the browser."""349350type_: str | None = None351level: Any | None = None352text: Any | None = None353source: Any | None = None354timestamp: Any | None = None355stacktrace: Any | None = None356357@classmethod358def from_json(cls, params: dict) -> JavascriptLogEntry:359"""Deserialize from BiDi params dict."""360return cls(361type_=params.get("type"),362level=params.get("level"),363text=params.get("text"),364source=params.get("source"),365timestamp=params.get("timestamp"),366stacktrace=params.get("stackTrace"),367)''',368],369# Define Entry union type for log.entryAdded event deserialization370"extra_type_aliases": [371"Entry = GenericLogEntry | ConsoleLogEntry | JavascriptLogEntry",372],373"event_type_aliases": {374"entry_added": "Entry",375},376},377"emulation": {378"exclude_types": ["setNetworkConditionsParameters"],379"extra_dataclasses": [380'''@dataclass381class SetNetworkConditionsParameters:382"""SetNetworkConditionsParameters."""383384network_conditions: Any | None = None385contexts: list[Any] = field(default_factory=list)386user_contexts: list[Any] = field(default_factory=list)387388389# Backward-compatible alias for existing imports390setNetworkConditionsParameters = SetNetworkConditionsParameters''',391],392"extra_methods": [393''' def set_geolocation_override(394self,395coordinates=None,396error=None,397contexts: list[Any] | None = None,398user_contexts: list[Any] | None = None,399):400"""Execute emulation.setGeolocationOverride.401402Sets or clears the geolocation override for specified browsing or user contexts.403404Args:405coordinates: A GeolocationCoordinates instance (or dict) to override the406position, or ``None`` to clear a previously-set override.407error: A GeolocationPositionError instance (or dict) to simulate a408position-unavailable error. Mutually exclusive with *coordinates*.409contexts: List of browsing context IDs to target.410user_contexts: List of user context IDs to target.411"""412params: dict[str, Any] = {}413if coordinates is not None:414if isinstance(coordinates, dict):415coords_dict = coordinates416else:417coords_dict = {}418if coordinates.latitude is not None:419coords_dict["latitude"] = coordinates.latitude420if coordinates.longitude is not None:421coords_dict["longitude"] = coordinates.longitude422if coordinates.accuracy is not None:423coords_dict["accuracy"] = coordinates.accuracy424if coordinates.altitude is not None:425coords_dict["altitude"] = coordinates.altitude426if coordinates.altitude_accuracy is not None:427coords_dict["altitudeAccuracy"] = coordinates.altitude_accuracy428if coordinates.heading is not None:429coords_dict["heading"] = coordinates.heading430if coordinates.speed is not None:431coords_dict["speed"] = coordinates.speed432params["coordinates"] = coords_dict433if error is not None:434if isinstance(error, dict):435params["error"] = error436else:437params["error"] = {438"type": error.type if error.type is not None else "positionUnavailable"439}440if contexts is not None:441params["contexts"] = contexts442if user_contexts is not None:443params["userContexts"] = user_contexts444cmd = command_builder("emulation.setGeolocationOverride", params)445result = self._conn.execute(cmd)446return result''',447''' def set_timezone_override(448self,449timezone=None,450contexts: list[Any] | None = None,451user_contexts: list[Any] | None = None,452):453"""Execute emulation.setTimezoneOverride.454455Sets or clears the timezone override for specified browsing or user contexts.456Pass ``timezone=None`` (or omit it) to clear a previously-set override.457458Args:459timezone: IANA timezone string (e.g. ``"America/New_York"``) or ``None``460to clear the override.461contexts: List of browsing context IDs to target.462user_contexts: List of user context IDs to target.463"""464params: dict[str, Any] = {"timezone": timezone}465if contexts is not None:466params["contexts"] = contexts467if user_contexts is not None:468params["userContexts"] = user_contexts469cmd = command_builder("emulation.setTimezoneOverride", params)470return self._conn.execute(cmd)''',471''' def set_scripting_enabled(472self,473enabled=None,474contexts: list[Any] | None = None,475user_contexts: list[Any] | None = None,476):477"""Execute emulation.setScriptingEnabled.478479Enables or disables scripting for specified browsing or user contexts.480Pass ``enabled=None`` to restore the default behaviour.481482Args:483enabled: ``True`` to enable scripting, ``False`` to disable it, or484``None`` to clear the override.485contexts: List of browsing context IDs to target.486user_contexts: List of user context IDs to target.487"""488params: dict[str, Any] = {"enabled": enabled}489if contexts is not None:490params["contexts"] = contexts491if user_contexts is not None:492params["userContexts"] = user_contexts493cmd = command_builder("emulation.setScriptingEnabled", params)494return self._conn.execute(cmd)''',495''' def set_user_agent_override(496self,497user_agent=None,498contexts: list[Any] | None = None,499user_contexts: list[Any] | None = None,500):501"""Execute emulation.setUserAgentOverride.502503Overrides the User-Agent string for specified browsing or user contexts.504Pass ``user_agent=None`` to clear a previously-set override.505506Args:507user_agent: Custom User-Agent string, or ``None`` to clear the override.508contexts: List of browsing context IDs to target.509user_contexts: List of user context IDs to target.510"""511params: dict[str, Any] = {"userAgent": user_agent}512if contexts is not None:513params["contexts"] = contexts514if user_contexts is not None:515params["userContexts"] = user_contexts516cmd = command_builder("emulation.setUserAgentOverride", params)517return self._conn.execute(cmd)''',518''' def set_screen_orientation_override(519self,520screen_orientation=None,521contexts: list[Any] | None = None,522user_contexts: list[Any] | None = None,523):524"""Execute emulation.setScreenOrientationOverride.525526Sets or clears the screen orientation override for specified browsing or527user contexts.528529Args:530screen_orientation: A :class:`ScreenOrientation` instance (or dict with531``natural`` and ``type`` keys) to lock the orientation, or ``None``532to clear a previously-set override.533contexts: List of browsing context IDs to target.534user_contexts: List of user context IDs to target.535"""536if screen_orientation is None:537so_value = None538elif isinstance(screen_orientation, dict):539so_value = screen_orientation540else:541natural = screen_orientation.natural542orientation_type = screen_orientation.type543so_value = {544"natural": natural.lower() if isinstance(natural, str) else natural,545"type": orientation_type.lower() if isinstance(orientation_type, str) else orientation_type,546}547params: dict[str, Any] = {"screenOrientation": so_value}548if contexts is not None:549params["contexts"] = contexts550if user_contexts is not None:551params["userContexts"] = user_contexts552cmd = command_builder("emulation.setScreenOrientationOverride", params)553return self._conn.execute(cmd)''',554''' def set_network_conditions(555self,556network_conditions=None,557offline: bool | None = None,558contexts: list[Any] | None = None,559user_contexts: list[Any] | None = None,560):561"""Execute emulation.setNetworkConditions.562563Sets or clears network condition emulation for specified browsing or user564contexts.565566Args:567network_conditions: A dict with the raw ``networkConditions`` value568(e.g. ``{"type": "offline"}``), or ``None`` to clear the override.569Mutually exclusive with *offline*.570offline: Convenience bool — ``True`` sets offline conditions,571``False`` clears them (sends ``null``). When provided, this takes572precedence over *network_conditions*.573contexts: List of browsing context IDs to target.574user_contexts: List of user context IDs to target.575"""576if offline is not None:577nc_value = {"type": "offline"} if offline else None578else:579nc_value = network_conditions580params: dict[str, Any] = {"networkConditions": nc_value}581if contexts is not None:582params["contexts"] = contexts583if user_contexts is not None:584params["userContexts"] = user_contexts585cmd = command_builder("emulation.setNetworkConditions", params)586return self._conn.execute(cmd)''',587''' def set_screen_settings_override(588self,589width: int | None = None,590height: int | None = None,591contexts: list[Any] | None = None,592user_contexts: list[Any] | None = None,593):594"""Execute emulation.setScreenSettingsOverride.595596Sets or clears the screen settings override for specified browsing or user597contexts.598599Args:600width: The screen width in pixels, or ``None`` to clear the override.601height: The screen height in pixels, or ``None`` to clear the override.602contexts: List of browsing context IDs to target.603user_contexts: List of user context IDs to target.604"""605screen_area = None606if width is not None or height is not None:607screen_area = {}608if width is not None:609screen_area["width"] = width610if height is not None:611screen_area["height"] = height612params: dict[str, Any] = {"screenArea": screen_area}613if contexts is not None:614params["contexts"] = contexts615if user_contexts is not None:616params["userContexts"] = user_contexts617cmd = command_builder("emulation.setScreenSettingsOverride", params)618return self._conn.execute(cmd)''',619],620},621"script": {622"extra_dataclasses": [623'''@dataclass624class DomMutation:625"""Represents a DOM attribute mutation event from add_dom_mutation_handler.626627Attributes:628element_id: The ``data-__webdriver_id`` attribute value set on the629mutated element by the MutationObserver. Use this to locate the630element from the main thread if needed.631attribute_name: The name of the changed attribute.632current_value: The attribute value after the mutation (may be ``None``633if the attribute was removed).634old_value: The attribute value before the mutation.635"""636637element_id: str | None = None638attribute_name: str | None = None639current_value: str | None = None640old_value: str | None = None641''',642],643"extra_methods": [644''' def execute(self, function_declaration: str, *args, context_id: str | None = None) -> Any:645"""Execute a function declaration in the browser context.646647Args:648function_declaration: The function as a string, e.g. ``"() => document.title"``.649*args: Optional Python values to pass as arguments to the function.650Each value is serialised to a BiDi ``LocalValue`` automatically.651Supported types: ``None``, ``bool``, ``int``, ``float``652(including ``NaN`` and ``Infinity``), ``str``, ``list``,653``dict``, and ``datetime.datetime``.654context_id: The browsing context ID to run in. Defaults to the655driver\'s current window handle when a driver was provided.656657Returns:658The inner RemoteValue result dict, or raises WebDriverException on exception.659"""660import math as _math661import datetime as _datetime662from selenium.common.exceptions import WebDriverException as _WebDriverException663664def _serialize_arg(value):665"""Serialise a Python value to a BiDi LocalValue dict."""666if value is None:667return {"type": "null"}668if isinstance(value, bool):669return {"type": "boolean", "value": value}670if isinstance(value, _datetime.datetime):671return {"type": "date", "value": value.isoformat()}672if isinstance(value, float):673if _math.isnan(value):674return {"type": "number", "value": "NaN"}675if _math.isinf(value):676return {"type": "number", "value": "Infinity" if value > 0 else "-Infinity"}677return {"type": "number", "value": value}678if isinstance(value, int):679_MAX_SAFE_INT = 9007199254740991680if abs(value) > _MAX_SAFE_INT:681return {"type": "bigint", "value": str(value)}682return {"type": "number", "value": value}683if isinstance(value, str):684return {"type": "string", "value": value}685if isinstance(value, list):686return {"type": "array", "value": [_serialize_arg(v) for v in value]}687if isinstance(value, dict):688return {"type": "object", "value": [[str(k), _serialize_arg(v)] for k, v in value.items()]}689return value690691if context_id is None and self._driver is not None:692try:693context_id = self._driver.current_window_handle694except Exception:695pass696target = {"context": context_id} if context_id else {}697serialized_args = [_serialize_arg(a) for a in args] if args else None698raw = self.call_function(699function_declaration=function_declaration,700await_promise=True,701target=target,702arguments=serialized_args,703)704if isinstance(raw, dict):705if raw.get("type") == "exception":706exc = raw.get("exceptionDetails", {})707msg = exc.get("text", str(exc)) if isinstance(exc, dict) else str(exc)708raise _WebDriverException(msg)709if raw.get("type") == "success":710return raw.get("result")711return raw''',712''' def _add_preload_script(713self,714function_declaration,715arguments=None,716contexts=None,717user_contexts=None,718sandbox=None,719):720"""Add a preload script with validation.721722Args:723function_declaration: The JS function to run on page load.724arguments: Optional list of BiDi arguments.725contexts: Optional list of browsing context IDs.726user_contexts: Optional list of user context IDs.727sandbox: Optional sandbox name.728729Returns:730script_id: The ID of the added preload script (str).731732Raises:733ValueError: If both contexts and user_contexts are specified.734"""735if contexts is not None and user_contexts is not None:736raise ValueError("Cannot specify both contexts and user_contexts")737result = self.add_preload_script(738function_declaration=function_declaration,739arguments=arguments,740contexts=contexts,741user_contexts=user_contexts,742sandbox=sandbox,743)744if isinstance(result, dict):745return result.get("script")746return result''',747''' def _remove_preload_script(self, script_id):748"""Remove a preload script by ID.749750Args:751script_id: The ID of the preload script to remove.752"""753return self.remove_preload_script(script=script_id)''',754''' def pin(self, function_declaration):755"""Pin (add) a preload script that runs on every page load.756757Args:758function_declaration: The JS function to execute on page load.759760Returns:761script_id: The ID of the pinned script (str).762"""763return self._add_preload_script(function_declaration)''',764''' def unpin(self, script_id):765"""Unpin (remove) a previously pinned preload script.766767Args:768script_id: The ID returned by pin().769"""770return self._remove_preload_script(script_id=script_id)''',771''' def _evaluate(772self,773expression,774target,775await_promise,776result_ownership=None,777serialization_options=None,778user_activation=None,779):780"""Evaluate a script expression and return a structured result.781782Args:783expression: The JavaScript expression to evaluate.784target: A dict like {"context": <id>} or {"realm": <id>}.785await_promise: Whether to await a returned promise.786result_ownership: Optional result ownership setting.787serialization_options: Optional serialization options dict.788user_activation: Optional user activation flag.789790Returns:791An object with .realm, .result (dict or None), and .exception_details (or None).792"""793class _EvalResult:794def __init__(self2, realm, result, exception_details):795self2.realm = realm796self2.result = result797self2.exception_details = exception_details798799raw = self.evaluate(800expression=expression,801target=target,802await_promise=await_promise,803result_ownership=result_ownership,804serialization_options=serialization_options,805user_activation=user_activation,806)807if isinstance(raw, dict):808realm = raw.get("realm")809if raw.get("type") == "exception":810exc = raw.get("exceptionDetails")811return _EvalResult(realm=realm, result=None, exception_details=exc)812return _EvalResult(realm=realm, result=raw.get("result"), exception_details=None)813return _EvalResult(realm=None, result=raw, exception_details=None)''',814''' def _call_function(815self,816function_declaration,817await_promise,818target,819arguments=None,820result_ownership=None,821this=None,822user_activation=None,823serialization_options=None,824):825"""Call a function and return a structured result.826827Args:828function_declaration: The JS function string.829await_promise: Whether to await the return value.830target: A dict like {"context": <id>}.831arguments: Optional list of BiDi arguments.832result_ownership: Optional result ownership.833this: Optional \'this\' binding.834user_activation: Optional user activation flag.835serialization_options: Optional serialization options dict.836837Returns:838An object with .result (dict or None) and .exception_details (or None).839"""840class _CallResult:841def __init__(self2, result, exception_details):842self2.result = result843self2.exception_details = exception_details844845raw = self.call_function(846function_declaration=function_declaration,847await_promise=await_promise,848target=target,849arguments=arguments,850result_ownership=result_ownership,851this=this,852user_activation=user_activation,853serialization_options=serialization_options,854)855if isinstance(raw, dict):856if raw.get("type") == "exception":857exc = raw.get("exceptionDetails")858return _CallResult(result=None, exception_details=exc)859if raw.get("type") == "success":860return _CallResult(result=raw.get("result"), exception_details=None)861return _CallResult(result=raw, exception_details=None)''',862''' def _get_realms(self, context=None, type=None):863"""Get all realms, optionally filtered by context and type.864865Args:866context: Optional browsing context ID to filter by.867type: Optional realm type string to filter by (e.g. RealmType.WINDOW).868869Returns:870List of realm info objects with .realm, .origin, .type, .context attributes.871"""872class _RealmInfo:873def __init__(self2, realm, origin, type_, context):874self2.realm = realm875self2.origin = origin876self2.type = type_877self2.context = context878879raw = self.get_realms(context=context, type=type)880realms_list = raw.get("realms", []) if isinstance(raw, dict) else []881result = []882for r in realms_list:883if isinstance(r, dict):884result.append(_RealmInfo(885realm=r.get("realm"),886origin=r.get("origin"),887type_=r.get("type"),888context=r.get("context"),889))890return result''',891''' def _disown(self, handles, target):892"""Disown handles in a browsing context.893894Args:895handles: List of handle strings to disown.896target: A dict like {"context": <id>}.897"""898return self.disown(handles=handles, target=target)''',899''' def _subscribe_log_entry(self, callback, entry_type_filter=None):900"""Subscribe to log.entryAdded BiDi events with optional type filtering."""901import threading as _threading902from selenium.webdriver.common.bidi.session import Session as _Session903from selenium.webdriver.common.bidi import log as _log_mod904905bidi_event = "log.entryAdded"906907if not hasattr(self, "_log_subscriptions"):908self._log_subscriptions = {}909self._log_lock = _threading.Lock()910911def _deserialize(params):912t = params.get("type") if isinstance(params, dict) else None913if t == "console":914cls = getattr(_log_mod, "ConsoleLogEntry", None)915if cls is not None and hasattr(cls, "from_json"):916try:917return cls.from_json(params)918except Exception:919pass920elif t == "javascript":921cls = getattr(_log_mod, "JavascriptLogEntry", None)922if cls is not None and hasattr(cls, "from_json"):923try:924return cls.from_json(params)925except Exception:926pass927return params928929def _wrapped(raw):930entry = _deserialize(raw)931if entry_type_filter is None:932callback(entry)933else:934t = getattr(entry, "type_", None) or (935entry.get("type") if isinstance(entry, dict) else None936)937if t == entry_type_filter:938callback(entry)939940class _BidiRef:941event_class = bidi_event942943def from_json(self2, p):944return p945946_wrapper = _BidiRef()947callback_id = self._conn.add_callback(_wrapper, _wrapped)948with self._log_lock:949if bidi_event not in self._log_subscriptions:950session = _Session(self._conn)951result = session.subscribe([bidi_event])952sub_id = (953result.get("subscription") if isinstance(result, dict) else None954)955self._log_subscriptions[bidi_event] = {956"callbacks": [],957"subscription_id": sub_id,958}959self._log_subscriptions[bidi_event]["callbacks"].append(callback_id)960return callback_id''',961''' def _unsubscribe_log_entry(self, callback_id):962"""Unsubscribe a log entry callback by ID."""963from selenium.webdriver.common.bidi.session import Session as _Session964965bidi_event = "log.entryAdded"966if not hasattr(self, "_log_subscriptions"):967return968969class _BidiRef:970event_class = bidi_event971972def from_json(self2, p):973return p974975_wrapper = _BidiRef()976self._conn.remove_callback(_wrapper, callback_id)977with self._log_lock:978entry = self._log_subscriptions.get(bidi_event)979if entry and callback_id in entry["callbacks"]:980entry["callbacks"].remove(callback_id)981if entry is not None and not entry["callbacks"]:982session = _Session(self._conn)983sub_id = entry.get("subscription_id")984if sub_id:985session.unsubscribe(subscriptions=[sub_id])986else:987session.unsubscribe(events=[bidi_event])988del self._log_subscriptions[bidi_event]''',989''' def add_console_message_handler(self, callback: Callable) -> int:990"""Add a handler for console log messages (log.entryAdded type=console).991992Args:993callback: Function called with a ConsoleLogEntry on each console message.994995Returns:996callback_id for use with remove_console_message_handler.997"""998return self._subscribe_log_entry(callback, entry_type_filter="console")''',999''' def remove_console_message_handler(self, callback_id: int) -> None:1000"""Remove a console message handler by callback ID."""1001self._unsubscribe_log_entry(callback_id)''',1002''' def add_javascript_error_handler(self, callback: Callable) -> int:1003"""Add a handler for JavaScript error log messages (log.entryAdded type=javascript).10041005Args:1006callback: Function called with a JavascriptLogEntry on each JS error.10071008Returns:1009callback_id for use with remove_javascript_error_handler.1010"""1011return self._subscribe_log_entry(callback, entry_type_filter="javascript")''',1012''' def remove_javascript_error_handler(self, callback_id: int) -> None:1013"""Remove a JavaScript error handler by callback ID."""1014self._unsubscribe_log_entry(callback_id)''',1015''' def _subscribe_mutation_handler(self, callback):1016"""Subscribe to DOM mutation events using a BiDi preload script and script.message channel.10171018Loads bidi-mutation-listener.js as a preload script with a channel argument,1019then subscribes to script.message events from that channel to detect1020DOM attribute mutations.1021"""1022import json as _json1023import pkgutil as _pkgutil1024import threading as _threading1025from selenium.webdriver.common.bidi.session import Session as _Session10261027bidi_event = "script.message"10281029if not hasattr(self, "_mutation_subscriptions"):1030self._mutation_subscriptions = {}1031self._mutation_lock = _threading.Lock()10321033# Load bidi-mutation-listener.js only once (cache it on the instance)1034if not hasattr(self, "_bidi_mutation_listener_js"):1035_pkg = "selenium.webdriver.common"1036_js_bytes = _pkgutil.get_data(_pkg, "bidi-mutation-listener.js")1037if _js_bytes is None:1038raise ValueError("Failed to load bidi-mutation-listener.js")1039self._bidi_mutation_listener_js = _js_bytes.decode("utf8").strip()10401041# Use a stable, namespaced channel to avoid collisions with user scripts.1042if not hasattr(self, "_mutation_channel_name"):1043import uuid as _uuid1044self._mutation_channel_name = f"selenium.domMutation.{_uuid.uuid4().hex}"1045_channel_name = self._mutation_channel_name1046_channel_arg = {"type": "channel", "value": {"channel": _channel_name}}10471048def _on_message(message):1049# Filter to only our channel1050channel = message.get("channel") if isinstance(message, dict) else None1051if channel != _channel_name:1052return1053data = message.get("data", {}) if isinstance(message, dict) else {}1054value = data.get("value") if isinstance(data, dict) else None1055if value is None:1056return1057try:1058payload = _json.loads(value)1059except (ValueError, TypeError):1060return1061target_id = payload.get("target")1062if not target_id and target_id != 0:1063return1064from selenium.webdriver.common.bidi.script import DomMutation as _DomMutation1065event = _DomMutation(1066element_id=str(target_id),1067attribute_name=payload.get("name"),1068current_value=payload.get("value"),1069old_value=payload.get("oldValue"),1070)1071callback(event)10721073class _BidiRef:1074event_class = bidi_event10751076def from_json(self2, p):1077return p10781079with self._mutation_lock:1080# Register the preload script only once per Script instance to avoid1081# accumulating duplicate MutationObservers across handler registrations.1082if not hasattr(self, "_mutation_preload_script_id"):1083self._mutation_preload_script_id = self._add_preload_script(1084self._bidi_mutation_listener_js, arguments=[_channel_arg]1085)1086# Also invoke immediately on the current page since the preload1087# script only fires on future document creations.1088if self._driver is not None:1089_context = None1090try:1091_context = self._driver.current_window_handle1092except Exception:1093pass1094if _context is not None:1095self.call_function(1096function_declaration=self._bidi_mutation_listener_js,1097target={"context": _context},1098await_promise=False,1099arguments=[_channel_arg],1100)1101if bidi_event not in self._mutation_subscriptions:1102session = _Session(self._conn)1103result = session.subscribe([bidi_event])1104sub_id = (1105result.get("subscription") if isinstance(result, dict) else None1106)1107self._mutation_subscriptions[bidi_event] = {1108"callbacks": [],1109"subscription_id": sub_id,1110}1111# Register the callback AFTER setup to avoid leaking it if setup fails.1112_wrapper = _BidiRef()1113callback_id = self._conn.add_callback(_wrapper, _on_message)1114self._mutation_subscriptions[bidi_event]["callbacks"].append(callback_id)1115return callback_id''',1116''' def _unsubscribe_mutation_handler(self, callback_id):1117"""Unsubscribe a DOM mutation handler by callback ID."""1118from selenium.webdriver.common.bidi.session import Session as _Session11191120bidi_event = "script.message"1121if not hasattr(self, "_mutation_subscriptions"):1122return11231124class _BidiRef:1125event_class = bidi_event11261127def from_json(self2, p):1128return p11291130_wrapper = _BidiRef()1131self._conn.remove_callback(_wrapper, callback_id)1132with self._mutation_lock:1133entry = self._mutation_subscriptions.get(bidi_event)1134if entry and callback_id in entry["callbacks"]:1135entry["callbacks"].remove(callback_id)1136if entry is not None and not entry["callbacks"]:1137session = _Session(self._conn)1138sub_id = entry.get("subscription_id")1139if sub_id:1140session.unsubscribe(subscriptions=[sub_id])1141else:1142session.unsubscribe(events=[bidi_event])1143del self._mutation_subscriptions[bidi_event]1144if hasattr(self, "_mutation_preload_script_id"):1145preload_script_id = self._mutation_preload_script_id1146try:1147self._remove_preload_script(preload_script_id)1148finally:1149del self._mutation_preload_script_id''',1150''' def add_dom_mutation_handler(self, callback: Callable) -> int:1151"""Add a handler for DOM attribute mutation events.11521153Uses a BiDi preload script and channel to observe DOM attribute mutations1154on the page. When an attribute changes, the callback is invoked with a1155``DomMutation`` object describing the element and attribute change.11561157Args:1158callback: Function called with a ``DomMutation`` on each attribute mutation.11591160Returns:1161callback_id for use with remove_dom_mutation_handler.1162"""1163return self._subscribe_mutation_handler(callback)''',1164''' def remove_dom_mutation_handler(self, callback_id: int) -> None:1165"""Remove a DOM mutation handler by callback ID."""1166self._unsubscribe_mutation_handler(callback_id)''',1167],1168},1169"network": {1170"exclude_types": ["disownDataParameters"],1171# Initialize intercepts tracking list and per-handler intercept map1172"extra_init_code": [1173"self.intercepts: list[Any] = []",1174"self._handler_intercepts: dict[str, Any] = {}",1175],1176# Request class wraps a beforeRequestSent event params and provides actions1177"extra_dataclasses": [1178'''@dataclass1179class DisownDataParameters:1180"""DisownDataParameters."""11811182data_type: Any | None = None1183collector: Any | None = None1184request: Any | None = None118511861187# Backward-compatible alias for existing imports1188disownDataParameters = DisownDataParameters''',1189'''class BytesValue:1190"""A string or base64-encoded bytes value used in cookie operations.11911192This corresponds to network.BytesValue in the WebDriver BiDi specification,1193wrapping either a plain string or a base64-encoded binary value.1194"""11951196TYPE_STRING = "string"1197TYPE_BASE64 = "base64"11981199def __init__(self, type: Any | None, value: Any | None) -> None:1200self.type = type1201self.value = value12021203def to_bidi_dict(self) -> dict:1204return {"type": self.type, "value": self.value}''',1205'''class Request:1206"""Wraps a BiDi network request event params and provides request action methods."""12071208def __init__(self, conn, params):1209self._conn = conn1210self._params = params if isinstance(params, dict) else {}1211req = self._params.get("request", {}) or {}1212self.url = req.get("url", "")1213self._request_id = req.get("request")12141215def continue_request(self, **kwargs):1216"""Continue the intercepted request.12171218Data URLs (``data:``) are skipped silently because browsers do not1219create an interceptable request entry for them, so calling1220``network.continueRequest`` would raise "no such request".1221"""1222if self.url.startswith("data:"):1223return1224from selenium.webdriver.common.bidi.common import command_builder as _cb12251226params = {"request": self._request_id}1227params.update(kwargs)1228self._conn.execute(_cb("network.continueRequest", params))''',1229],1230# Override auth_required to use raw dict so _auth_callback receives all1231# fields (including "request") from the BiDi event params. The1232# generated AuthRequiredParameters dataclass only contains "response",1233# losing the "request" field that holds the request ID required to call1234# network.continueWithAuth. extra_events entries appear last in the1235# EVENT_CONFIGS dict literal, so this duplicate key overrides the1236# CDDL-generated entry.1237# Add before_request event (maps to network.beforeRequestSent)1238"extra_events": [1239{1240"event_key": "auth_required",1241"bidi_event": "network.authRequired",1242"event_class": "dict",1243},1244{1245"event_key": "before_request",1246"bidi_event": "network.beforeRequestSent",1247"event_class": "dict",1248},1249],1250"extra_methods": [1251''' def _add_intercept(self, phases=None, url_patterns=None):1252"""Add a low-level network intercept.12531254Args:1255phases: list of intercept phases (default: ["beforeRequestSent"])1256url_patterns: optional URL patterns to filter12571258Returns:1259dict with "intercept" key containing the intercept ID1260"""1261from selenium.webdriver.common.bidi.common import command_builder as _cb12621263if phases is None:1264phases = ["beforeRequestSent"]1265params = {"phases": phases}1266if url_patterns:1267params["urlPatterns"] = url_patterns1268result = self._conn.execute(_cb("network.addIntercept", params))1269if result:1270intercept_id = result.get("intercept")1271if intercept_id and intercept_id not in self.intercepts:1272self.intercepts.append(intercept_id)1273return result''',1274''' def _remove_intercept(self, intercept_id):1275"""Remove a low-level network intercept."""1276from selenium.webdriver.common.bidi.common import command_builder as _cb12771278self._conn.execute(_cb("network.removeIntercept", {"intercept": intercept_id}))1279if intercept_id in self.intercepts:1280self.intercepts.remove(intercept_id)''',1281''' def _canonical_request_handler_event(self, event):1282"""Map public request-handler aliases to supported event keys."""1283event_aliases = {1284"auth_required": "auth_required",1285"before_request": "before_request",1286"before_request_sent": "before_request",1287}1288canonical_event = event_aliases.get(event)1289if canonical_event is None:1290available_events = ", ".join(sorted(event_aliases))1291raise ValueError(1292f"Unsupported request handler event '{event}'. Available events: {available_events}"1293)1294return canonical_event''',1295''' def add_request_handler(self, event, callback, url_patterns=None):1296"""Add a handler for network requests at the specified phase.12971298Args:1299event: Event name, e.g. ``"before_request"`` or ``"before_request_sent"``.1300callback: Callable receiving a :class:`Request` instance.1301url_patterns: optional list of URL pattern dicts to filter.13021303Returns:1304callback_id int for later removal via remove_request_handler.1305"""1306canonical_event = self._canonical_request_handler_event(event)1307phase_map = {1308"before_request": "beforeRequestSent",1309"auth_required": "authRequired",1310}1311phase = phase_map[canonical_event]1312intercept_result = self._add_intercept(phases=[phase], url_patterns=url_patterns)1313intercept_id = intercept_result.get("intercept") if intercept_result else None13141315def _request_callback(params):1316raw = (1317params1318if isinstance(params, dict)1319else (params.__dict__ if hasattr(params, "__dict__") else {})1320)1321request = Request(self._conn, raw)1322callback(request)13231324callback_id = self.add_event_handler(canonical_event, _request_callback)1325if intercept_id:1326self._handler_intercepts[callback_id] = intercept_id1327return callback_id''',1328''' def remove_request_handler(self, event, callback_id):1329"""Remove a network request handler and its associated network intercept.13301331Args:1332event: The event name used when adding the handler.1333callback_id: The int returned by add_request_handler.1334"""1335canonical_event = self._canonical_request_handler_event(event)1336self.remove_event_handler(canonical_event, callback_id)1337intercept_id = self._handler_intercepts.pop(callback_id, None)1338if intercept_id:1339self._remove_intercept(intercept_id)''',1340''' def clear_request_handlers(self):1341"""Clear all request handlers and remove all tracked intercepts."""1342self.clear_event_handlers()1343for intercept_id in list(self.intercepts):1344self._remove_intercept(intercept_id)''',1345''' def add_auth_handler(self, username, password):1346"""Add an auth handler that automatically provides credentials.13471348Args:1349username: The username for basic authentication.1350password: The password for basic authentication.13511352Returns:1353callback_id int for later removal via remove_auth_handler.1354"""1355from selenium.webdriver.common.bidi.common import command_builder as _cb13561357# Set up network intercept for authRequired phase1358intercept_result = self._add_intercept(phases=["authRequired"])1359intercept_id = intercept_result.get("intercept") if intercept_result else None13601361def _auth_callback(params):1362raw = (1363params1364if isinstance(params, dict)1365else (params.__dict__ if hasattr(params, "__dict__") else {})1366)1367request_id = (1368raw.get("request", {}).get("request")1369if isinstance(raw, dict)1370else None1371)1372if request_id:1373self._conn.execute(1374_cb(1375"network.continueWithAuth",1376{1377"request": request_id,1378"action": "provideCredentials",1379"credentials": {1380"type": "password",1381"username": username,1382"password": password,1383},1384},1385)1386)13871388callback_id = self.add_event_handler("auth_required", _auth_callback)1389if intercept_id:1390self._handler_intercepts[callback_id] = intercept_id1391return callback_id''',1392''' def remove_auth_handler(self, callback_id):1393"""Remove an auth handler by callback ID and its associated network intercept.13941395Args:1396callback_id: The handler ID returned by add_auth_handler.1397"""1398self.remove_event_handler("auth_required", callback_id)1399intercept_id = self._handler_intercepts.pop(callback_id, None)1400if intercept_id:1401self._remove_intercept(intercept_id)''',1402],1403},1404"storage": {1405# Exclude auto-generated dataclasses that need custom to_bidi_dict()1406# for JSON-over-WebSocket serialization, or custom constructors.1407"exclude_types": [1408"CookieFilter",1409"PartialCookie",1410"BrowsingContextPartitionDescriptor",1411"StorageKeyPartitionDescriptor",1412],1413"extra_dataclasses": [1414# Re-export network types used in cookie operations so they can be1415# imported from selenium.webdriver.common.bidi.storage alongside1416# the storage-specific classes.1417'''class BytesValue:1418"""A string or base64-encoded bytes value used in cookie operations.14191420This corresponds to network.BytesValue in the WebDriver BiDi specification,1421wrapping either a plain string or a base64-encoded binary value.1422"""14231424TYPE_STRING = "string"1425TYPE_BASE64 = "base64"14261427def __init__(self, type: Any | None, value: Any | None) -> None:1428self.type = type1429self.value = value14301431def to_bidi_dict(self) -> dict:1432return {"type": self.type, "value": self.value}14331434def to_dict(self) -> dict:1435"""Backward-compatible alias for to_bidi_dict()."""1436return self.to_bidi_dict()''',1437'''class SameSite:1438"""SameSite cookie attribute values."""14391440STRICT = "strict"1441LAX = "lax"1442NONE = "none"1443DEFAULT = "default"''',1444# Helper: cookie object returned inside a GetCookiesResult response1445'''@dataclass1446class StorageCookie:1447"""A cookie object returned by storage.getCookies."""14481449name: str | None = None1450value: Any | None = None1451domain: str | None = None1452path: str | None = None1453size: Any | None = None1454http_only: bool | None = None1455secure: bool | None = None1456same_site: Any | None = None1457expiry: Any | None = None14581459@classmethod1460def from_bidi_dict(cls, raw: dict) -> StorageCookie:1461"""Deserialize a wire-level cookie dict to a StorageCookie."""1462value_raw = raw.get("value")1463if isinstance(value_raw, dict):1464value: Any = BytesValue(value_raw.get("type"), value_raw.get("value"))1465else:1466value = value_raw1467return cls(1468name=raw.get("name"),1469value=value,1470domain=raw.get("domain"),1471path=raw.get("path"),1472size=raw.get("size"),1473http_only=raw.get("httpOnly"),1474secure=raw.get("secure"),1475same_site=raw.get("sameSite"),1476expiry=raw.get("expiry"),1477)''',1478# Custom CookieFilter with camelCase serialization1479'''@dataclass1480class CookieFilter:1481"""CookieFilter."""14821483name: str | None = None1484value: Any | None = None1485domain: str | None = None1486path: str | None = None1487size: Any | None = None1488http_only: bool | None = None1489secure: bool | None = None1490same_site: Any | None = None1491expiry: Any | None = None14921493def to_bidi_dict(self) -> dict:1494"""Serialize to the BiDi wire-protocol dict."""1495result: dict = {}1496if self.name is not None:1497result["name"] = self.name1498if self.value is not None:1499result["value"] = self.value.to_bidi_dict() if hasattr(self.value, "to_bidi_dict") else self.value1500if self.domain is not None:1501result["domain"] = self.domain1502if self.path is not None:1503result["path"] = self.path1504if self.size is not None:1505result["size"] = self.size1506if self.http_only is not None:1507result["httpOnly"] = self.http_only1508if self.secure is not None:1509result["secure"] = self.secure1510if self.same_site is not None:1511result["sameSite"] = self.same_site1512if self.expiry is not None:1513result["expiry"] = self.expiry1514return result15151516def to_dict(self) -> dict:1517"""Backward-compatible alias for to_bidi_dict()."""1518return self.to_bidi_dict()''',1519# Custom PartialCookie with camelCase serialization1520'''@dataclass1521class PartialCookie:1522"""PartialCookie."""15231524name: str | None = None1525value: Any | None = None1526domain: str | None = None1527path: str | None = None1528http_only: bool | None = None1529secure: bool | None = None1530same_site: Any | None = None1531expiry: Any | None = None15321533def to_bidi_dict(self) -> dict:1534"""Serialize to the BiDi wire-protocol dict."""1535result: dict = {}1536if self.name is not None:1537result["name"] = self.name1538if self.value is not None:1539result["value"] = self.value.to_bidi_dict() if hasattr(self.value, "to_bidi_dict") else self.value1540if self.domain is not None:1541result["domain"] = self.domain1542if self.path is not None:1543result["path"] = self.path1544if self.http_only is not None:1545result["httpOnly"] = self.http_only1546if self.secure is not None:1547result["secure"] = self.secure1548if self.same_site is not None:1549result["sameSite"] = self.same_site1550if self.expiry is not None:1551result["expiry"] = self.expiry1552return result15531554def to_dict(self) -> dict:1555"""Backward-compatible alias for to_bidi_dict()."""1556return self.to_bidi_dict()''',1557# BrowsingContextPartitionDescriptor: first positional arg is *context*1558# (the auto-generated dataclass had `type` first, breaking positional1559# usage like BrowsingContextPartitionDescriptor(driver.current_window_handle))1560'''class BrowsingContextPartitionDescriptor:1561"""BrowsingContextPartitionDescriptor.15621563The first positional argument is *context* (a browsing-context ID / window1564handle), mirroring how the class is used throughout the test suite:1565``BrowsingContextPartitionDescriptor(driver.current_window_handle)``.1566"""15671568def __init__(self, context: Any = None, type: str = "context") -> None:1569self.context = context1570self.type = type15711572def to_bidi_dict(self) -> dict:1573return {"type": "context", "context": self.context}15741575def to_dict(self) -> dict:1576"""Backward-compatible alias for to_bidi_dict()."""1577return self.to_bidi_dict()''',1578# StorageKeyPartitionDescriptor with camelCase serialization1579'''@dataclass1580class StorageKeyPartitionDescriptor:1581"""StorageKeyPartitionDescriptor."""15821583type: Any | None = "storageKey"1584user_context: str | None = None1585source_origin: str | None = None15861587def to_bidi_dict(self) -> dict:1588"""Serialize to the BiDi wire-protocol dict."""1589result: dict = {"type": "storageKey"}1590if self.user_context is not None:1591result["userContext"] = self.user_context1592if self.source_origin is not None:1593result["sourceOrigin"] = self.source_origin1594return result15951596def to_dict(self) -> dict:1597"""Backward-compatible alias for to_bidi_dict()."""1598return self.to_bidi_dict()''',1599],1600# Override the generated Storage class methods (Python's last-definition-1601# wins semantics means these extra_methods shadow the generated ones).1602"extra_methods": [1603''' def get_cookies(self, filter=None, partition=None):1604"""Execute storage.getCookies and return a GetCookiesResult."""1605if filter and hasattr(filter, "to_bidi_dict"):1606filter = filter.to_bidi_dict()1607if partition and hasattr(partition, "to_bidi_dict"):1608partition = partition.to_bidi_dict()1609params = {1610"filter": filter,1611"partition": partition,1612}1613params = {k: v for k, v in params.items() if v is not None}1614cmd = command_builder("storage.getCookies", params)1615result = self._conn.execute(cmd)1616if result and "cookies" in result:1617cookies = [1618StorageCookie.from_bidi_dict(c)1619for c in result.get("cookies", [])1620if isinstance(c, dict)1621]1622pk_raw = result.get("partitionKey")1623pk = (1624PartitionKey(1625user_context=pk_raw.get("userContext"),1626source_origin=pk_raw.get("sourceOrigin"),1627)1628if isinstance(pk_raw, dict)1629else None1630)1631return GetCookiesResult(cookies=cookies, partition_key=pk)1632return GetCookiesResult(cookies=[], partition_key=None)''',1633''' def set_cookie(self, cookie=None, partition=None):1634"""Execute storage.setCookie."""1635if cookie and hasattr(cookie, "to_bidi_dict"):1636cookie = cookie.to_bidi_dict()1637if partition and hasattr(partition, "to_bidi_dict"):1638partition = partition.to_bidi_dict()1639params = {1640"cookie": cookie,1641"partition": partition,1642}1643params = {k: v for k, v in params.items() if v is not None}1644cmd = command_builder("storage.setCookie", params)1645result = self._conn.execute(cmd)1646if isinstance(result, dict):1647pk_raw = result.get("partitionKey")1648pk = (1649PartitionKey(1650user_context=pk_raw.get("userContext"),1651source_origin=pk_raw.get("sourceOrigin"),1652)1653if isinstance(pk_raw, dict)1654else None1655)1656return SetCookieResult(partition_key=pk)1657return result''',1658''' def delete_cookies(self, filter=None, partition=None):1659"""Execute storage.deleteCookies."""1660if filter and hasattr(filter, "to_bidi_dict"):1661filter = filter.to_bidi_dict()1662if partition and hasattr(partition, "to_bidi_dict"):1663partition = partition.to_bidi_dict()1664params = {1665"filter": filter,1666"partition": partition,1667}1668params = {k: v for k, v in params.items() if v is not None}1669cmd = command_builder("storage.deleteCookies", params)1670result = self._conn.execute(cmd)1671if isinstance(result, dict):1672pk_raw = result.get("partitionKey")1673pk = (1674PartitionKey(1675user_context=pk_raw.get("userContext"),1676source_origin=pk_raw.get("sourceOrigin"),1677)1678if isinstance(pk_raw, dict)1679else None1680)1681return DeleteCookiesResult(partition_key=pk)1682return result''',1683],1684},1685"session": {1686# Override UserPromptHandler to add to_bidi_dict() for JSON serialization1687"exclude_types": ["UserPromptHandler"],1688"extra_dataclasses": [1689'''@dataclass1690class UserPromptHandler:1691"""UserPromptHandler."""16921693alert: Any | None = None1694before_unload: Any | None = None1695confirm: Any | None = None1696default: Any | None = None1697file: Any | None = None1698prompt: Any | None = None16991700def to_bidi_dict(self) -> dict:1701"""Convert to BiDi protocol dict with camelCase keys."""1702result = {}1703if self.alert is not None:1704result["alert"] = self.alert1705if self.before_unload is not None:1706result["beforeUnload"] = self.before_unload1707if self.confirm is not None:1708result["confirm"] = self.confirm1709if self.default is not None:1710result["default"] = self.default1711if self.file is not None:1712result["file"] = self.file1713if self.prompt is not None:1714result["prompt"] = self.prompt1715return result17161717def to_dict(self) -> dict:1718"""Backward-compatible alias for to_bidi_dict()."""1719return self.to_bidi_dict()''',1720],1721},1722"webExtension": {1723# Suppress the raw generated stubs; hand-written versions follow below1724"exclude_methods": ["install", "uninstall"],1725"extra_methods": [1726''' def install(1727self,1728path: str | None = None,1729archive_path: str | None = None,1730base64_value: str | None = None,1731):1732"""Install a web extension.17331734Exactly one of the three keyword arguments must be provided.17351736Args:1737path: Directory path to an unpacked extension (also accepted for1738signed ``.xpi`` / ``.crx`` archive files on Firefox).1739archive_path: File-system path to a packed extension archive.1740base64_value: Base64-encoded extension archive string.17411742Returns:1743The raw result dict from the BiDi ``webExtension.install`` command1744(contains at least an ``"extension"`` key with the extension ID).17451746Raises:1747ValueError: If more than one, or none, of the arguments is provided.1748"""1749provided = [1750k for k, v in {1751"path": path, "archive_path": archive_path, "base64_value": base64_value,1752}.items() if v is not None1753]1754if len(provided) != 1:1755raise ValueError(1756f"Exactly one of path, archive_path, or base64_value must be provided; got: {provided}"1757)1758if path is not None:1759extension_data = {"type": "path", "path": path}1760elif archive_path is not None:1761extension_data = {"type": "archivePath", "path": archive_path}1762else:1763assert base64_value is not None1764extension_data = {"type": "base64", "value": base64_value}1765params = {"extensionData": extension_data}1766cmd = command_builder("webExtension.install", params)1767try:1768return self._conn.execute(cmd)1769except Exception as e:1770if "Method not available" in str(e):1771raise RuntimeError(1772"webExtension.install failed with 'Method not available'. "1773"This likely means that web extension support is disabled. "1774"Enable unsafe extension debugging and/or set options.enable_webextensions "1775"in your WebDriver configuration."1776) from e1777raise''',1778''' def uninstall(self, extension: str | dict):1779"""Uninstall a web extension.17801781Args:1782extension: Either the extension ID string returned by ``install``,1783or the full result dict returned by ``install`` (the1784``"extension"`` value is extracted automatically).17851786Raises:1787ValueError: If extension is not provided or is None.1788"""1789if isinstance(extension, dict):1790extension_id: Any = extension.get("extension")1791else:1792extension_id = extension17931794if extension_id is None:1795raise ValueError("extension parameter is required")17961797params = {"extension": extension_id}1798cmd = command_builder("webExtension.uninstall", params)1799return self._conn.execute(cmd)''',1800],1801},1802"input": {1803# FileDialogInfo needs from_json for event deserialization1804"exclude_types": ["FileDialogInfo", "PointerMoveAction", "PointerDownAction"],1805"extra_dataclasses": [1806'''@dataclass1807class FileDialogInfo:1808"""FileDialogInfo - parameters for the input.fileDialogOpened event."""18091810context: Any | None = None1811element: Any | None = None1812multiple: bool | None = None18131814@classmethod1815def from_json(cls, params: dict) -> FileDialogInfo:1816"""Deserialize event params into FileDialogInfo."""1817return cls(1818context=params.get("context"),1819element=params.get("element"),1820multiple=params.get("multiple"),1821)''',1822'''@dataclass1823class PointerMoveAction:1824"""PointerMoveAction."""18251826type: str = field(default="pointerMove", init=False)1827x: Any | None = None1828y: Any | None = None1829duration: Any | None = None1830origin: Any | None = None1831properties: Any | None = None''',1832'''@dataclass1833class PointerDownAction:1834"""PointerDownAction."""18351836type: str = field(default="pointerDown", init=False)1837button: Any | None = None1838properties: Any | None = None''',1839],1840"extra_methods": [1841''' def add_file_dialog_handler(self, callback) -> int:1842"""Subscribe to the input.fileDialogOpened event.18431844Args:1845callback: Callable invoked with a FileDialogInfo when a file dialog opens.18461847Returns:1848A handler ID that can be passed to remove_file_dialog_handler.1849"""1850return self._event_manager.add_event_handler("file_dialog_opened", callback)18511852def remove_file_dialog_handler(self, handler_id: int) -> None:1853"""Unsubscribe a previously registered file dialog event handler.18541855Args:1856handler_id: The handler ID returned by add_file_dialog_handler.1857"""1858return self._event_manager.remove_event_handler("file_dialog_opened", handler_id)''',1859],1860},1861"permissions": {1862"module_docstring": (1863"WebDriver BiDi permissions module.\n\n"1864"Provides control over browser permission grants during automated tests,\n"1865"as specified by the W3C Permissions specification.\n\n"1866"Typical usage::\n\n"1867" driver.permissions.set_permission('geolocation', 'granted', origin)\n"1868),1869"class_docstrings": {1870"PermissionState": (1871"Permission state constants.\n\n"1872"GRANTED: The permission is granted — the browser will not prompt the user.\n"1873"DENIED: The permission is denied — the browser will block the request.\n"1874"PROMPT: The browser will show a permission prompt (default browser behaviour)."1875),1876"Permissions": (1877"BiDi interface for controlling browser permissions.\n\nAccess via ``driver.permissions``."1878),1879},1880"extra_dataclasses": [1881'''class PermissionDescriptor:1882"""Descriptor identifying a permission by name.18831884Args:1885name: The permission name (e.g. 'geolocation', 'microphone', 'camera').1886"""18871888def __init__(self, name: str) -> None:1889self.name = name18901891def __repr__(self) -> str:1892return f"PermissionDescriptor(name={self.name!r})"''',1893],1894"extra_methods": [1895''' def set_permission(1896self,1897descriptor: "PermissionDescriptor | str",1898state: "PermissionState | str",1899origin: str | None = None,1900user_context: str | None = None,1901*,1902embedded_origin: str | None = None,1903) -> None:1904"""Set a browser permission.19051906Args:1907descriptor: The permission descriptor or permission name as a string.1908state: The desired permission state (granted, denied, or prompt).1909origin: The origin to scope the permission to.1910user_context: Optional user context ID to scope the permission.1911embedded_origin: Keyword-only. Embedded origin for cross-origin1912iframes; scopes the permission to that iframe's origin.19131914Raises:1915ValueError: If *state* is not a valid permission state.1916"""1917state_value = state.value if isinstance(state, PermissionState) else state1918valid_states = {"granted", "denied", "prompt"}1919if state_value not in valid_states:1920raise ValueError(1921f"Invalid permission state: {state_value!r}. "1922f"Must be one of {sorted(valid_states)}"1923)19241925descriptor_dict = {"name": descriptor} if isinstance(descriptor, str) else {"name": descriptor.name}19261927params: dict = {1928"descriptor": descriptor_dict,1929"state": state_value,1930}1931if origin is not None:1932params["origin"] = origin1933if embedded_origin is not None:1934params["embeddedOrigin"] = embedded_origin1935if user_context is not None:1936params["userContext"] = user_context19371938cmd = command_builder("permissions.setPermission", params)1939self._conn.execute(cmd)''',1940],1941},1942"bluetooth": {1943"module_docstring": (1944"WebDriver BiDi bluetooth module.\n\n"1945"Provides a simulation API for Web Bluetooth, allowing tests to fake\n"1946"Bluetooth adapters, nearby peripherals, GATT services, characteristics,\n"1947"and descriptors without physical hardware.\n"1948),1949"class_docstrings": {1950"Bluetooth": (1951"BiDi interface for simulating Web Bluetooth hardware.\n\n"1952"Simulate adapters, peripherals, GATT services, characteristics,\n"1953"and descriptors without physical hardware."1954),1955"RequestDeviceInfo": (1956"Identifies a simulated Bluetooth device returned in a device-request prompt.\n\n"1957"Attributes:\n"1958" id: The internal device identifier.\n"1959" name: The human-readable device name shown in the prompt."1960),1961"SimulateAdapterParameters": (1962"Parameters for simulating a Bluetooth adapter state.\n\n"1963"Attributes:\n"1964" context: The browsing context ID to target.\n"1965" le_supported: Whether the adapter supports Bluetooth Low Energy.\n"1966" state: Adapter power state (e.g. 'powered-on', 'powered-off', 'absent')."1967),1968"SimulatePreconnectedPeripheralParameters": (1969"Parameters for adding a pre-connected simulated peripheral.\n\n"1970"Attributes:\n"1971" context: The browsing context ID to target.\n"1972" address: The Bluetooth device address (e.g. '09:09:09:09:09:09').\n"1973" name: The device name advertised to the page.\n"1974" manufacturer_data: List of manufacturer-specific data records.\n"1975" known_service_uuids: UUIDs of GATT services the device exposes."1976),1977"SimulateAdvertisementParameters": (1978"Parameters for injecting a simulated advertisement packet.\n\n"1979"Attributes:\n"1980" context: The browsing context ID to target.\n"1981" scan_entry: The advertisement scan record to inject."1982),1983"SimulateGattConnectionResponseParameters": (1984"Parameters for simulating a GATT connection response.\n\n"1985"Attributes:\n"1986" context: The browsing context ID to target.\n"1987" address: The address of the peripheral.\n"1988" code: The ATT error code (0 = success)."1989),1990"SimulateCharacteristicParameters": (1991"Parameters for adding a simulated GATT characteristic to a service.\n\n"1992"Attributes:\n"1993" context: The browsing context ID to target.\n"1994" address: The peripheral address.\n"1995" service: The service UUID the characteristic belongs to.\n"1996" characteristic: UUID of the characteristic.\n"1997" properties: Supported operations (read, write, notify, etc.)."1998),1999},2000"command_docstrings": {2001"handle_request_device_prompt": (2002"Dismiss or accept a Bluetooth device-chooser prompt.\n\n"2003"Args:\n"2004" context: The browsing context containing the prompt.\n"2005" prompt: The prompt ID returned in the prompt-opened event."2006),2007"simulate_adapter": (2008"Simulate a Bluetooth adapter in the given browsing context.\n\n"2009"Args:\n"2010" context: The browsing context ID to target.\n"2011" le_supported: Whether Low Energy is supported.\n"2012" state: Adapter state ('powered-on', 'powered-off', 'absent')."2013),2014"disable_simulation": (2015"Disable all Bluetooth simulation in the given context, restoring real behaviour.\n\n"2016"Args:\n"2017" context: The browsing context ID to stop simulating."2018),2019"simulate_preconnected_peripheral": (2020"Register a simulated peripheral as already connected to the adapter.\n\n"2021"Args:\n"2022" context: The browsing context ID to target.\n"2023" address: The Bluetooth device address.\n"2024" name: The device name.\n"2025" manufacturer_data: Manufacturer-specific advertisement data.\n"2026" known_service_uuids: List of GATT service UUIDs the device exposes."2027),2028"simulate_advertisement": (2029"Inject a simulated Bluetooth advertisement packet.\n\n"2030"Args:\n"2031" context: The browsing context ID to target.\n"2032" scan_entry: The advertisement scan record to inject."2033),2034"simulate_gatt_connection_response": (2035"Respond to a pending GATT connection attempt from the page.\n\n"2036"Args:\n"2037" context: The browsing context ID.\n"2038" address: The peripheral address.\n"2039" code: ATT error code (0 = success; non-zero signals failure)."2040),2041"simulate_gatt_disconnection": (2042"Simulate a GATT disconnection for the given peripheral.\n\n"2043"Args:\n"2044" context: The browsing context ID.\n"2045" address: The address of the peripheral to disconnect."2046),2047"simulate_service": (2048"Add a simulated GATT service to a peripheral.\n\n"2049"Args:\n"2050" context: The browsing context ID.\n"2051" address: The peripheral address.\n"2052" uuid: The service UUID."2053),2054"simulate_characteristic": (2055"Add a simulated GATT characteristic to a service.\n\n"2056"Args:\n"2057" context: The browsing context ID.\n"2058" address: The peripheral address.\n"2059" service: The service UUID.\n"2060" characteristic: The characteristic UUID.\n"2061" properties: Supported operations bitmap."2062),2063"simulate_characteristic_response": (2064"Respond to a pending read or write on a simulated characteristic.\n\n"2065"Args:\n"2066" context: The browsing context ID.\n"2067" address: The peripheral address.\n"2068" service: The service UUID.\n"2069" characteristic: The characteristic UUID.\n"2070" code: ATT error code (0 = success).\n"2071" body: The characteristic value bytes (for reads)."2072),2073"simulate_descriptor": (2074"Add a simulated GATT descriptor to a characteristic.\n\n"2075"Args:\n"2076" context: The browsing context ID.\n"2077" address: The peripheral address.\n"2078" service: The service UUID.\n"2079" characteristic: The characteristic UUID.\n"2080" descriptor: The descriptor UUID."2081),2082"simulate_descriptor_response": (2083"Respond to a pending read or write on a simulated descriptor.\n\n"2084"Args:\n"2085" context: The browsing context ID.\n"2086" address: The peripheral address.\n"2087" service: The service UUID.\n"2088" characteristic: The characteristic UUID.\n"2089" descriptor: The descriptor UUID.\n"2090" code: ATT error code (0 = success).\n"2091" body: The descriptor value bytes (for reads)."2092),2093},2094},2095"speculation": {2096"module_docstring": (2097"WebDriver BiDi speculation module.\n\n"2098"Provides events for observing the status of Speculation Rules prefetch\n"2099"requests initiated by the browser (e.g. via <script type='speculationrules'>).\n"2100),2101"class_docstrings": {2102"Speculation": ("BiDi interface for observing Speculation Rules prefetch activity."),2103"PreloadingStatus": (2104"Status values for a speculation-rules prefetch operation.\n\n"2105"PENDING: The prefetch has been queued but not yet attempted.\n"2106"READY: The prefetch succeeded and the resource is cached.\n"2107"SUCCESS: The prefetched navigation was used successfully.\n"2108"FAILURE: The prefetch failed or was cancelled."2109),2110"PrefetchStatusUpdatedParameters": (2111"Event payload emitted when a prefetch status changes.\n\n"2112"Attributes:\n"2113" context: The browsing context ID that owns the speculation rule.\n"2114" url: The URL being prefetched.\n"2115" status: The new prefetch status (see PreloadingStatus)."2116),2117},2118},2119"userAgentClientHints": {2120"module_docstring": (2121"WebDriver BiDi userAgentClientHints module.\n\n"2122"Provides an API for overriding the User-Agent Client Hints reported\n"2123"by the browser, enabling tests to simulate different devices, platforms,\n"2124"and browser brands without changing the actual browser binary.\n"2125),2126"class_docstrings": {2127"UserAgentClientHints": ("BiDi interface for overriding User-Agent Client Hints."),2128"ClientHintsMetadata": (2129"Full set of User-Agent Client Hint values to override.\n\n"2130"Attributes:\n"2131" brands: List of browser brand/version pairs (e.g. [BrandVersion('Chrome', '120')]).\n"2132" full_version_list: Brands with full version strings.\n"2133" platform: Operating system name (e.g. 'Windows', 'macOS').\n"2134" platform_version: OS version string.\n"2135" architecture: CPU architecture (e.g. 'x86', 'arm').\n"2136" model: Device model (primarily for mobile).\n"2137" mobile: True if the UA should appear to be a mobile device.\n"2138" bitness: Pointer-size bitness string ('32' or '64').\n"2139" wow64: True if running a 32-bit process on 64-bit Windows.\n"2140" form_factors: Device form factors (e.g. 'Desktop', 'Phone')."2141),2142"BrandVersion": (2143"A single browser brand entry used in Client Hints brand lists.\n\n"2144"Attributes:\n"2145" brand: The browser/engine brand name (e.g. 'Google Chrome').\n"2146" version: The major or full version string (e.g. '120')."2147),2148},2149},2150}215121522153# ============================================================================2154# Pre-processing Functions2155# ============================================================================215621572158def check_serialize_method(obj: Any) -> Any:2159"""Check if object has to_bidi_dict() method and use it for serialization."""2160if obj and hasattr(obj, "to_bidi_dict"):2161return obj.to_bidi_dict()2162return obj216321642165# ============================================================================2166# Validation Functions2167# ============================================================================216821692170def validate_download_behavior(2171allowed: bool | None,2172destination_folder: str | None,2173user_contexts: Any | None = None,2174) -> None:2175"""Validate download behavior parameters.21762177Args:2178allowed: Whether downloads are allowed2179destination_folder: Destination folder for downloads2180user_contexts: Optional list of user contexts (ignored for validation)21812182Raises:2183ValueError: If parameters are invalid2184"""2185if allowed is True and not destination_folder:2186raise ValueError("destination_folder is required when allowed=True")2187if allowed is False and destination_folder:2188raise ValueError("destination_folder should not be provided when allowed=False")218921902191# ============================================================================2192# Transformation Functions2193# ============================================================================219421952196def transform_download_params(2197allowed: bool | None,2198destination_folder: str | None,2199) -> dict[str, Any]:2200"""Transform download parameters into download_behavior object.22012202Args:2203allowed: Whether downloads are allowed2204destination_folder: Destination folder for downloads22052206Returns:2207Dictionary representing the download_behavior object, or None if allowed is None2208"""2209if allowed is True:2210return {2211"type": "allowed",2212# Convert pathlib.Path (or any path-like) to str so the BiDi2213# protocol always receives a plain JSON string.2214"destinationFolder": (str(destination_folder) if destination_folder is not None else None),2215}2216elif allowed is False:2217return {"type": "denied"}2218else: # None — reset to browser default (sent as JSON null)2219return None222022212222# ============================================================================2223# Dataclass Method Templates2224# ============================================================================22252226DATACLASS_METHOD_TEMPLATES: dict[str, dict[str, str]] = {2227"ClientWindowInfo": {2228"get_client_window": "return self.client_window",2229"get_state": "return self.state",2230"get_width": "return self.width",2231"get_height": "return self.height",2232"is_active": "return self.active",2233"get_x": "return self.x",2234"get_y": "return self.y",2235},2236"BrowsingContext": {2237"add_event_handler": "_add_event_handler_impl",2238"remove_event_handler": "_remove_event_handler_impl",2239},2240}22412242DATACLASS_METHOD_DOCSTRINGS: dict[str, dict[str, str]] = {2243"ClientWindowInfo": {2244"get_client_window": "Get the client window ID.",2245"get_state": "Get the client window state.",2246"get_width": "Get the client window width.",2247"get_height": "Get the client window height.",2248"is_active": "Check if the client window is active.",2249"get_x": "Get the client window X position.",2250"get_y": "Get the client window Y position.",2251},2252"BrowsingContext": {2253"add_event_handler": "Add an event handler for browsing context events.",2254"remove_event_handler": "Remove an event handler by callback ID.",2255},2256}22572258# ============================================================================2259# Event Handler Support for BrowsingContext2260# ============================================================================226122622263def _add_event_handler(2264self,2265event_name: str,2266callback: callable,2267contexts: list[str] | None = None,2268) -> str:2269"""Add an event handler for a browsing context event.22702271Supported events:2272- 'context_created'2273- 'context_destroyed'2274- 'navigation_started'2275- 'navigation_committed'2276- 'navigation_failed'2277- 'dom_content_loaded'2278- 'load'2279- 'fragment_navigated'2280- 'user_prompt_opened'2281- 'user_prompt_closed'2282- 'download_will_begin'2283- 'download_end'2284- 'history_updated'22852286Args:2287self: The module instance this handler is bound to.2288event_name: The name of the event to subscribe to2289callback: Callback function to invoke when event occurs2290contexts: Optional list of context IDs to limit event subscription22912292Returns:2293A callback ID that can be used to unsubscribe the handler2294"""2295if not hasattr(self, "_event_handlers"):2296self._event_handlers = {}2297self._event_callback_id_counter = 022982299# Generate unique callback ID2300self._event_callback_id_counter += 12301callback_id = f"callback_{self._event_callback_id_counter}"23022303# Store the handler2304self._event_handlers[callback_id] = {2305"event": event_name,2306"callback": callback,2307"contexts": contexts,2308}23092310# Subscribe via the driver's event listening mechanism2311if hasattr(self._driver, "_subscribe_event"):2312self._driver._subscribe_event(event_name, callback, contexts)23132314return callback_id231523162317def _remove_event_handler(2318self,2319callback_id: str,2320) -> None:2321"""Remove an event handler by its callback ID.23222323Args:2324self: The module instance this handler is bound to.2325callback_id: The callback ID returned from add_event_handler2326"""2327if not hasattr(self, "_event_handlers"):2328return23292330if callback_id in self._event_handlers:2331handler_info = self._event_handlers[callback_id]23322333# Unsubscribe from the driver2334if hasattr(self._driver, "_unsubscribe_event"):2335self._driver._unsubscribe_event(2336handler_info["event"],2337handler_info["callback"],2338handler_info["contexts"],2339)23402341del self._event_handlers[callback_id]234223432344