Path: blob/trunk/py/selenium/webdriver/common/bidi/browsing_context.py
4017 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.1617import threading18from collections.abc import Callable19from dataclasses import dataclass20from typing import Any2122from typing_extensions import Sentinel2324from selenium.webdriver.common.bidi.common import command_builder25from selenium.webdriver.common.bidi.session import Session2627UNDEFINED = Sentinel("UNDEFINED")282930class ReadinessState:31"""Represents the stage of document loading at which a navigation command will return."""3233NONE = "none"34INTERACTIVE = "interactive"35COMPLETE = "complete"363738class UserPromptType:39"""Represents the possible user prompt types."""4041ALERT = "alert"42BEFORE_UNLOAD = "beforeunload"43CONFIRM = "confirm"44PROMPT = "prompt"454647class NavigationInfo:48"""Provides details of an ongoing navigation."""4950def __init__(51self,52context: str,53navigation: str | None,54timestamp: int,55url: str,56):57self.context = context58self.navigation = navigation59self.timestamp = timestamp60self.url = url6162@classmethod63def from_json(cls, json: dict) -> "NavigationInfo":64"""Creates a NavigationInfo instance from a dictionary.6566Args:67json: A dictionary containing the navigation information.6869Returns:70A new instance of NavigationInfo.71"""72context = json.get("context")73if context is None or not isinstance(context, str):74raise ValueError("context is required and must be a string")7576navigation = json.get("navigation")77if navigation is not None and not isinstance(navigation, str):78raise ValueError("navigation must be a string")7980timestamp = json.get("timestamp")81if timestamp is None or not isinstance(timestamp, int) or timestamp < 0:82raise ValueError("timestamp is required and must be a non-negative integer")8384url = json.get("url")85if url is None or not isinstance(url, str):86raise ValueError("url is required and must be a string")8788return cls(context, navigation, timestamp, url)899091class BrowsingContextInfo:92"""Represents the properties of a navigable."""9394def __init__(95self,96context: str,97url: str,98children: list["BrowsingContextInfo"] | None,99client_window: str,100user_context: str,101parent: str | None = None,102original_opener: str | None = None,103):104self.context = context105self.url = url106self.children = children107self.parent = parent108self.user_context = user_context109self.original_opener = original_opener110self.client_window = client_window111112@classmethod113def from_json(cls, json: dict) -> "BrowsingContextInfo":114"""Creates a BrowsingContextInfo instance from a dictionary.115116Args:117json: A dictionary containing the browsing context information.118119Returns:120A new instance of BrowsingContextInfo.121"""122children = None123raw_children = json.get("children")124if raw_children is not None:125if not isinstance(raw_children, list):126raise ValueError("children must be a list if provided")127128children = []129for child in raw_children:130if not isinstance(child, dict):131raise ValueError(f"Each child must be a dictionary, got {type(child)}")132children.append(BrowsingContextInfo.from_json(child))133134context = json.get("context")135if context is None or not isinstance(context, str):136raise ValueError("context is required and must be a string")137138url = json.get("url")139if url is None or not isinstance(url, str):140raise ValueError("url is required and must be a string")141142parent = json.get("parent")143if parent is not None and not isinstance(parent, str):144raise ValueError("parent must be a string if provided")145146user_context = json.get("userContext")147if user_context is None or not isinstance(user_context, str):148raise ValueError("userContext is required and must be a string")149150original_opener = json.get("originalOpener")151if original_opener is not None and not isinstance(original_opener, str):152raise ValueError("originalOpener must be a string if provided")153154client_window = json.get("clientWindow")155if client_window is None or not isinstance(client_window, str):156raise ValueError("clientWindow is required and must be a string")157158return cls(159context=context,160url=url,161children=children,162client_window=client_window,163user_context=user_context,164parent=parent,165original_opener=original_opener,166)167168169class DownloadWillBeginParams(NavigationInfo):170"""Parameters for the downloadWillBegin event."""171172def __init__(173self,174context: str,175navigation: str | None,176timestamp: int,177url: str,178suggested_filename: str,179):180super().__init__(context, navigation, timestamp, url)181self.suggested_filename = suggested_filename182183@classmethod184def from_json(cls, json: dict) -> "DownloadWillBeginParams":185nav_info = NavigationInfo.from_json(json)186187suggested_filename = json.get("suggestedFilename")188if suggested_filename is None or not isinstance(suggested_filename, str):189raise ValueError("suggestedFilename is required and must be a string")190191return cls(192context=nav_info.context,193navigation=nav_info.navigation,194timestamp=nav_info.timestamp,195url=nav_info.url,196suggested_filename=suggested_filename,197)198199200class UserPromptOpenedParams:201"""Parameters for the userPromptOpened event."""202203def __init__(204self,205context: str,206handler: str,207message: str,208type: str,209default_value: str | None = None,210):211self.context = context212self.handler = handler213self.message = message214self.type = type215self.default_value = default_value216217@classmethod218def from_json(cls, json: dict) -> "UserPromptOpenedParams":219"""Creates a UserPromptOpenedParams instance from a dictionary.220221Args:222json: A dictionary containing the user prompt parameters.223224Returns:225A new instance of UserPromptOpenedParams.226"""227context = json.get("context")228if context is None or not isinstance(context, str):229raise ValueError("context is required and must be a string")230231handler = json.get("handler")232if handler is None or not isinstance(handler, str):233raise ValueError("handler is required and must be a string")234235message = json.get("message")236if message is None or not isinstance(message, str):237raise ValueError("message is required and must be a string")238239type_value = json.get("type")240if type_value is None or not isinstance(type_value, str):241raise ValueError("type is required and must be a string")242243default_value = json.get("defaultValue")244if default_value is not None and not isinstance(default_value, str):245raise ValueError("defaultValue must be a string if provided")246247return cls(248context=context,249handler=handler,250message=message,251type=type_value,252default_value=default_value,253)254255256class UserPromptClosedParams:257"""Parameters for the userPromptClosed event."""258259def __init__(260self,261context: str,262accepted: bool,263type: str,264user_text: str | None = None,265):266self.context = context267self.accepted = accepted268self.type = type269self.user_text = user_text270271@classmethod272def from_json(cls, json: dict) -> "UserPromptClosedParams":273"""Creates a UserPromptClosedParams instance from a dictionary.274275Args:276json: A dictionary containing the user prompt closed parameters.277278Returns:279A new instance of UserPromptClosedParams.280"""281context = json.get("context")282if context is None or not isinstance(context, str):283raise ValueError("context is required and must be a string")284285accepted = json.get("accepted")286if accepted is None or not isinstance(accepted, bool):287raise ValueError("accepted is required and must be a boolean")288289type_value = json.get("type")290if type_value is None or not isinstance(type_value, str):291raise ValueError("type is required and must be a string")292293user_text = json.get("userText")294if user_text is not None and not isinstance(user_text, str):295raise ValueError("userText must be a string if provided")296297return cls(298context=context,299accepted=accepted,300type=type_value,301user_text=user_text,302)303304305class HistoryUpdatedParams:306"""Parameters for the historyUpdated event."""307308def __init__(309self,310context: str,311timestamp: int,312url: str,313):314self.context = context315self.timestamp = timestamp316self.url = url317318@classmethod319def from_json(cls, json: dict) -> "HistoryUpdatedParams":320"""Creates a HistoryUpdatedParams instance from a dictionary.321322Args:323json: A dictionary containing the history updated parameters.324325Returns:326A new instance of HistoryUpdatedParams.327"""328context = json.get("context")329if context is None or not isinstance(context, str):330raise ValueError("context is required and must be a string")331332timestamp = json.get("timestamp")333if timestamp is None or not isinstance(timestamp, int) or timestamp < 0:334raise ValueError("timestamp is required and must be a non-negative integer")335336url = json.get("url")337if url is None or not isinstance(url, str):338raise ValueError("url is required and must be a string")339340return cls(341context=context,342timestamp=timestamp,343url=url,344)345346347class DownloadCanceledParams(NavigationInfo):348def __init__(349self,350context: str,351navigation: str | None,352timestamp: int,353url: str,354status: str = "canceled",355):356super().__init__(context, navigation, timestamp, url)357self.status = status358359@classmethod360def from_json(cls, json: dict) -> "DownloadCanceledParams":361nav_info = NavigationInfo.from_json(json)362363status = json.get("status")364if status is None or status != "canceled":365raise ValueError("status is required and must be 'canceled'")366367return cls(368context=nav_info.context,369navigation=nav_info.navigation,370timestamp=nav_info.timestamp,371url=nav_info.url,372status=status,373)374375376class DownloadCompleteParams(NavigationInfo):377def __init__(378self,379context: str,380navigation: str | None,381timestamp: int,382url: str,383status: str = "complete",384filepath: str | None = None,385):386super().__init__(context, navigation, timestamp, url)387self.status = status388self.filepath = filepath389390@classmethod391def from_json(cls, json: dict) -> "DownloadCompleteParams":392nav_info = NavigationInfo.from_json(json)393394status = json.get("status")395if status is None or status != "complete":396raise ValueError("status is required and must be 'complete'")397398filepath = json.get("filepath")399if filepath is not None and not isinstance(filepath, str):400raise ValueError("filepath must be a string if provided")401402return cls(403context=nav_info.context,404navigation=nav_info.navigation,405timestamp=nav_info.timestamp,406url=nav_info.url,407status=status,408filepath=filepath,409)410411412class DownloadEndParams:413"""Parameters for the downloadEnd event."""414415def __init__(416self,417download_params: DownloadCanceledParams | DownloadCompleteParams,418):419self.download_params = download_params420421@classmethod422def from_json(cls, json: dict) -> "DownloadEndParams":423status = json.get("status")424if status == "canceled":425return cls(DownloadCanceledParams.from_json(json))426elif status == "complete":427return cls(DownloadCompleteParams.from_json(json))428else:429raise ValueError("status must be either 'canceled' or 'complete'")430431432class ContextCreated:433"""Event class for browsingContext.contextCreated event."""434435event_class = "browsingContext.contextCreated"436437@classmethod438def from_json(cls, json: dict):439if isinstance(json, BrowsingContextInfo):440return json441return BrowsingContextInfo.from_json(json)442443444class ContextDestroyed:445"""Event class for browsingContext.contextDestroyed event."""446447event_class = "browsingContext.contextDestroyed"448449@classmethod450def from_json(cls, json: dict):451if isinstance(json, BrowsingContextInfo):452return json453return BrowsingContextInfo.from_json(json)454455456class NavigationStarted:457"""Event class for browsingContext.navigationStarted event."""458459event_class = "browsingContext.navigationStarted"460461@classmethod462def from_json(cls, json: dict):463if isinstance(json, NavigationInfo):464return json465return NavigationInfo.from_json(json)466467468class NavigationCommitted:469"""Event class for browsingContext.navigationCommitted event."""470471event_class = "browsingContext.navigationCommitted"472473@classmethod474def from_json(cls, json: dict):475if isinstance(json, NavigationInfo):476return json477return NavigationInfo.from_json(json)478479480class NavigationFailed:481"""Event class for browsingContext.navigationFailed event."""482483event_class = "browsingContext.navigationFailed"484485@classmethod486def from_json(cls, json: dict):487if isinstance(json, NavigationInfo):488return json489return NavigationInfo.from_json(json)490491492class NavigationAborted:493"""Event class for browsingContext.navigationAborted event."""494495event_class = "browsingContext.navigationAborted"496497@classmethod498def from_json(cls, json: dict):499if isinstance(json, NavigationInfo):500return json501return NavigationInfo.from_json(json)502503504class DomContentLoaded:505"""Event class for browsingContext.domContentLoaded event."""506507event_class = "browsingContext.domContentLoaded"508509@classmethod510def from_json(cls, json: dict):511if isinstance(json, NavigationInfo):512return json513return NavigationInfo.from_json(json)514515516class Load:517"""Event class for browsingContext.load event."""518519event_class = "browsingContext.load"520521@classmethod522def from_json(cls, json: dict):523if isinstance(json, NavigationInfo):524return json525return NavigationInfo.from_json(json)526527528class FragmentNavigated:529"""Event class for browsingContext.fragmentNavigated event."""530531event_class = "browsingContext.fragmentNavigated"532533@classmethod534def from_json(cls, json: dict):535if isinstance(json, NavigationInfo):536return json537return NavigationInfo.from_json(json)538539540class DownloadWillBegin:541"""Event class for browsingContext.downloadWillBegin event."""542543event_class = "browsingContext.downloadWillBegin"544545@classmethod546def from_json(cls, json: dict):547return DownloadWillBeginParams.from_json(json)548549550class UserPromptOpened:551"""Event class for browsingContext.userPromptOpened event."""552553event_class = "browsingContext.userPromptOpened"554555@classmethod556def from_json(cls, json: dict):557return UserPromptOpenedParams.from_json(json)558559560class UserPromptClosed:561"""Event class for browsingContext.userPromptClosed event."""562563event_class = "browsingContext.userPromptClosed"564565@classmethod566def from_json(cls, json: dict):567return UserPromptClosedParams.from_json(json)568569570class HistoryUpdated:571"""Event class for browsingContext.historyUpdated event."""572573event_class = "browsingContext.historyUpdated"574575@classmethod576def from_json(cls, json: dict):577return HistoryUpdatedParams.from_json(json)578579580class DownloadEnd:581"""Event class for browsingContext.downloadEnd event."""582583event_class = "browsingContext.downloadEnd"584585@classmethod586def from_json(cls, json: dict):587return DownloadEndParams.from_json(json)588589590@dataclass591class EventConfig:592event_key: str593bidi_event: str594event_class: type595596597class _EventManager:598"""Class to manage event subscriptions and callbacks for BrowsingContext."""599600def __init__(self, conn, event_configs: dict[str, EventConfig]):601self.conn = conn602self.event_configs = event_configs603self.subscriptions: dict = {}604self._bidi_to_class = {config.bidi_event: config.event_class for config in event_configs.values()}605self._available_events = ", ".join(sorted(event_configs.keys()))606# Thread safety lock for subscription operations607self._subscription_lock = threading.Lock()608609def validate_event(self, event: str) -> EventConfig:610event_config = self.event_configs.get(event)611if not event_config:612raise ValueError(f"Event '{event}' not found. Available events: {self._available_events}")613return event_config614615def subscribe_to_event(self, bidi_event: str, contexts: list[str] | None = None) -> None:616"""Subscribe to a BiDi event if not already subscribed.617618Args:619bidi_event: The BiDi event name.620contexts: Optional browsing context IDs to subscribe to.621"""622with self._subscription_lock:623if bidi_event not in self.subscriptions:624session = Session(self.conn)625self.conn.execute(session.subscribe(bidi_event, browsing_contexts=contexts))626self.subscriptions[bidi_event] = []627628def unsubscribe_from_event(self, bidi_event: str) -> None:629"""Unsubscribe from a BiDi event if no more callbacks exist.630631Args:632bidi_event: The BiDi event name.633"""634with self._subscription_lock:635callback_list = self.subscriptions.get(bidi_event)636if callback_list is not None and not callback_list:637session = Session(self.conn)638self.conn.execute(session.unsubscribe(bidi_event))639del self.subscriptions[bidi_event]640641def add_callback_to_tracking(self, bidi_event: str, callback_id: int) -> None:642with self._subscription_lock:643self.subscriptions[bidi_event].append(callback_id)644645def remove_callback_from_tracking(self, bidi_event: str, callback_id: int) -> None:646with self._subscription_lock:647callback_list = self.subscriptions.get(bidi_event)648if callback_list and callback_id in callback_list:649callback_list.remove(callback_id)650651def add_event_handler(self, event: str, callback: Callable, contexts: list[str] | None = None) -> int:652event_config = self.validate_event(event)653654callback_id = self.conn.add_callback(event_config.event_class, callback)655656# Subscribe to the event if needed657self.subscribe_to_event(event_config.bidi_event, contexts)658659# Track the callback660self.add_callback_to_tracking(event_config.bidi_event, callback_id)661662return callback_id663664def remove_event_handler(self, event: str, callback_id: int) -> None:665event_config = self.validate_event(event)666667# Remove the callback from the connection668self.conn.remove_callback(event_config.event_class, callback_id)669670# Remove from tracking collections671self.remove_callback_from_tracking(event_config.bidi_event, callback_id)672673# Unsubscribe if no more callbacks exist674self.unsubscribe_from_event(event_config.bidi_event)675676def clear_event_handlers(self) -> None:677"""Clear all event handlers from the browsing context."""678with self._subscription_lock:679if not self.subscriptions:680return681682session = Session(self.conn)683684for bidi_event, callback_ids in list(self.subscriptions.items()):685event_class = self._bidi_to_class.get(bidi_event)686if event_class:687# Remove all callbacks for this event688for callback_id in callback_ids:689self.conn.remove_callback(event_class, callback_id)690691self.conn.execute(session.unsubscribe(bidi_event))692693self.subscriptions.clear()694695696class BrowsingContext:697"""BiDi implementation of the browsingContext module."""698699EVENT_CONFIGS = {700"context_created": EventConfig("context_created", "browsingContext.contextCreated", ContextCreated),701"context_destroyed": EventConfig("context_destroyed", "browsingContext.contextDestroyed", ContextDestroyed),702"dom_content_loaded": EventConfig("dom_content_loaded", "browsingContext.domContentLoaded", DomContentLoaded),703"download_end": EventConfig("download_end", "browsingContext.downloadEnd", DownloadEnd),704"download_will_begin": EventConfig(705"download_will_begin", "browsingContext.downloadWillBegin", DownloadWillBegin706),707"fragment_navigated": EventConfig("fragment_navigated", "browsingContext.fragmentNavigated", FragmentNavigated),708"history_updated": EventConfig("history_updated", "browsingContext.historyUpdated", HistoryUpdated),709"load": EventConfig("load", "browsingContext.load", Load),710"navigation_aborted": EventConfig("navigation_aborted", "browsingContext.navigationAborted", NavigationAborted),711"navigation_committed": EventConfig(712"navigation_committed", "browsingContext.navigationCommitted", NavigationCommitted713),714"navigation_failed": EventConfig("navigation_failed", "browsingContext.navigationFailed", NavigationFailed),715"navigation_started": EventConfig("navigation_started", "browsingContext.navigationStarted", NavigationStarted),716"user_prompt_closed": EventConfig("user_prompt_closed", "browsingContext.userPromptClosed", UserPromptClosed),717"user_prompt_opened": EventConfig("user_prompt_opened", "browsingContext.userPromptOpened", UserPromptOpened),718}719720def __init__(self, conn):721self.conn = conn722self._event_manager = _EventManager(conn, self.EVENT_CONFIGS)723724@classmethod725def get_event_names(cls) -> list[str]:726"""Get a list of all available event names.727728Returns:729A list of event names that can be used with event handlers.730"""731return list(cls.EVENT_CONFIGS.keys())732733def activate(self, context: str) -> None:734"""Activates and focuses the given top-level traversable.735736Args:737context: The browsing context ID to activate.738739Raises:740Exception: If the browsing context is not a top-level traversable.741"""742params = {"context": context}743self.conn.execute(command_builder("browsingContext.activate", params))744745def capture_screenshot(746self,747context: str,748origin: str = "viewport",749format: dict | None = None,750clip: dict | None = None,751) -> str:752"""Captures an image of the given navigable, and returns it as a Base64-encoded string.753754Args:755context: The browsing context ID to capture.756origin: The origin of the screenshot, either "viewport" or "document".757format: The format of the screenshot.758clip: The clip rectangle of the screenshot.759760Returns:761The Base64-encoded screenshot.762"""763params: dict[str, Any] = {"context": context, "origin": origin}764if format is not None:765params["format"] = format766if clip is not None:767params["clip"] = clip768769result = self.conn.execute(command_builder("browsingContext.captureScreenshot", params))770return result["data"]771772def close(self, context: str, prompt_unload: bool = False) -> None:773"""Closes a top-level traversable.774775Args:776context: The browsing context ID to close.777prompt_unload: Whether to prompt to unload.778779Raises:780Exception: If the browsing context is not a top-level traversable.781"""782params = {"context": context, "promptUnload": prompt_unload}783self.conn.execute(command_builder("browsingContext.close", params))784785def create(786self,787type: str,788reference_context: str | None = None,789background: bool = False,790user_context: str | None = None,791) -> str:792"""Creates a new navigable, either in a new tab or in a new window, and returns its navigable id.793794Args:795type: The type of the new navigable, either "tab" or "window".796reference_context: The reference browsing context ID.797background: Whether to create the new navigable in the background.798user_context: The user context ID.799800Returns:801The browsing context ID of the created navigable.802"""803params: dict[str, Any] = {"type": type}804if reference_context is not None:805params["referenceContext"] = reference_context806if background is not None:807params["background"] = background808if user_context is not None:809params["userContext"] = user_context810811result = self.conn.execute(command_builder("browsingContext.create", params))812return result["context"]813814def get_tree(815self,816max_depth: int | None = None,817root: str | None = None,818) -> list[BrowsingContextInfo]:819"""Get a tree of all descendent navigables including the given parent itself.820821Returns a tree of all descendent navigables including the given parent itself, or all top-level contexts822when no parent is provided.823824Args:825max_depth: The maximum depth of the tree.826root: The root browsing context ID.827828Returns:829A list of browsing context information.830"""831params: dict[str, Any] = {}832if max_depth is not None:833params["maxDepth"] = max_depth834if root is not None:835params["root"] = root836837result = self.conn.execute(command_builder("browsingContext.getTree", params))838return [BrowsingContextInfo.from_json(context) for context in result["contexts"]]839840def handle_user_prompt(841self,842context: str,843accept: bool | None = None,844user_text: str | None = None,845) -> None:846"""Allows closing an open prompt.847848Args:849context: The browsing context ID.850accept: Whether to accept the prompt.851user_text: The text to enter in the prompt.852"""853params: dict[str, Any] = {"context": context}854if accept is not None:855params["accept"] = accept856if user_text is not None:857params["userText"] = user_text858859self.conn.execute(command_builder("browsingContext.handleUserPrompt", params))860861def locate_nodes(862self,863context: str,864locator: dict,865max_node_count: int | None = None,866serialization_options: dict | None = None,867start_nodes: list[dict] | None = None,868) -> list[dict]:869"""Returns a list of all nodes matching the specified locator.870871Args:872context: The browsing context ID.873locator: The locator to use.874max_node_count: The maximum number of nodes to return.875serialization_options: The serialization options.876start_nodes: The start nodes.877878Returns:879A list of nodes.880"""881params: dict[str, Any] = {"context": context, "locator": locator}882if max_node_count is not None:883params["maxNodeCount"] = max_node_count884if serialization_options is not None:885params["serializationOptions"] = serialization_options886if start_nodes is not None:887params["startNodes"] = start_nodes888889result = self.conn.execute(command_builder("browsingContext.locateNodes", params))890return result["nodes"]891892def navigate(893self,894context: str,895url: str,896wait: str | None = None,897) -> dict:898"""Navigates a navigable to the given URL.899900Args:901context: The browsing context ID.902url: The URL to navigate to.903wait: The readiness state to wait for.904905Returns:906A dictionary containing the navigation result.907"""908params = {"context": context, "url": url}909if wait is not None:910params["wait"] = wait911912result = self.conn.execute(command_builder("browsingContext.navigate", params))913return result914915def print(916self,917context: str,918background: bool = False,919margin: dict | None = None,920orientation: str = "portrait",921page: dict | None = None,922page_ranges: list[int | str] | None = None,923scale: float = 1.0,924shrink_to_fit: bool = True,925) -> str:926"""Create a paginated PDF representation of the document as a Base64-encoded string.927928Args:929context: The browsing context ID.930background: Whether to include the background.931margin: The margin parameters.932orientation: The orientation, either "portrait" or "landscape".933page: The page parameters.934page_ranges: The page ranges.935scale: The scale.936shrink_to_fit: Whether to shrink to fit.937938Returns:939The Base64-encoded PDF document.940"""941params = {942"context": context,943"background": background,944"orientation": orientation,945"scale": scale,946"shrinkToFit": shrink_to_fit,947}948if margin is not None:949params["margin"] = margin950if page is not None:951params["page"] = page952if page_ranges is not None:953params["pageRanges"] = page_ranges954955result = self.conn.execute(command_builder("browsingContext.print", params))956return result["data"]957958def reload(959self,960context: str,961ignore_cache: bool | None = None,962wait: str | None = None,963) -> dict:964"""Reloads a navigable.965966Args:967context: The browsing context ID.968ignore_cache: Whether to ignore the cache.969wait: The readiness state to wait for.970971Returns:972A dictionary containing the navigation result.973"""974params: dict[str, Any] = {"context": context}975if ignore_cache is not None:976params["ignoreCache"] = ignore_cache977if wait is not None:978params["wait"] = wait979980result = self.conn.execute(command_builder("browsingContext.reload", params))981return result982983def set_viewport(984self,985context: str | None = None,986viewport: dict | None | Sentinel = UNDEFINED,987device_pixel_ratio: float | None | Sentinel = UNDEFINED,988user_contexts: list[str] | None = None,989) -> None:990"""Modifies specific viewport characteristics on the given top-level traversable.991992Args:993context: The browsing context ID.994viewport: The viewport parameters - {"width": <int>, "height": <int>} (`None` resets to default).995device_pixel_ratio: The device pixel ratio (`None` resets to default).996user_contexts: The user context IDs.997998Raises:999Exception: If the browsing context is not a top-level traversable1000ValueError: If neither `context` nor `user_contexts` is provided1001ValueError: If both `context` and `user_contexts` are provided1002"""1003if context is not None and user_contexts is not None:1004raise ValueError("Cannot specify both context and user_contexts")10051006if context is None and user_contexts is None:1007raise ValueError("Must specify either context or user_contexts")10081009params: dict[str, Any] = {}1010if context is not None:1011params["context"] = context1012elif user_contexts is not None:1013params["userContexts"] = user_contexts1014if viewport is not UNDEFINED:1015params["viewport"] = viewport1016if device_pixel_ratio is not UNDEFINED:1017params["devicePixelRatio"] = device_pixel_ratio10181019self.conn.execute(command_builder("browsingContext.setViewport", params))10201021def traverse_history(self, context: str, delta: int) -> dict:1022"""Traverses the history of a given navigable by a delta.10231024Args:1025context: The browsing context ID.1026delta: The delta to traverse by.10271028Returns:1029A dictionary containing the traverse history result.1030"""1031params = {"context": context, "delta": delta}1032result = self.conn.execute(command_builder("browsingContext.traverseHistory", params))1033return result10341035def add_event_handler(self, event: str, callback: Callable, contexts: list[str] | None = None) -> int:1036"""Add an event handler to the browsing context.10371038Args:1039event: The event to subscribe to.1040callback: The callback function to execute on event.1041contexts: The browsing context IDs to subscribe to.10421043Returns:1044Callback id.1045"""1046return self._event_manager.add_event_handler(event, callback, contexts)10471048def remove_event_handler(self, event: str, callback_id: int) -> None:1049"""Remove an event handler from the browsing context.10501051Args:1052event: The event to unsubscribe from.1053callback_id: The callback id to remove.1054"""1055self._event_manager.remove_event_handler(event, callback_id)10561057def clear_event_handlers(self) -> None:1058"""Clear all event handlers from the browsing context."""1059self._event_manager.clear_event_handlers()106010611062