Path: blob/trunk/py/selenium/webdriver/common/bidi/browsing_context.py
1864 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 dataclasses import dataclass19from typing import Any, Callable, Optional, Union2021from selenium.webdriver.common.bidi.common import command_builder2223from .session import Session242526class ReadinessState:27"""Represents the stage of document loading at which a navigation command will return."""2829NONE = "none"30INTERACTIVE = "interactive"31COMPLETE = "complete"323334class UserPromptType:35"""Represents the possible user prompt types."""3637ALERT = "alert"38BEFORE_UNLOAD = "beforeunload"39CONFIRM = "confirm"40PROMPT = "prompt"414243class NavigationInfo:44"""Provides details of an ongoing navigation."""4546def __init__(47self,48context: str,49navigation: Optional[str],50timestamp: int,51url: str,52):53self.context = context54self.navigation = navigation55self.timestamp = timestamp56self.url = url5758@classmethod59def from_json(cls, json: dict) -> "NavigationInfo":60"""Creates a NavigationInfo instance from a dictionary.6162Parameters:63-----------64json: A dictionary containing the navigation information.6566Returns:67-------68NavigationInfo: A new instance of NavigationInfo.69"""70context = json.get("context")71if context is None or not isinstance(context, str):72raise ValueError("context is required and must be a string")7374navigation = json.get("navigation")75if navigation is not None and not isinstance(navigation, str):76raise ValueError("navigation must be a string")7778timestamp = json.get("timestamp")79if timestamp is None or not isinstance(timestamp, int) or timestamp < 0:80raise ValueError("timestamp is required and must be a non-negative integer")8182url = json.get("url")83if url is None or not isinstance(url, str):84raise ValueError("url is required and must be a string")8586return cls(context, navigation, timestamp, url)878889class BrowsingContextInfo:90"""Represents the properties of a navigable."""9192def __init__(93self,94context: str,95url: str,96children: Optional[list["BrowsingContextInfo"]],97client_window: str,98user_context: str,99parent: Optional[str] = None,100original_opener: Optional[str] = None,101):102self.context = context103self.url = url104self.children = children105self.parent = parent106self.user_context = user_context107self.original_opener = original_opener108self.client_window = client_window109110@classmethod111def from_json(cls, json: dict) -> "BrowsingContextInfo":112"""Creates a BrowsingContextInfo instance from a dictionary.113114Parameters:115-----------116json: A dictionary containing the browsing context information.117118Returns:119-------120BrowsingContextInfo: A 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: Optional[str],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: Optional[str] = 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.220221Parameters:222-----------223json: A dictionary containing the user prompt parameters.224225Returns:226-------227UserPromptOpenedParams: A new instance of UserPromptOpenedParams.228"""229context = json.get("context")230if context is None or not isinstance(context, str):231raise ValueError("context is required and must be a string")232233handler = json.get("handler")234if handler is None or not isinstance(handler, str):235raise ValueError("handler is required and must be a string")236237message = json.get("message")238if message is None or not isinstance(message, str):239raise ValueError("message is required and must be a string")240241type_value = json.get("type")242if type_value is None or not isinstance(type_value, str):243raise ValueError("type is required and must be a string")244245default_value = json.get("defaultValue")246if default_value is not None and not isinstance(default_value, str):247raise ValueError("defaultValue must be a string if provided")248249return cls(250context=context,251handler=handler,252message=message,253type=type_value,254default_value=default_value,255)256257258class UserPromptClosedParams:259"""Parameters for the userPromptClosed event."""260261def __init__(262self,263context: str,264accepted: bool,265type: str,266user_text: Optional[str] = None,267):268self.context = context269self.accepted = accepted270self.type = type271self.user_text = user_text272273@classmethod274def from_json(cls, json: dict) -> "UserPromptClosedParams":275"""Creates a UserPromptClosedParams instance from a dictionary.276277Parameters:278-----------279json: A dictionary containing the user prompt closed parameters.280281Returns:282-------283UserPromptClosedParams: A new instance of UserPromptClosedParams.284"""285context = json.get("context")286if context is None or not isinstance(context, str):287raise ValueError("context is required and must be a string")288289accepted = json.get("accepted")290if accepted is None or not isinstance(accepted, bool):291raise ValueError("accepted is required and must be a boolean")292293type_value = json.get("type")294if type_value is None or not isinstance(type_value, str):295raise ValueError("type is required and must be a string")296297user_text = json.get("userText")298if user_text is not None and not isinstance(user_text, str):299raise ValueError("userText must be a string if provided")300301return cls(302context=context,303accepted=accepted,304type=type_value,305user_text=user_text,306)307308309class HistoryUpdatedParams:310"""Parameters for the historyUpdated event."""311312def __init__(313self,314context: str,315timestamp: int,316url: str,317):318self.context = context319self.timestamp = timestamp320self.url = url321322@classmethod323def from_json(cls, json: dict) -> "HistoryUpdatedParams":324"""Creates a HistoryUpdatedParams instance from a dictionary.325326Parameters:327-----------328json: A dictionary containing the history updated parameters.329330Returns:331-------332HistoryUpdatedParams: A new instance of HistoryUpdatedParams.333"""334context = json.get("context")335if context is None or not isinstance(context, str):336raise ValueError("context is required and must be a string")337338timestamp = json.get("timestamp")339if timestamp is None or not isinstance(timestamp, int) or timestamp < 0:340raise ValueError("timestamp is required and must be a non-negative integer")341342url = json.get("url")343if url is None or not isinstance(url, str):344raise ValueError("url is required and must be a string")345346return cls(347context=context,348timestamp=timestamp,349url=url,350)351352353class DownloadCanceledParams(NavigationInfo):354def __init__(355self,356context: str,357navigation: Optional[str],358timestamp: int,359url: str,360status: str = "canceled",361):362super().__init__(context, navigation, timestamp, url)363self.status = status364365@classmethod366def from_json(cls, json: dict) -> "DownloadCanceledParams":367nav_info = NavigationInfo.from_json(json)368369status = json.get("status")370if status is None or status != "canceled":371raise ValueError("status is required and must be 'canceled'")372373return cls(374context=nav_info.context,375navigation=nav_info.navigation,376timestamp=nav_info.timestamp,377url=nav_info.url,378status=status,379)380381382class DownloadCompleteParams(NavigationInfo):383def __init__(384self,385context: str,386navigation: Optional[str],387timestamp: int,388url: str,389status: str = "complete",390filepath: Optional[str] = None,391):392super().__init__(context, navigation, timestamp, url)393self.status = status394self.filepath = filepath395396@classmethod397def from_json(cls, json: dict) -> "DownloadCompleteParams":398nav_info = NavigationInfo.from_json(json)399400status = json.get("status")401if status is None or status != "complete":402raise ValueError("status is required and must be 'complete'")403404filepath = json.get("filepath")405if filepath is not None and not isinstance(filepath, str):406raise ValueError("filepath must be a string if provided")407408return cls(409context=nav_info.context,410navigation=nav_info.navigation,411timestamp=nav_info.timestamp,412url=nav_info.url,413status=status,414filepath=filepath,415)416417418class DownloadEndParams:419"""Parameters for the downloadEnd event."""420421def __init__(422self,423download_params: Union[DownloadCanceledParams, DownloadCompleteParams],424):425self.download_params = download_params426427@classmethod428def from_json(cls, json: dict) -> "DownloadEndParams":429status = json.get("status")430if status == "canceled":431return cls(DownloadCanceledParams.from_json(json))432elif status == "complete":433return cls(DownloadCompleteParams.from_json(json))434else:435raise ValueError("status must be either 'canceled' or 'complete'")436437438class ContextCreated:439"""Event class for browsingContext.contextCreated event."""440441event_class = "browsingContext.contextCreated"442443@classmethod444def from_json(cls, json: dict):445if isinstance(json, BrowsingContextInfo):446return json447return BrowsingContextInfo.from_json(json)448449450class ContextDestroyed:451"""Event class for browsingContext.contextDestroyed event."""452453event_class = "browsingContext.contextDestroyed"454455@classmethod456def from_json(cls, json: dict):457if isinstance(json, BrowsingContextInfo):458return json459return BrowsingContextInfo.from_json(json)460461462class NavigationStarted:463"""Event class for browsingContext.navigationStarted event."""464465event_class = "browsingContext.navigationStarted"466467@classmethod468def from_json(cls, json: dict):469if isinstance(json, NavigationInfo):470return json471return NavigationInfo.from_json(json)472473474class NavigationCommitted:475"""Event class for browsingContext.navigationCommitted event."""476477event_class = "browsingContext.navigationCommitted"478479@classmethod480def from_json(cls, json: dict):481if isinstance(json, NavigationInfo):482return json483return NavigationInfo.from_json(json)484485486class NavigationFailed:487"""Event class for browsingContext.navigationFailed event."""488489event_class = "browsingContext.navigationFailed"490491@classmethod492def from_json(cls, json: dict):493if isinstance(json, NavigationInfo):494return json495return NavigationInfo.from_json(json)496497498class NavigationAborted:499"""Event class for browsingContext.navigationAborted event."""500501event_class = "browsingContext.navigationAborted"502503@classmethod504def from_json(cls, json: dict):505if isinstance(json, NavigationInfo):506return json507return NavigationInfo.from_json(json)508509510class DomContentLoaded:511"""Event class for browsingContext.domContentLoaded event."""512513event_class = "browsingContext.domContentLoaded"514515@classmethod516def from_json(cls, json: dict):517if isinstance(json, NavigationInfo):518return json519return NavigationInfo.from_json(json)520521522class Load:523"""Event class for browsingContext.load event."""524525event_class = "browsingContext.load"526527@classmethod528def from_json(cls, json: dict):529if isinstance(json, NavigationInfo):530return json531return NavigationInfo.from_json(json)532533534class FragmentNavigated:535"""Event class for browsingContext.fragmentNavigated event."""536537event_class = "browsingContext.fragmentNavigated"538539@classmethod540def from_json(cls, json: dict):541if isinstance(json, NavigationInfo):542return json543return NavigationInfo.from_json(json)544545546class DownloadWillBegin:547"""Event class for browsingContext.downloadWillBegin event."""548549event_class = "browsingContext.downloadWillBegin"550551@classmethod552def from_json(cls, json: dict):553return DownloadWillBeginParams.from_json(json)554555556class UserPromptOpened:557"""Event class for browsingContext.userPromptOpened event."""558559event_class = "browsingContext.userPromptOpened"560561@classmethod562def from_json(cls, json: dict):563return UserPromptOpenedParams.from_json(json)564565566class UserPromptClosed:567"""Event class for browsingContext.userPromptClosed event."""568569event_class = "browsingContext.userPromptClosed"570571@classmethod572def from_json(cls, json: dict):573return UserPromptClosedParams.from_json(json)574575576class HistoryUpdated:577"""Event class for browsingContext.historyUpdated event."""578579event_class = "browsingContext.historyUpdated"580581@classmethod582def from_json(cls, json: dict):583return HistoryUpdatedParams.from_json(json)584585586class DownloadEnd:587"""Event class for browsingContext.downloadEnd event."""588589event_class = "browsingContext.downloadEnd"590591@classmethod592def from_json(cls, json: dict):593return DownloadEndParams.from_json(json)594595596@dataclass597class EventConfig:598event_key: str599bidi_event: str600event_class: type601602603class _EventManager:604"""Class to manage event subscriptions and callbacks for BrowsingContext."""605606def __init__(self, conn, event_configs: dict[str, EventConfig]):607self.conn = conn608self.event_configs = event_configs609self.subscriptions: dict = {}610self._bidi_to_class = {config.bidi_event: config.event_class for config in event_configs.values()}611self._available_events = ", ".join(sorted(event_configs.keys()))612# Thread safety lock for subscription operations613self._subscription_lock = threading.Lock()614615def validate_event(self, event: str) -> EventConfig:616event_config = self.event_configs.get(event)617if not event_config:618raise ValueError(f"Event '{event}' not found. Available events: {self._available_events}")619return event_config620621def subscribe_to_event(self, bidi_event: str, contexts: Optional[list[str]] = None) -> None:622"""Subscribe to a BiDi event if not already subscribed.623624Parameters:625----------626bidi_event: The BiDi event name.627contexts: Optional browsing context IDs to subscribe to.628"""629with self._subscription_lock:630if bidi_event not in self.subscriptions:631session = Session(self.conn)632self.conn.execute(session.subscribe(bidi_event, browsing_contexts=contexts))633self.subscriptions[bidi_event] = []634635def unsubscribe_from_event(self, bidi_event: str) -> None:636"""Unsubscribe from a BiDi event if no more callbacks exist.637638Parameters:639----------640bidi_event: The BiDi event name.641"""642with self._subscription_lock:643callback_list = self.subscriptions.get(bidi_event)644if callback_list is not None and not callback_list:645session = Session(self.conn)646self.conn.execute(session.unsubscribe(bidi_event))647del self.subscriptions[bidi_event]648649def add_callback_to_tracking(self, bidi_event: str, callback_id: int) -> None:650with self._subscription_lock:651self.subscriptions[bidi_event].append(callback_id)652653def remove_callback_from_tracking(self, bidi_event: str, callback_id: int) -> None:654with self._subscription_lock:655callback_list = self.subscriptions.get(bidi_event)656if callback_list and callback_id in callback_list:657callback_list.remove(callback_id)658659def add_event_handler(self, event: str, callback: Callable, contexts: Optional[list[str]] = None) -> int:660event_config = self.validate_event(event)661662callback_id = self.conn.add_callback(event_config.event_class, callback)663664# Subscribe to the event if needed665self.subscribe_to_event(event_config.bidi_event, contexts)666667# Track the callback668self.add_callback_to_tracking(event_config.bidi_event, callback_id)669670return callback_id671672def remove_event_handler(self, event: str, callback_id: int) -> None:673event_config = self.validate_event(event)674675# Remove the callback from the connection676self.conn.remove_callback(event_config.event_class, callback_id)677678# Remove from tracking collections679self.remove_callback_from_tracking(event_config.bidi_event, callback_id)680681# Unsubscribe if no more callbacks exist682self.unsubscribe_from_event(event_config.bidi_event)683684def clear_event_handlers(self) -> None:685"""Clear all event handlers from the browsing context."""686with self._subscription_lock:687if not self.subscriptions:688return689690session = Session(self.conn)691692for bidi_event, callback_ids in list(self.subscriptions.items()):693event_class = self._bidi_to_class.get(bidi_event)694if event_class:695# Remove all callbacks for this event696for callback_id in callback_ids:697self.conn.remove_callback(event_class, callback_id)698699self.conn.execute(session.unsubscribe(bidi_event))700701self.subscriptions.clear()702703704class BrowsingContext:705"""BiDi implementation of the browsingContext module."""706707EVENT_CONFIGS = {708"context_created": EventConfig("context_created", "browsingContext.contextCreated", ContextCreated),709"context_destroyed": EventConfig("context_destroyed", "browsingContext.contextDestroyed", ContextDestroyed),710"dom_content_loaded": EventConfig("dom_content_loaded", "browsingContext.domContentLoaded", DomContentLoaded),711"download_end": EventConfig("download_end", "browsingContext.downloadEnd", DownloadEnd),712"download_will_begin": EventConfig(713"download_will_begin", "browsingContext.downloadWillBegin", DownloadWillBegin714),715"fragment_navigated": EventConfig("fragment_navigated", "browsingContext.fragmentNavigated", FragmentNavigated),716"history_updated": EventConfig("history_updated", "browsingContext.historyUpdated", HistoryUpdated),717"load": EventConfig("load", "browsingContext.load", Load),718"navigation_aborted": EventConfig("navigation_aborted", "browsingContext.navigationAborted", NavigationAborted),719"navigation_committed": EventConfig(720"navigation_committed", "browsingContext.navigationCommitted", NavigationCommitted721),722"navigation_failed": EventConfig("navigation_failed", "browsingContext.navigationFailed", NavigationFailed),723"navigation_started": EventConfig("navigation_started", "browsingContext.navigationStarted", NavigationStarted),724"user_prompt_closed": EventConfig("user_prompt_closed", "browsingContext.userPromptClosed", UserPromptClosed),725"user_prompt_opened": EventConfig("user_prompt_opened", "browsingContext.userPromptOpened", UserPromptOpened),726}727728def __init__(self, conn):729self.conn = conn730self._event_manager = _EventManager(conn, self.EVENT_CONFIGS)731732@classmethod733def get_event_names(cls) -> list[str]:734"""Get a list of all available event names.735736Returns:737-------738List[str]: A list of event names that can be used with event handlers.739"""740return list(cls.EVENT_CONFIGS.keys())741742def activate(self, context: str) -> None:743"""Activates and focuses the given top-level traversable.744745Parameters:746-----------747context: The browsing context ID to activate.748749Raises:750------751Exception: If the browsing context is not a top-level traversable.752"""753params = {"context": context}754self.conn.execute(command_builder("browsingContext.activate", params))755756def capture_screenshot(757self,758context: str,759origin: str = "viewport",760format: Optional[dict] = None,761clip: Optional[dict] = None,762) -> str:763"""Captures an image of the given navigable, and returns it as a Base64-encoded string.764765Parameters:766-----------767context: The browsing context ID to capture.768origin: The origin of the screenshot, either "viewport" or "document".769format: The format of the screenshot.770clip: The clip rectangle of the screenshot.771772Returns:773-------774str: The Base64-encoded screenshot.775"""776params: dict[str, Any] = {"context": context, "origin": origin}777if format is not None:778params["format"] = format779if clip is not None:780params["clip"] = clip781782result = self.conn.execute(command_builder("browsingContext.captureScreenshot", params))783return result["data"]784785def close(self, context: str, prompt_unload: bool = False) -> None:786"""Closes a top-level traversable.787788Parameters:789-----------790context: The browsing context ID to close.791prompt_unload: Whether to prompt to unload.792793Raises:794------795Exception: If the browsing context is not a top-level traversable.796"""797params = {"context": context, "promptUnload": prompt_unload}798self.conn.execute(command_builder("browsingContext.close", params))799800def create(801self,802type: str,803reference_context: Optional[str] = None,804background: bool = False,805user_context: Optional[str] = None,806) -> str:807"""Creates a new navigable, either in a new tab or in a new window, and returns its navigable id.808809Parameters:810-----------811type: The type of the new navigable, either "tab" or "window".812reference_context: The reference browsing context ID.813background: Whether to create the new navigable in the background.814user_context: The user context ID.815816Returns:817-------818str: The browsing context ID of the created navigable.819"""820params: dict[str, Any] = {"type": type}821if reference_context is not None:822params["referenceContext"] = reference_context823if background is not None:824params["background"] = background825if user_context is not None:826params["userContext"] = user_context827828result = self.conn.execute(command_builder("browsingContext.create", params))829return result["context"]830831def get_tree(832self,833max_depth: Optional[int] = None,834root: Optional[str] = None,835) -> list[BrowsingContextInfo]:836"""Returns a tree of all descendent navigables including the given parent itself, or all top-level contexts837when no parent is provided.838839Parameters:840-----------841max_depth: The maximum depth of the tree.842root: The root browsing context ID.843844Returns:845-------846List[BrowsingContextInfo]: A list of browsing context information.847"""848params: dict[str, Any] = {}849if max_depth is not None:850params["maxDepth"] = max_depth851if root is not None:852params["root"] = root853854result = self.conn.execute(command_builder("browsingContext.getTree", params))855return [BrowsingContextInfo.from_json(context) for context in result["contexts"]]856857def handle_user_prompt(858self,859context: str,860accept: Optional[bool] = None,861user_text: Optional[str] = None,862) -> None:863"""Allows closing an open prompt.864865Parameters:866-----------867context: The browsing context ID.868accept: Whether to accept the prompt.869user_text: The text to enter in the prompt.870"""871params: dict[str, Any] = {"context": context}872if accept is not None:873params["accept"] = accept874if user_text is not None:875params["userText"] = user_text876877self.conn.execute(command_builder("browsingContext.handleUserPrompt", params))878879def locate_nodes(880self,881context: str,882locator: dict,883max_node_count: Optional[int] = None,884serialization_options: Optional[dict] = None,885start_nodes: Optional[list[dict]] = None,886) -> list[dict]:887"""Returns a list of all nodes matching the specified locator.888889Parameters:890-----------891context: The browsing context ID.892locator: The locator to use.893max_node_count: The maximum number of nodes to return.894serialization_options: The serialization options.895start_nodes: The start nodes.896897Returns:898-------899List[Dict]: A list of nodes.900"""901params: dict[str, Any] = {"context": context, "locator": locator}902if max_node_count is not None:903params["maxNodeCount"] = max_node_count904if serialization_options is not None:905params["serializationOptions"] = serialization_options906if start_nodes is not None:907params["startNodes"] = start_nodes908909result = self.conn.execute(command_builder("browsingContext.locateNodes", params))910return result["nodes"]911912def navigate(913self,914context: str,915url: str,916wait: Optional[str] = None,917) -> dict:918"""Navigates a navigable to the given URL.919920Parameters:921-----------922context: The browsing context ID.923url: The URL to navigate to.924wait: The readiness state to wait for.925926Returns:927-------928Dict: A dictionary containing the navigation result.929"""930params = {"context": context, "url": url}931if wait is not None:932params["wait"] = wait933934result = self.conn.execute(command_builder("browsingContext.navigate", params))935return result936937def print(938self,939context: str,940background: bool = False,941margin: Optional[dict] = None,942orientation: str = "portrait",943page: Optional[dict] = None,944page_ranges: Optional[list[Union[int, str]]] = None,945scale: float = 1.0,946shrink_to_fit: bool = True,947) -> str:948"""Creates a paginated representation of a document, and returns it as a PDF document represented as a949Base64-encoded string.950951Parameters:952-----------953context: The browsing context ID.954background: Whether to include the background.955margin: The margin parameters.956orientation: The orientation, either "portrait" or "landscape".957page: The page parameters.958page_ranges: The page ranges.959scale: The scale.960shrink_to_fit: Whether to shrink to fit.961962Returns:963-------964str: The Base64-encoded PDF document.965"""966params = {967"context": context,968"background": background,969"orientation": orientation,970"scale": scale,971"shrinkToFit": shrink_to_fit,972}973if margin is not None:974params["margin"] = margin975if page is not None:976params["page"] = page977if page_ranges is not None:978params["pageRanges"] = page_ranges979980result = self.conn.execute(command_builder("browsingContext.print", params))981return result["data"]982983def reload(984self,985context: str,986ignore_cache: Optional[bool] = None,987wait: Optional[str] = None,988) -> dict:989"""Reloads a navigable.990991Parameters:992-----------993context: The browsing context ID.994ignore_cache: Whether to ignore the cache.995wait: The readiness state to wait for.996997Returns:998-------999Dict: A dictionary containing the navigation result.1000"""1001params: dict[str, Any] = {"context": context}1002if ignore_cache is not None:1003params["ignoreCache"] = ignore_cache1004if wait is not None:1005params["wait"] = wait10061007result = self.conn.execute(command_builder("browsingContext.reload", params))1008return result10091010def set_viewport(1011self,1012context: Optional[str] = None,1013viewport: Optional[dict] = None,1014device_pixel_ratio: Optional[float] = None,1015user_contexts: Optional[list[str]] = None,1016) -> None:1017"""Modifies specific viewport characteristics on the given top-level traversable.10181019Parameters:1020-----------1021context: The browsing context ID.1022viewport: The viewport parameters.1023device_pixel_ratio: The device pixel ratio.1024user_contexts: The user context IDs.10251026Raises:1027------1028Exception: If the browsing context is not a top-level traversable.1029"""1030params: dict[str, Any] = {}1031if context is not None:1032params["context"] = context1033if viewport is not None:1034params["viewport"] = viewport1035if device_pixel_ratio is not None:1036params["devicePixelRatio"] = device_pixel_ratio1037if user_contexts is not None:1038params["userContexts"] = user_contexts10391040self.conn.execute(command_builder("browsingContext.setViewport", params))10411042def traverse_history(self, context: str, delta: int) -> dict:1043"""Traverses the history of a given navigable by a delta.10441045Parameters:1046-----------1047context: The browsing context ID.1048delta: The delta to traverse by.10491050Returns:1051-------1052Dict: A dictionary containing the traverse history result.1053"""1054params = {"context": context, "delta": delta}1055result = self.conn.execute(command_builder("browsingContext.traverseHistory", params))1056return result10571058def add_event_handler(self, event: str, callback: Callable, contexts: Optional[list[str]] = None) -> int:1059"""Add an event handler to the browsing context.10601061Parameters:1062----------1063event: The event to subscribe to.1064callback: The callback function to execute on event.1065contexts: The browsing context IDs to subscribe to.10661067Returns:1068-------1069int: callback id1070"""1071return self._event_manager.add_event_handler(event, callback, contexts)10721073def remove_event_handler(self, event: str, callback_id: int) -> None:1074"""Remove an event handler from the browsing context.10751076Parameters:1077----------1078event: The event to unsubscribe from.1079callback_id: The callback id to remove.1080"""1081self._event_manager.remove_event_handler(event, callback_id)10821083def clear_event_handlers(self) -> None:1084"""Clear all event handlers from the browsing context."""1085self._event_manager.clear_event_handlers()108610871088