blob: 9cd2913d80dc395e50c9fe535f1f5fc4b47fcf31 [file]
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import threading
from dataclasses import dataclass
from typing import Any, Callable, Optional, Union
from selenium.webdriver.common.bidi.common import command_builder
from .session import Session
class ReadinessState:
"""Represents the stage of document loading at which a navigation command will return."""
NONE = "none"
INTERACTIVE = "interactive"
COMPLETE = "complete"
class UserPromptType:
"""Represents the possible user prompt types."""
ALERT = "alert"
BEFORE_UNLOAD = "beforeunload"
CONFIRM = "confirm"
PROMPT = "prompt"
class NavigationInfo:
"""Provides details of an ongoing navigation."""
def __init__(
self,
context: str,
navigation: Optional[str],
timestamp: int,
url: str,
):
self.context = context
self.navigation = navigation
self.timestamp = timestamp
self.url = url
@classmethod
def from_json(cls, json: dict) -> "NavigationInfo":
"""Creates a NavigationInfo instance from a dictionary.
Parameters:
-----------
json: A dictionary containing the navigation information.
Returns:
-------
NavigationInfo: A new instance of NavigationInfo.
"""
context = json.get("context")
if context is None or not isinstance(context, str):
raise ValueError("context is required and must be a string")
navigation = json.get("navigation")
if navigation is not None and not isinstance(navigation, str):
raise ValueError("navigation must be a string")
timestamp = json.get("timestamp")
if timestamp is None or not isinstance(timestamp, int) or timestamp < 0:
raise ValueError("timestamp is required and must be a non-negative integer")
url = json.get("url")
if url is None or not isinstance(url, str):
raise ValueError("url is required and must be a string")
return cls(context, navigation, timestamp, url)
class BrowsingContextInfo:
"""Represents the properties of a navigable."""
def __init__(
self,
context: str,
url: str,
children: Optional[list["BrowsingContextInfo"]],
client_window: str,
user_context: str,
parent: Optional[str] = None,
original_opener: Optional[str] = None,
):
self.context = context
self.url = url
self.children = children
self.parent = parent
self.user_context = user_context
self.original_opener = original_opener
self.client_window = client_window
@classmethod
def from_json(cls, json: dict) -> "BrowsingContextInfo":
"""Creates a BrowsingContextInfo instance from a dictionary.
Parameters:
-----------
json: A dictionary containing the browsing context information.
Returns:
-------
BrowsingContextInfo: A new instance of BrowsingContextInfo.
"""
children = None
raw_children = json.get("children")
if raw_children is not None:
if not isinstance(raw_children, list):
raise ValueError("children must be a list if provided")
children = []
for child in raw_children:
if not isinstance(child, dict):
raise ValueError(f"Each child must be a dictionary, got {type(child)}")
children.append(BrowsingContextInfo.from_json(child))
context = json.get("context")
if context is None or not isinstance(context, str):
raise ValueError("context is required and must be a string")
url = json.get("url")
if url is None or not isinstance(url, str):
raise ValueError("url is required and must be a string")
parent = json.get("parent")
if parent is not None and not isinstance(parent, str):
raise ValueError("parent must be a string if provided")
user_context = json.get("userContext")
if user_context is None or not isinstance(user_context, str):
raise ValueError("userContext is required and must be a string")
original_opener = json.get("originalOpener")
if original_opener is not None and not isinstance(original_opener, str):
raise ValueError("originalOpener must be a string if provided")
client_window = json.get("clientWindow")
if client_window is None or not isinstance(client_window, str):
raise ValueError("clientWindow is required and must be a string")
return cls(
context=context,
url=url,
children=children,
client_window=client_window,
user_context=user_context,
parent=parent,
original_opener=original_opener,
)
class DownloadWillBeginParams(NavigationInfo):
"""Parameters for the downloadWillBegin event."""
def __init__(
self,
context: str,
navigation: Optional[str],
timestamp: int,
url: str,
suggested_filename: str,
):
super().__init__(context, navigation, timestamp, url)
self.suggested_filename = suggested_filename
@classmethod
def from_json(cls, json: dict) -> "DownloadWillBeginParams":
"""Creates a DownloadWillBeginParams instance from a dictionary.
Parameters:
-----------
json: A dictionary containing the download parameters.
Returns:
-------
DownloadWillBeginParams: A new instance of DownloadWillBeginParams.
"""
context = json.get("context")
if context is None or not isinstance(context, str):
raise ValueError("context is required and must be a string")
navigation = json.get("navigation")
if navigation is not None and not isinstance(navigation, str):
raise ValueError("navigation must be a string")
timestamp = json.get("timestamp")
if timestamp is None or not isinstance(timestamp, int) or timestamp < 0:
raise ValueError("timestamp is required and must be a non-negative integer")
url = json.get("url")
if url is None or not isinstance(url, str):
raise ValueError("url is required and must be a string")
suggested_filename = json.get("suggestedFilename")
if suggested_filename is None or not isinstance(suggested_filename, str):
raise ValueError("suggestedFilename is required and must be a string")
return cls(
context=context,
navigation=navigation,
timestamp=timestamp,
url=url,
suggested_filename=suggested_filename,
)
class UserPromptOpenedParams:
"""Parameters for the userPromptOpened event."""
def __init__(
self,
context: str,
handler: str,
message: str,
type: str,
default_value: Optional[str] = None,
):
self.context = context
self.handler = handler
self.message = message
self.type = type
self.default_value = default_value
@classmethod
def from_json(cls, json: dict) -> "UserPromptOpenedParams":
"""Creates a UserPromptOpenedParams instance from a dictionary.
Parameters:
-----------
json: A dictionary containing the user prompt parameters.
Returns:
-------
UserPromptOpenedParams: A new instance of UserPromptOpenedParams.
"""
context = json.get("context")
if context is None or not isinstance(context, str):
raise ValueError("context is required and must be a string")
handler = json.get("handler")
if handler is None or not isinstance(handler, str):
raise ValueError("handler is required and must be a string")
message = json.get("message")
if message is None or not isinstance(message, str):
raise ValueError("message is required and must be a string")
type_value = json.get("type")
if type_value is None or not isinstance(type_value, str):
raise ValueError("type is required and must be a string")
default_value = json.get("defaultValue")
if default_value is not None and not isinstance(default_value, str):
raise ValueError("defaultValue must be a string if provided")
return cls(
context=context,
handler=handler,
message=message,
type=type_value,
default_value=default_value,
)
class UserPromptClosedParams:
"""Parameters for the userPromptClosed event."""
def __init__(
self,
context: str,
accepted: bool,
type: str,
user_text: Optional[str] = None,
):
self.context = context
self.accepted = accepted
self.type = type
self.user_text = user_text
@classmethod
def from_json(cls, json: dict) -> "UserPromptClosedParams":
"""Creates a UserPromptClosedParams instance from a dictionary.
Parameters:
-----------
json: A dictionary containing the user prompt closed parameters.
Returns:
-------
UserPromptClosedParams: A new instance of UserPromptClosedParams.
"""
context = json.get("context")
if context is None or not isinstance(context, str):
raise ValueError("context is required and must be a string")
accepted = json.get("accepted")
if accepted is None or not isinstance(accepted, bool):
raise ValueError("accepted is required and must be a boolean")
type_value = json.get("type")
if type_value is None or not isinstance(type_value, str):
raise ValueError("type is required and must be a string")
user_text = json.get("userText")
if user_text is not None and not isinstance(user_text, str):
raise ValueError("userText must be a string if provided")
return cls(
context=context,
accepted=accepted,
type=type_value,
user_text=user_text,
)
class HistoryUpdatedParams:
"""Parameters for the historyUpdated event."""
def __init__(
self,
context: str,
timestamp: int,
url: str,
):
self.context = context
self.timestamp = timestamp
self.url = url
@classmethod
def from_json(cls, json: dict) -> "HistoryUpdatedParams":
"""Creates a HistoryUpdatedParams instance from a dictionary.
Parameters:
-----------
json: A dictionary containing the history updated parameters.
Returns:
-------
HistoryUpdatedParams: A new instance of HistoryUpdatedParams.
"""
context = json.get("context")
if context is None or not isinstance(context, str):
raise ValueError("context is required and must be a string")
timestamp = json.get("timestamp")
if timestamp is None or not isinstance(timestamp, int) or timestamp < 0:
raise ValueError("timestamp is required and must be a non-negative integer")
url = json.get("url")
if url is None or not isinstance(url, str):
raise ValueError("url is required and must be a string")
return cls(
context=context,
timestamp=timestamp,
url=url,
)
class ContextCreated:
"""Event class for browsingContext.contextCreated event."""
event_class = "browsingContext.contextCreated"
@classmethod
def from_json(cls, json: dict):
if isinstance(json, BrowsingContextInfo):
return json
return BrowsingContextInfo.from_json(json)
class ContextDestroyed:
"""Event class for browsingContext.contextDestroyed event."""
event_class = "browsingContext.contextDestroyed"
@classmethod
def from_json(cls, json: dict):
if isinstance(json, BrowsingContextInfo):
return json
return BrowsingContextInfo.from_json(json)
class NavigationStarted:
"""Event class for browsingContext.navigationStarted event."""
event_class = "browsingContext.navigationStarted"
@classmethod
def from_json(cls, json: dict):
if isinstance(json, NavigationInfo):
return json
return NavigationInfo.from_json(json)
class NavigationCommitted:
"""Event class for browsingContext.navigationCommitted event."""
event_class = "browsingContext.navigationCommitted"
@classmethod
def from_json(cls, json: dict):
if isinstance(json, NavigationInfo):
return json
return NavigationInfo.from_json(json)
class NavigationFailed:
"""Event class for browsingContext.navigationFailed event."""
event_class = "browsingContext.navigationFailed"
@classmethod
def from_json(cls, json: dict):
if isinstance(json, NavigationInfo):
return json
return NavigationInfo.from_json(json)
class NavigationAborted:
"""Event class for browsingContext.navigationAborted event."""
event_class = "browsingContext.navigationAborted"
@classmethod
def from_json(cls, json: dict):
if isinstance(json, NavigationInfo):
return json
return NavigationInfo.from_json(json)
class DomContentLoaded:
"""Event class for browsingContext.domContentLoaded event."""
event_class = "browsingContext.domContentLoaded"
@classmethod
def from_json(cls, json: dict):
if isinstance(json, NavigationInfo):
return json
return NavigationInfo.from_json(json)
class Load:
"""Event class for browsingContext.load event."""
event_class = "browsingContext.load"
@classmethod
def from_json(cls, json: dict):
if isinstance(json, NavigationInfo):
return json
return NavigationInfo.from_json(json)
class FragmentNavigated:
"""Event class for browsingContext.fragmentNavigated event."""
event_class = "browsingContext.fragmentNavigated"
@classmethod
def from_json(cls, json: dict):
if isinstance(json, NavigationInfo):
return json
return NavigationInfo.from_json(json)
class DownloadWillBegin:
"""Event class for browsingContext.downloadWillBegin event."""
event_class = "browsingContext.downloadWillBegin"
@classmethod
def from_json(cls, json: dict):
return DownloadWillBeginParams.from_json(json)
class UserPromptOpened:
"""Event class for browsingContext.userPromptOpened event."""
event_class = "browsingContext.userPromptOpened"
@classmethod
def from_json(cls, json: dict):
return UserPromptOpenedParams.from_json(json)
class UserPromptClosed:
"""Event class for browsingContext.userPromptClosed event."""
event_class = "browsingContext.userPromptClosed"
@classmethod
def from_json(cls, json: dict):
return UserPromptClosedParams.from_json(json)
class HistoryUpdated:
"""Event class for browsingContext.historyUpdated event."""
event_class = "browsingContext.historyUpdated"
@classmethod
def from_json(cls, json: dict):
return HistoryUpdatedParams.from_json(json)
@dataclass
class EventConfig:
event_key: str
bidi_event: str
event_class: type
class _EventManager:
"""Class to manage event subscriptions and callbacks for BrowsingContext."""
def __init__(self, conn, event_configs: dict[str, EventConfig]):
self.conn = conn
self.event_configs = event_configs
self.subscriptions: dict = {}
self._bidi_to_class = {config.bidi_event: config.event_class for config in event_configs.values()}
self._available_events = ", ".join(sorted(event_configs.keys()))
# Thread safety lock for subscription operations
self._subscription_lock = threading.Lock()
def validate_event(self, event: str) -> EventConfig:
event_config = self.event_configs.get(event)
if not event_config:
raise ValueError(f"Event '{event}' not found. Available events: {self._available_events}")
return event_config
def subscribe_to_event(self, bidi_event: str, contexts: Optional[list[str]] = None) -> None:
"""Subscribe to a BiDi event if not already subscribed.
Parameters:
----------
bidi_event: The BiDi event name.
contexts: Optional browsing context IDs to subscribe to.
"""
with self._subscription_lock:
if bidi_event not in self.subscriptions:
session = Session(self.conn)
self.conn.execute(session.subscribe(bidi_event, browsing_contexts=contexts))
self.subscriptions[bidi_event] = []
def unsubscribe_from_event(self, bidi_event: str) -> None:
"""Unsubscribe from a BiDi event if no more callbacks exist.
Parameters:
----------
bidi_event: The BiDi event name.
"""
with self._subscription_lock:
callback_list = self.subscriptions.get(bidi_event)
if callback_list is not None and not callback_list:
session = Session(self.conn)
self.conn.execute(session.unsubscribe(bidi_event))
del self.subscriptions[bidi_event]
def add_callback_to_tracking(self, bidi_event: str, callback_id: int) -> None:
with self._subscription_lock:
self.subscriptions[bidi_event].append(callback_id)
def remove_callback_from_tracking(self, bidi_event: str, callback_id: int) -> None:
with self._subscription_lock:
callback_list = self.subscriptions.get(bidi_event)
if callback_list and callback_id in callback_list:
callback_list.remove(callback_id)
def add_event_handler(self, event: str, callback: Callable, contexts: Optional[list[str]] = None) -> int:
event_config = self.validate_event(event)
callback_id = self.conn.add_callback(event_config.event_class, callback)
# Subscribe to the event if needed
self.subscribe_to_event(event_config.bidi_event, contexts)
# Track the callback
self.add_callback_to_tracking(event_config.bidi_event, callback_id)
return callback_id
def remove_event_handler(self, event: str, callback_id: int) -> None:
event_config = self.validate_event(event)
# Remove the callback from the connection
self.conn.remove_callback(event_config.event_class, callback_id)
# Remove from tracking collections
self.remove_callback_from_tracking(event_config.bidi_event, callback_id)
# Unsubscribe if no more callbacks exist
self.unsubscribe_from_event(event_config.bidi_event)
def clear_event_handlers(self) -> None:
"""Clear all event handlers from the browsing context."""
with self._subscription_lock:
if not self.subscriptions:
return
session = Session(self.conn)
for bidi_event, callback_ids in list(self.subscriptions.items()):
event_class = self._bidi_to_class.get(bidi_event)
if event_class:
# Remove all callbacks for this event
for callback_id in callback_ids:
self.conn.remove_callback(event_class, callback_id)
self.conn.execute(session.unsubscribe(bidi_event))
self.subscriptions.clear()
class BrowsingContext:
"""BiDi implementation of the browsingContext module."""
EVENT_CONFIGS = {
"context_created": EventConfig("context_created", "browsingContext.contextCreated", ContextCreated),
"context_destroyed": EventConfig("context_destroyed", "browsingContext.contextDestroyed", ContextDestroyed),
"dom_content_loaded": EventConfig("dom_content_loaded", "browsingContext.domContentLoaded", DomContentLoaded),
"download_will_begin": EventConfig(
"download_will_begin", "browsingContext.downloadWillBegin", DownloadWillBegin
),
"fragment_navigated": EventConfig("fragment_navigated", "browsingContext.fragmentNavigated", FragmentNavigated),
"history_updated": EventConfig("history_updated", "browsingContext.historyUpdated", HistoryUpdated),
"load": EventConfig("load", "browsingContext.load", Load),
"navigation_aborted": EventConfig("navigation_aborted", "browsingContext.navigationAborted", NavigationAborted),
"navigation_committed": EventConfig(
"navigation_committed", "browsingContext.navigationCommitted", NavigationCommitted
),
"navigation_failed": EventConfig("navigation_failed", "browsingContext.navigationFailed", NavigationFailed),
"navigation_started": EventConfig("navigation_started", "browsingContext.navigationStarted", NavigationStarted),
"user_prompt_closed": EventConfig("user_prompt_closed", "browsingContext.userPromptClosed", UserPromptClosed),
"user_prompt_opened": EventConfig("user_prompt_opened", "browsingContext.userPromptOpened", UserPromptOpened),
}
def __init__(self, conn):
self.conn = conn
self._event_manager = _EventManager(conn, self.EVENT_CONFIGS)
@classmethod
def get_event_names(cls) -> list[str]:
"""Get a list of all available event names.
Returns:
-------
List[str]: A list of event names that can be used with event handlers.
"""
return list(cls.EVENT_CONFIGS.keys())
def activate(self, context: str) -> None:
"""Activates and focuses the given top-level traversable.
Parameters:
-----------
context: The browsing context ID to activate.
Raises:
------
Exception: If the browsing context is not a top-level traversable.
"""
params = {"context": context}
self.conn.execute(command_builder("browsingContext.activate", params))
def capture_screenshot(
self,
context: str,
origin: str = "viewport",
format: Optional[dict] = None,
clip: Optional[dict] = None,
) -> str:
"""Captures an image of the given navigable, and returns it as a Base64-encoded string.
Parameters:
-----------
context: The browsing context ID to capture.
origin: The origin of the screenshot, either "viewport" or "document".
format: The format of the screenshot.
clip: The clip rectangle of the screenshot.
Returns:
-------
str: The Base64-encoded screenshot.
"""
params: dict[str, Any] = {"context": context, "origin": origin}
if format is not None:
params["format"] = format
if clip is not None:
params["clip"] = clip
result = self.conn.execute(command_builder("browsingContext.captureScreenshot", params))
return result["data"]
def close(self, context: str, prompt_unload: bool = False) -> None:
"""Closes a top-level traversable.
Parameters:
-----------
context: The browsing context ID to close.
prompt_unload: Whether to prompt to unload.
Raises:
------
Exception: If the browsing context is not a top-level traversable.
"""
params = {"context": context, "promptUnload": prompt_unload}
self.conn.execute(command_builder("browsingContext.close", params))
def create(
self,
type: str,
reference_context: Optional[str] = None,
background: bool = False,
user_context: Optional[str] = None,
) -> str:
"""Creates a new navigable, either in a new tab or in a new window, and returns its navigable id.
Parameters:
-----------
type: The type of the new navigable, either "tab" or "window".
reference_context: The reference browsing context ID.
background: Whether to create the new navigable in the background.
user_context: The user context ID.
Returns:
-------
str: The browsing context ID of the created navigable.
"""
params: dict[str, Any] = {"type": type}
if reference_context is not None:
params["referenceContext"] = reference_context
if background is not None:
params["background"] = background
if user_context is not None:
params["userContext"] = user_context
result = self.conn.execute(command_builder("browsingContext.create", params))
return result["context"]
def get_tree(
self,
max_depth: Optional[int] = None,
root: Optional[str] = None,
) -> list[BrowsingContextInfo]:
"""Returns a tree of all descendent navigables including the given parent itself, or all top-level contexts
when no parent is provided.
Parameters:
-----------
max_depth: The maximum depth of the tree.
root: The root browsing context ID.
Returns:
-------
List[BrowsingContextInfo]: A list of browsing context information.
"""
params: dict[str, Any] = {}
if max_depth is not None:
params["maxDepth"] = max_depth
if root is not None:
params["root"] = root
result = self.conn.execute(command_builder("browsingContext.getTree", params))
return [BrowsingContextInfo.from_json(context) for context in result["contexts"]]
def handle_user_prompt(
self,
context: str,
accept: Optional[bool] = None,
user_text: Optional[str] = None,
) -> None:
"""Allows closing an open prompt.
Parameters:
-----------
context: The browsing context ID.
accept: Whether to accept the prompt.
user_text: The text to enter in the prompt.
"""
params: dict[str, Any] = {"context": context}
if accept is not None:
params["accept"] = accept
if user_text is not None:
params["userText"] = user_text
self.conn.execute(command_builder("browsingContext.handleUserPrompt", params))
def locate_nodes(
self,
context: str,
locator: dict,
max_node_count: Optional[int] = None,
serialization_options: Optional[dict] = None,
start_nodes: Optional[list[dict]] = None,
) -> list[dict]:
"""Returns a list of all nodes matching the specified locator.
Parameters:
-----------
context: The browsing context ID.
locator: The locator to use.
max_node_count: The maximum number of nodes to return.
serialization_options: The serialization options.
start_nodes: The start nodes.
Returns:
-------
List[Dict]: A list of nodes.
"""
params: dict[str, Any] = {"context": context, "locator": locator}
if max_node_count is not None:
params["maxNodeCount"] = max_node_count
if serialization_options is not None:
params["serializationOptions"] = serialization_options
if start_nodes is not None:
params["startNodes"] = start_nodes
result = self.conn.execute(command_builder("browsingContext.locateNodes", params))
return result["nodes"]
def navigate(
self,
context: str,
url: str,
wait: Optional[str] = None,
) -> dict:
"""Navigates a navigable to the given URL.
Parameters:
-----------
context: The browsing context ID.
url: The URL to navigate to.
wait: The readiness state to wait for.
Returns:
-------
Dict: A dictionary containing the navigation result.
"""
params = {"context": context, "url": url}
if wait is not None:
params["wait"] = wait
result = self.conn.execute(command_builder("browsingContext.navigate", params))
return result
def print(
self,
context: str,
background: bool = False,
margin: Optional[dict] = None,
orientation: str = "portrait",
page: Optional[dict] = None,
page_ranges: Optional[list[Union[int, str]]] = None,
scale: float = 1.0,
shrink_to_fit: bool = True,
) -> str:
"""Creates a paginated representation of a document, and returns it as a PDF document represented as a
Base64-encoded string.
Parameters:
-----------
context: The browsing context ID.
background: Whether to include the background.
margin: The margin parameters.
orientation: The orientation, either "portrait" or "landscape".
page: The page parameters.
page_ranges: The page ranges.
scale: The scale.
shrink_to_fit: Whether to shrink to fit.
Returns:
-------
str: The Base64-encoded PDF document.
"""
params = {
"context": context,
"background": background,
"orientation": orientation,
"scale": scale,
"shrinkToFit": shrink_to_fit,
}
if margin is not None:
params["margin"] = margin
if page is not None:
params["page"] = page
if page_ranges is not None:
params["pageRanges"] = page_ranges
result = self.conn.execute(command_builder("browsingContext.print", params))
return result["data"]
def reload(
self,
context: str,
ignore_cache: Optional[bool] = None,
wait: Optional[str] = None,
) -> dict:
"""Reloads a navigable.
Parameters:
-----------
context: The browsing context ID.
ignore_cache: Whether to ignore the cache.
wait: The readiness state to wait for.
Returns:
-------
Dict: A dictionary containing the navigation result.
"""
params: dict[str, Any] = {"context": context}
if ignore_cache is not None:
params["ignoreCache"] = ignore_cache
if wait is not None:
params["wait"] = wait
result = self.conn.execute(command_builder("browsingContext.reload", params))
return result
def set_viewport(
self,
context: Optional[str] = None,
viewport: Optional[dict] = None,
device_pixel_ratio: Optional[float] = None,
user_contexts: Optional[list[str]] = None,
) -> None:
"""Modifies specific viewport characteristics on the given top-level traversable.
Parameters:
-----------
context: The browsing context ID.
viewport: The viewport parameters.
device_pixel_ratio: The device pixel ratio.
user_contexts: The user context IDs.
Raises:
------
Exception: If the browsing context is not a top-level traversable.
"""
params: dict[str, Any] = {}
if context is not None:
params["context"] = context
if viewport is not None:
params["viewport"] = viewport
if device_pixel_ratio is not None:
params["devicePixelRatio"] = device_pixel_ratio
if user_contexts is not None:
params["userContexts"] = user_contexts
self.conn.execute(command_builder("browsingContext.setViewport", params))
def traverse_history(self, context: str, delta: int) -> dict:
"""Traverses the history of a given navigable by a delta.
Parameters:
-----------
context: The browsing context ID.
delta: The delta to traverse by.
Returns:
-------
Dict: A dictionary containing the traverse history result.
"""
params = {"context": context, "delta": delta}
result = self.conn.execute(command_builder("browsingContext.traverseHistory", params))
return result
def add_event_handler(self, event: str, callback: Callable, contexts: Optional[list[str]] = None) -> int:
"""Add an event handler to the browsing context.
Parameters:
----------
event: The event to subscribe to.
callback: The callback function to execute on event.
contexts: The browsing context IDs to subscribe to.
Returns:
-------
int: callback id
"""
return self._event_manager.add_event_handler(event, callback, contexts)
def remove_event_handler(self, event: str, callback_id: int) -> None:
"""Remove an event handler from the browsing context.
Parameters:
----------
event: The event to unsubscribe from.
callback_id: The callback id to remove.
"""
self._event_manager.remove_event_handler(event, callback_id)
def clear_event_handlers(self) -> None:
"""Clear all event handlers from the browsing context."""
self._event_manager.clear_event_handlers()