Path: blob/trunk/py/selenium/webdriver/remote/webdriver.py
4004 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.1617"""The WebDriver implementation."""1819import base6420import contextlib21import copy22import os23import pkgutil24import tempfile25import types26import warnings27import zipfile28from abc import ABCMeta29from base64 import b64decode, urlsafe_b64encode30from contextlib import asynccontextmanager, contextmanager31from importlib import import_module32from typing import Any, cast3334from selenium.common.exceptions import (35InvalidArgumentException,36JavascriptException,37NoSuchCookieException,38NoSuchElementException,39WebDriverException,40)41from selenium.webdriver.common.bidi.browser import Browser42from selenium.webdriver.common.bidi.browsing_context import BrowsingContext43from selenium.webdriver.common.bidi.emulation import Emulation44from selenium.webdriver.common.bidi.input import Input45from selenium.webdriver.common.bidi.network import Network46from selenium.webdriver.common.bidi.permissions import Permissions47from selenium.webdriver.common.bidi.script import Script48from selenium.webdriver.common.bidi.session import Session49from selenium.webdriver.common.bidi.storage import Storage50from selenium.webdriver.common.bidi.webextension import WebExtension51from selenium.webdriver.common.by import By52from selenium.webdriver.common.fedcm.dialog import Dialog53from selenium.webdriver.common.options import ArgOptions, BaseOptions54from selenium.webdriver.common.print_page_options import PrintOptions55from selenium.webdriver.common.timeouts import Timeouts56from selenium.webdriver.common.virtual_authenticator import (57Credential,58VirtualAuthenticatorOptions,59required_virtual_authenticator,60)61from selenium.webdriver.remote.bidi_connection import BidiConnection62from selenium.webdriver.remote.client_config import ClientConfig63from selenium.webdriver.remote.command import Command64from selenium.webdriver.remote.errorhandler import ErrorHandler65from selenium.webdriver.remote.fedcm import FedCM66from selenium.webdriver.remote.file_detector import FileDetector, LocalFileDetector67from selenium.webdriver.remote.locator_converter import LocatorConverter68from selenium.webdriver.remote.mobile import Mobile69from selenium.webdriver.remote.remote_connection import RemoteConnection70from selenium.webdriver.remote.script_key import ScriptKey71from selenium.webdriver.remote.shadowroot import ShadowRoot72from selenium.webdriver.remote.switch_to import SwitchTo73from selenium.webdriver.remote.webelement import WebElement74from selenium.webdriver.remote.websocket_connection import WebSocketConnection75from selenium.webdriver.support.relative_locator import RelativeBy7677cdp = None787980def import_cdp() -> None:81global cdp82if not cdp:83cdp = import_module("selenium.webdriver.common.bidi.cdp")848586def _create_caps(caps) -> dict:87"""Makes a W3C alwaysMatch capabilities object.8889Filters out capability names that are not in the W3C spec. Spec-compliant90drivers will reject requests containing unknown capability names.9192Moves the Firefox profile, if present, from the old location to the new Firefox93options object.9495Args:96caps: A dictionary of capabilities requested by the caller.97"""98caps = copy.deepcopy(caps)99always_match = {}100for k, v in caps.items():101always_match[k] = v102return {"capabilities": {"firstMatch": [{}], "alwaysMatch": always_match}}103104105def get_remote_connection(106capabilities: dict,107command_executor: str | RemoteConnection,108keep_alive: bool,109ignore_local_proxy: bool,110client_config: ClientConfig | None = None,111) -> RemoteConnection:112if isinstance(command_executor, str):113client_config = client_config or ClientConfig(remote_server_addr=command_executor)114client_config.remote_server_addr = command_executor115command_executor = RemoteConnection(client_config=client_config)116117browser_name = capabilities.get("browserName")118handler: type[RemoteConnection]119if browser_name == "chrome":120from selenium.webdriver.chrome.remote_connection import ChromeRemoteConnection121122handler = ChromeRemoteConnection123elif browser_name == "MicrosoftEdge":124from selenium.webdriver.edge.remote_connection import EdgeRemoteConnection125126handler = EdgeRemoteConnection127elif browser_name == "firefox":128from selenium.webdriver.firefox.remote_connection import FirefoxRemoteConnection129130handler = FirefoxRemoteConnection131elif browser_name == "Safari":132from selenium.webdriver.safari.remote_connection import SafariRemoteConnection133134handler = SafariRemoteConnection135else:136handler = RemoteConnection137138if hasattr(command_executor, "client_config") and command_executor.client_config:139remote_server_addr = command_executor.client_config.remote_server_addr140else:141remote_server_addr = command_executor142143return handler(144remote_server_addr=remote_server_addr,145keep_alive=keep_alive,146ignore_proxy=ignore_local_proxy,147client_config=client_config,148)149150151def create_matches(options: list[BaseOptions]) -> dict:152capabilities: dict[str, Any] = {"capabilities": {}}153opts = []154for opt in options:155opts.append(opt.to_capabilities())156opts_size = len(opts)157samesies = {}158159# Can not use bitwise operations on the dicts or lists due to160# https://bugs.python.org/issue38210161for i in range(opts_size):162min_index = i163if i + 1 < opts_size:164first_keys = opts[min_index].keys()165166for kys in first_keys:167if kys in opts[i + 1].keys():168if opts[min_index][kys] == opts[i + 1][kys]:169samesies.update({kys: opts[min_index][kys]})170171always = {}172for k, v in samesies.items():173always[k] = v174175for opt_dict in opts:176for k in always:177del opt_dict[k]178179capabilities["capabilities"]["alwaysMatch"] = always180capabilities["capabilities"]["firstMatch"] = opts181182return capabilities183184185class BaseWebDriver(metaclass=ABCMeta):186"""Abstract Base Class for all Webdriver subtypes.187188ABC's allow custom implementations of Webdriver to be registered so189that isinstance type checks will succeed.190"""191192193class WebDriver(BaseWebDriver):194"""Control a browser by sending commands to a remote WebDriver server.195196This class expects the remote server to be running the WebDriver wire protocol197as defined at https://www.selenium.dev/documentation/legacy/json_wire_protocol/.198199Attributes:200-----------201session_id - String ID of the browser session started and controlled by this WebDriver.202capabilities - Dictionary of effective capabilities of this browser session as returned203by the remote server. See https://www.selenium.dev/documentation/legacy/desired_capabilities/204command_executor : str or remote_connection.RemoteConnection object used to execute commands.205error_handler - errorhandler.ErrorHandler object used to handle errors.206"""207208_web_element_cls = WebElement209_shadowroot_cls = ShadowRoot210211def __init__(212self,213command_executor: str | RemoteConnection = "http://127.0.0.1:4444",214keep_alive: bool = True,215file_detector: FileDetector | None = None,216options: BaseOptions | list[BaseOptions] | None = None,217locator_converter: LocatorConverter | None = None,218web_element_cls: type[WebElement] | None = None,219client_config: ClientConfig | None = None,220) -> None:221"""Create a new driver instance that issues commands using the WebDriver protocol.222223Args:224command_executor: Either a string representing the URL of the remote225server or a custom remote_connection.RemoteConnection object.226Defaults to 'http://127.0.0.1:4444/wd/hub'.227keep_alive: (Deprecated) Whether to configure228remote_connection.RemoteConnection to use HTTP keep-alive.229Defaults to True.230file_detector: Pass a custom file detector object during231instantiation. If None, the default LocalFileDetector() will be232used.233options: Instance of a driver options.Options class.234locator_converter: Custom locator converter to use. Defaults to None.235web_element_cls: Custom class to use for web elements. Defaults to236WebElement.237client_config: Custom client configuration to use. Defaults to None.238"""239if options is None:240raise TypeError(241"missing 1 required keyword-only argument: 'options' (instance of driver `options.Options` class)"242)243elif isinstance(options, list):244capabilities = create_matches(options)245_ignore_local_proxy = False246else:247capabilities = options.to_capabilities()248_ignore_local_proxy = options._ignore_local_proxy249self.command_executor = command_executor250if isinstance(self.command_executor, (str, bytes)):251self.command_executor = get_remote_connection(252capabilities,253command_executor=command_executor,254keep_alive=keep_alive,255ignore_local_proxy=_ignore_local_proxy,256client_config=client_config,257)258self._is_remote = True259self.session_id: str | None = None260self.caps: dict[str, Any] = {}261self.pinned_scripts: dict[str, Any] = {}262self.error_handler = ErrorHandler()263self._switch_to = SwitchTo(self)264self._mobile = Mobile(self)265self.file_detector = file_detector or LocalFileDetector()266self.locator_converter = locator_converter or LocatorConverter()267self._web_element_cls = web_element_cls or self._web_element_cls268self._authenticator_id = None269self.start_client()270self.start_session(capabilities)271self._fedcm = FedCM(self)272273self._websocket_connection: WebSocketConnection | None = None274self._script: Script | None = None275self._network: Network | None = None276self._browser: Browser | None = None277self._bidi_session: Session | None = None278self._browsing_context: BrowsingContext | None = None279self._storage: Storage | None = None280self._webextension: WebExtension | None = None281self._permissions: Permissions | None = None282self._emulation: Emulation | None = None283self._input: Input | None = None284self._devtools: Any | None = None285286def __repr__(self) -> str:287return f'<{type(self).__module__}.{type(self).__name__} (session="{self.session_id}")>'288289def __enter__(self) -> "WebDriver":290return self291292def __exit__(293self,294exc_type: type[BaseException] | None,295exc: BaseException | None,296traceback: types.TracebackType | None,297):298self.quit()299300@contextmanager301def file_detector_context(self, file_detector_class, *args, **kwargs):302"""Override the current file detector temporarily within a limited context.303304Ensures the original file detector is set after exiting the context.305306Args:307file_detector_class: Class of the desired file detector. If the308class is different from the current file_detector, then the309class is instantiated with args and kwargs and used as a file310detector during the duration of the context manager.311*args: Optional arguments that get passed to the file detector class312during instantiation.313**kwargs: Keyword arguments, passed the same way as args.314315Example:316```317with webdriver.file_detector_context(UselessFileDetector):318someinput.send_keys("/etc/hosts")319````320"""321last_detector = None322if not isinstance(self.file_detector, file_detector_class):323last_detector = self.file_detector324self.file_detector = file_detector_class(*args, **kwargs)325try:326yield327finally:328if last_detector:329self.file_detector = last_detector330331@property332def mobile(self) -> Mobile:333return self._mobile334335@property336def name(self) -> str:337"""Returns the name of the underlying browser for this instance."""338if "browserName" in self.caps:339return self.caps["browserName"]340raise KeyError("browserName not specified in session capabilities")341342def start_client(self) -> None:343"""Called before starting a new session.344345This method may be overridden to define custom startup behavior.346"""347pass348349def stop_client(self) -> None:350"""Called after executing a quit command.351352This method may be overridden to define custom shutdown353behavior.354"""355pass356357def start_session(self, capabilities: dict) -> None:358"""Creates a new session with the desired capabilities.359360Args:361capabilities: A capabilities dict to start the session with.362"""363caps = _create_caps(capabilities)364try:365response = self.execute(Command.NEW_SESSION, caps)["value"]366self.session_id = response.get("sessionId")367self.caps = response.get("capabilities")368except Exception:369if hasattr(self, "service") and self.service is not None:370self.service.stop()371raise372373def _wrap_value(self, value):374if isinstance(value, dict):375converted = {}376for key, val in value.items():377converted[key] = self._wrap_value(val)378return converted379if isinstance(value, self._web_element_cls):380return {"element-6066-11e4-a52e-4f735466cecf": value.id}381if isinstance(value, self._shadowroot_cls):382return {"shadow-6066-11e4-a52e-4f735466cecf": value.id}383if isinstance(value, list):384return list(self._wrap_value(item) for item in value)385return value386387def create_web_element(self, element_id: str) -> WebElement:388"""Creates a web element with the specified `element_id`."""389return self._web_element_cls(self, element_id)390391def _unwrap_value(self, value):392if isinstance(value, dict):393if "element-6066-11e4-a52e-4f735466cecf" in value:394return self.create_web_element(value["element-6066-11e4-a52e-4f735466cecf"])395if "shadow-6066-11e4-a52e-4f735466cecf" in value:396return self._shadowroot_cls(self, value["shadow-6066-11e4-a52e-4f735466cecf"])397for key, val in value.items():398value[key] = self._unwrap_value(val)399return value400if isinstance(value, list):401return list(self._unwrap_value(item) for item in value)402return value403404def execute_cdp_cmd(self, cmd: str, cmd_args: dict):405"""Execute Chrome Devtools Protocol command and get returned result.406407The command and command args should follow chrome devtools protocol domains/commands:408- https://chromedevtools.github.io/devtools-protocol/409410Args:411cmd: Command name.412cmd_args: Command args. Empty dict {} if there is no command args.413414Returns:415A dict, empty dict {} if there is no result to return. To416getResponseBody: {'base64Encoded': False, 'body': 'response body417string'}418419Example:420`driver.execute_cdp_cmd("Network.getResponseBody", {"requestId": requestId})`421"""422return self.execute("executeCdpCommand", {"cmd": cmd, "params": cmd_args})["value"]423424def execute(self, driver_command: str, params: dict[str, Any] | None = None) -> dict[str, Any]:425"""Sends a command to be executed by a command.CommandExecutor.426427Args:428driver_command: The name of the command to execute as a string.429params: A dictionary of named parameters to send with the command.430431Returns:432The command's JSON response loaded into a dictionary object.433"""434params = self._wrap_value(params)435436if self.session_id:437if not params:438params = {"sessionId": self.session_id}439elif "sessionId" not in params:440params["sessionId"] = self.session_id441442response = cast(RemoteConnection, self.command_executor).execute(driver_command, params)443444if response:445self.error_handler.check_response(response)446response["value"] = self._unwrap_value(response.get("value", None))447return response448# If the server doesn't send a response, assume the command was449# a success450return {"success": 0, "value": None, "sessionId": self.session_id}451452def get(self, url: str) -> None:453"""Navigate the browser to the specified URL.454455The method does not return until the page is fully loaded (i.e. the456onload event has fired) in the current window or tab.457458Args:459url: The URL to be opened by the browser. Must include the protocol460(e.g., http://, https://).461462Example:463`driver.get("https://example.com")`464"""465self.execute(Command.GET, {"url": url})466467@property468def title(self) -> str:469"""Returns the title of the current page.470471Example:472```473element = driver.find_element(By.ID, "foo")474print(element.title())475```476"""477return self.execute(Command.GET_TITLE).get("value", "")478479def pin_script(self, script: str, script_key=None) -> ScriptKey:480"""Store a JavaScript script by a unique hashable ID for later execution.481482Example:483`script = "return document.getElementById('foo').value"`484"""485script_key_instance = ScriptKey(script_key)486self.pinned_scripts[script_key_instance.id] = script487return script_key_instance488489def unpin(self, script_key: ScriptKey) -> None:490"""Remove a pinned script from storage.491492Example:493`driver.unpin(script_key)`494"""495try:496self.pinned_scripts.pop(script_key.id)497except KeyError:498raise KeyError(f"No script with key: {script_key} existed in {self.pinned_scripts}") from None499500def get_pinned_scripts(self) -> list[str]:501"""Return a list of all pinned scripts.502503Example:504`pinned_scripts = driver.get_pinned_scripts()`505"""506return list(self.pinned_scripts)507508def execute_script(self, script: str, *args):509"""Synchronously Executes JavaScript in the current window/frame.510511Args:512script: The javascript to execute.513*args: Any applicable arguments for your JavaScript.514515Example:516```517id = "username"518value = "test_user"519driver.execute_script("document.getElementById(arguments[0]).value = arguments[1];", id, value)520```521"""522if isinstance(script, ScriptKey):523try:524script = self.pinned_scripts[script.id]525except KeyError:526raise JavascriptException("Pinned script could not be found")527528converted_args = list(args)529command = Command.W3C_EXECUTE_SCRIPT530531return self.execute(command, {"script": script, "args": converted_args})["value"]532533def execute_async_script(self, script: str, *args) -> dict:534"""Asynchronously Executes JavaScript in the current window/frame.535536Args:537script: The javascript to execute.538*args: Any applicable arguments for your JavaScript.539540Example:541```542script = "var callback = arguments[arguments.length - 1]; "543"window.setTimeout(function(){ callback('timeout') }, 3000);"544driver.execute_async_script(script)545```546"""547converted_args = list(args)548command = Command.W3C_EXECUTE_SCRIPT_ASYNC549550return self.execute(command, {"script": script, "args": converted_args})["value"]551552@property553def current_url(self) -> str:554"""Gets the URL of the current page."""555return self.execute(Command.GET_CURRENT_URL)["value"]556557@property558def page_source(self) -> str:559"""Gets the source of the current page."""560return self.execute(Command.GET_PAGE_SOURCE)["value"]561562def close(self) -> None:563"""Closes the current window."""564self.execute(Command.CLOSE)565566def quit(self) -> None:567"""Quits the driver and closes every associated window."""568try:569self.execute(Command.QUIT)570finally:571self.stop_client()572executor = cast(RemoteConnection, self.command_executor)573executor.close()574575@property576def current_window_handle(self) -> str:577"""Returns the handle of the current window."""578return self.execute(Command.W3C_GET_CURRENT_WINDOW_HANDLE)["value"]579580@property581def window_handles(self) -> list[str]:582"""Returns the handles of all windows within the current session."""583return self.execute(Command.W3C_GET_WINDOW_HANDLES)["value"]584585def maximize_window(self) -> None:586"""Maximizes the current window that webdriver is using."""587command = Command.W3C_MAXIMIZE_WINDOW588self.execute(command, None)589590def fullscreen_window(self) -> None:591"""Invokes the window manager-specific 'full screen' operation."""592self.execute(Command.FULLSCREEN_WINDOW)593594def minimize_window(self) -> None:595"""Invokes the window manager-specific 'minimize' operation."""596self.execute(Command.MINIMIZE_WINDOW)597598def print_page(self, print_options: PrintOptions | None = None) -> str:599"""Takes PDF of the current page.600601The driver makes a best effort to return a PDF based on the602provided parameters.603"""604options: dict[str, Any] | Any = {}605if print_options:606options = print_options.to_dict()607608return self.execute(Command.PRINT_PAGE, options)["value"]609610@property611def switch_to(self) -> SwitchTo:612"""Return an object containing all options to switch focus into.613614Returns:615An object containing all options to switch focus into.616617Examples:618`element = driver.switch_to.active_element`619`alert = driver.switch_to.alert`620`driver.switch_to.default_content()`621`driver.switch_to.frame("frame_name")`622`driver.switch_to.frame(1)`623`driver.switch_to.frame(driver.find_elements(By.TAG_NAME, "iframe")[0])`624`driver.switch_to.parent_frame()`625`driver.switch_to.window("main")`626"""627return self._switch_to628629# Navigation630def back(self) -> None:631"""Goes one step backward in the browser history."""632self.execute(Command.GO_BACK)633634def forward(self) -> None:635"""Goes one step forward in the browser history."""636self.execute(Command.GO_FORWARD)637638def refresh(self) -> None:639"""Refreshes the current page."""640self.execute(Command.REFRESH)641642def get_cookies(self) -> list[dict]:643"""Get all cookies visible to the current WebDriver instance.644645Returns:646A list of dictionaries, corresponding to cookies visible in the647current session.648"""649return self.execute(Command.GET_ALL_COOKIES)["value"]650651def get_cookie(self, name) -> dict | None:652"""Get a single cookie by name (case-sensitive,).653654Returns:655A cookie dictionary or None if not found.656657Raises:658ValueError if the name is empty or whitespace.659660Example:661`cookie = driver.get_cookie("my_cookie")`662"""663if not name or name.isspace():664raise ValueError("Cookie name cannot be empty")665666with contextlib.suppress(NoSuchCookieException):667return self.execute(Command.GET_COOKIE, {"name": name})["value"]668669return None670671def delete_cookie(self, name) -> None:672"""Delete a single cookie with the given name (case-sensitive).673674Raises:675ValueError if the name is empty or whitespace.676677Example:678`driver.delete_cookie("my_cookie")`679"""680# Firefox deletes all cookies when "" is passed as name681if not name or name.isspace():682raise ValueError("Cookie name cannot be empty")683684self.execute(Command.DELETE_COOKIE, {"name": name})685686def delete_all_cookies(self) -> None:687"""Delete all cookies in the scope of the session."""688self.execute(Command.DELETE_ALL_COOKIES)689690def add_cookie(self, cookie_dict) -> None:691"""Adds a cookie to your current session.692693Args:694cookie_dict: A dictionary object, with required keys - "name" and695"value"; Optional keys - "path", "domain", "secure", "httpOnly",696"expiry", "sameSite".697698Examples:699`driver.add_cookie({"name": "foo", "value": "bar"})`700`driver.add_cookie({"name": "foo", "value": "bar", "path": "/"})`701`driver.add_cookie({"name": "foo", "value": "bar", "path": "/", "secure": True})`702`driver.add_cookie({"name": "foo", "value": "bar", "sameSite": "Strict"})`703"""704if "sameSite" in cookie_dict:705assert cookie_dict["sameSite"] in ["Strict", "Lax", "None"]706self.execute(Command.ADD_COOKIE, {"cookie": cookie_dict})707else:708self.execute(Command.ADD_COOKIE, {"cookie": cookie_dict})709710# Timeouts711def implicitly_wait(self, time_to_wait: float) -> None:712"""Set a sticky implicit timeout for element location and command completion.713714This method sets a timeout that applies to all element location strategies715for the duration of the session. It only needs to be called once per session.716To set the timeout for asynchronous script execution, see set_script_timeout.717718Args:719time_to_wait: Amount of time to wait (in seconds).720721Example:722`driver.implicitly_wait(30)`723"""724self.execute(Command.SET_TIMEOUTS, {"implicit": int(float(time_to_wait) * 1000)})725726def set_script_timeout(self, time_to_wait: float) -> None:727"""Set the timeout for asynchronous script execution.728729This timeout specifies how long a script can run during an730execute_async_script call before throwing an error.731732Args:733time_to_wait: The amount of time to wait (in seconds).734735Example:736`driver.set_script_timeout(30)`737"""738self.execute(Command.SET_TIMEOUTS, {"script": int(float(time_to_wait) * 1000)})739740def set_page_load_timeout(self, time_to_wait: float) -> None:741"""Set the timeout for page load completion.742743This specifies how long to wait for a page load to complete before744throwing an error.745746Args:747time_to_wait: The amount of time to wait (in seconds).748749Example:750`driver.set_page_load_timeout(30)`751"""752try:753self.execute(Command.SET_TIMEOUTS, {"pageLoad": int(float(time_to_wait) * 1000)})754except WebDriverException:755self.execute(Command.SET_TIMEOUTS, {"ms": float(time_to_wait) * 1000, "type": "page load"})756757@property758def timeouts(self) -> Timeouts:759"""Get all the timeouts that have been set on the current session.760761Returns:762A named tuple with the following fields:763- implicit_wait: The time to wait for elements to be found.764- page_load: The time to wait for a page to load.765- script: The time to wait for scripts to execute.766767Example:768`driver.timeouts`769"""770timeouts = self.execute(Command.GET_TIMEOUTS)["value"]771timeouts["implicit_wait"] = timeouts.pop("implicit") / 1000772timeouts["page_load"] = timeouts.pop("pageLoad") / 1000773timeouts["script"] = timeouts.pop("script") / 1000774return Timeouts(**timeouts)775776@timeouts.setter777def timeouts(self, timeouts) -> None:778"""Set all timeouts for the session.779780This will override any previously set timeouts.781782Example:783```784my_timeouts = Timeouts()785my_timeouts.implicit_wait = 10786driver.timeouts = my_timeouts787```788"""789_ = self.execute(Command.SET_TIMEOUTS, timeouts._to_json())["value"]790791def find_element(self, by: str | RelativeBy = By.ID, value: str | None = None) -> WebElement:792"""Find an element given a By strategy and locator.793794Args:795by: The locating strategy to use. Default is `By.ID`. Supported796values include: By.ID, By.NAME, By.XPATH, By.CSS_SELECTOR,797By.CLASS_NAME, By.TAG_NAME, By.LINK_TEXT, By.PARTIAL_LINK_TEXT,798or RelativeBy.799value: The locator value to use with the specified `by` strategy.800801Returns:802The first matching WebElement found on the page.803804Example:805`element = driver.find_element(By.ID, 'foo')`806"""807by, value = self.locator_converter.convert(by, value)808809if isinstance(by, RelativeBy):810elements = self.find_elements(by=by, value=value)811if not elements:812raise NoSuchElementException(f"Cannot locate relative element with: {by.root}")813return elements[0]814815return self.execute(Command.FIND_ELEMENT, {"using": by, "value": value})["value"]816817def find_elements(self, by: str | RelativeBy = By.ID, value: str | None = None) -> list[WebElement]:818"""Find elements given a By strategy and locator.819820Args:821by: The locating strategy to use. Default is `By.ID`. Supported822values include: By.ID, By.NAME, By.XPATH, By.CSS_SELECTOR,823By.CLASS_NAME, By.TAG_NAME, By.LINK_TEXT, By.PARTIAL_LINK_TEXT,824or RelativeBy.825value: The locator value to use with the specified `by` strategy.826827Returns:828List of WebElements matching locator strategy found on the page.829830Example:831`element = driver.find_elements(By.ID, 'foo')`832"""833by, value = self.locator_converter.convert(by, value)834835if isinstance(by, RelativeBy):836_pkg = ".".join(__name__.split(".")[:-1])837raw_data = pkgutil.get_data(_pkg, "findElements.js")838if raw_data is None:839raise FileNotFoundError(f"Could not find findElements.js in package {_pkg}")840raw_function = raw_data.decode("utf8")841find_element_js = f"/* findElements */return ({raw_function}).apply(null, arguments);"842return self.execute_script(find_element_js, by.to_dict())843844# Return empty list if driver returns null845# See https://github.com/SeleniumHQ/selenium/issues/4555846return self.execute(Command.FIND_ELEMENTS, {"using": by, "value": value})["value"] or []847848@property849def capabilities(self) -> dict:850"""Returns the drivers current capabilities being used."""851return self.caps852853def get_screenshot_as_file(self, filename) -> bool:854"""Save a screenshot of the current window to a PNG image file.855856Returns:857False if there is any IOError, else returns True. Use full paths in your filename.858859Args:860filename: The full path you wish to save your screenshot to. This861should end with a `.png` extension.862863Example:864`driver.get_screenshot_as_file("./screenshots/foo.png")`865"""866if not str(filename).lower().endswith(".png"):867warnings.warn(868"name used for saved screenshot does not match file type. It should end with a `.png` extension",869UserWarning,870stacklevel=2,871)872png = self.get_screenshot_as_png()873try:874with open(filename, "wb") as f:875f.write(png)876except OSError:877return False878finally:879del png880return True881882def save_screenshot(self, filename) -> bool:883"""Save a screenshot of the current window to a PNG image file.884885Returns:886False if there is any IOError, else returns True. Use full paths in your filename.887888Args:889filename: The full path you wish to save your screenshot to. This890should end with a `.png` extension.891892Example:893`driver.save_screenshot("./screenshots/foo.png")`894"""895return self.get_screenshot_as_file(filename)896897def get_screenshot_as_png(self) -> bytes:898"""Gets the screenshot of the current window as a binary data.899900Example:901`driver.get_screenshot_as_png()`902"""903return b64decode(self.get_screenshot_as_base64().encode("ascii"))904905def get_screenshot_as_base64(self) -> str:906"""Get a base64-encoded screenshot of the current window.907908This encoding is useful for embedding screenshots in HTML.909910Example:911`driver.get_screenshot_as_base64()`912"""913return self.execute(Command.SCREENSHOT)["value"]914915def set_window_size(self, width, height, windowHandle: str = "current") -> None:916"""Sets the width and height of the current window.917918Args:919width: The width in pixels to set the window to.920height: The height in pixels to set the window to.921windowHandle: The handle of the window to resize. Default is "current".922923Example:924`driver.set_window_size(800, 600)`925"""926self._check_if_window_handle_is_current(windowHandle)927self.set_window_rect(width=int(width), height=int(height))928929def get_window_size(self, windowHandle: str = "current") -> dict:930"""Gets the width and height of the current window.931932Example:933`driver.get_window_size()`934"""935self._check_if_window_handle_is_current(windowHandle)936size = self.get_window_rect()937938if size.get("value", None):939size = size["value"]940941return {k: size[k] for k in ("width", "height")}942943def set_window_position(self, x: float, y: float, windowHandle: str = "current") -> dict:944"""Sets the x,y position of the current window.945946Args:947x: The x-coordinate in pixels to set the window position.948y: The y-coordinate in pixels to set the window position.949windowHandle: The handle of the window to reposition. Default is "current".950951Example:952`driver.set_window_position(0, 0)`953"""954self._check_if_window_handle_is_current(windowHandle)955return self.set_window_rect(x=int(x), y=int(y))956957def get_window_position(self, windowHandle="current") -> dict:958"""Gets the x,y position of the current window.959960Example:961`driver.get_window_position()`962"""963self._check_if_window_handle_is_current(windowHandle)964position = self.get_window_rect()965966return {k: position[k] for k in ("x", "y")}967968def _check_if_window_handle_is_current(self, windowHandle: str) -> None:969"""Warns if the window handle is not equal to `current`."""970if windowHandle != "current":971warnings.warn("Only 'current' window is supported for W3C compatible browsers.", stacklevel=2)972973def get_window_rect(self) -> dict:974"""Get the window's position and size.975976Returns:977x, y coordinates and height and width of the current window.978979Example:980`driver.get_window_rect()`981"""982return self.execute(Command.GET_WINDOW_RECT)["value"]983984def set_window_rect(self, x=None, y=None, width=None, height=None) -> dict:985"""Set the window's position and size.986987Sets the x, y coordinates and height and width of the current window.988This method is only supported for W3C compatible browsers; other browsers989should use `set_window_position` and `set_window_size`.990991Example:992`driver.set_window_rect(x=10, y=10)`993`driver.set_window_rect(width=100, height=200)`994`driver.set_window_rect(x=10, y=10, width=100, height=200)`995"""996if (x is None and y is None) and (not height and not width):997raise InvalidArgumentException("x and y or height and width need values")998999return self.execute(Command.SET_WINDOW_RECT, {"x": x, "y": y, "width": width, "height": height})["value"]10001001@property1002def file_detector(self) -> FileDetector:1003return self._file_detector10041005@file_detector.setter1006def file_detector(self, detector) -> None:1007"""Set the file detector for keyboard input.10081009By default, this is set to a file detector that does nothing.1010See FileDetector, LocalFileDetector, and UselessFileDetector.10111012Args:1013detector: The detector to use. Must not be None.1014"""1015if not detector:1016raise WebDriverException("You may not set a file detector that is null")1017if not isinstance(detector, FileDetector):1018raise WebDriverException("Detector has to be instance of FileDetector")1019self._file_detector = detector10201021@property1022def orientation(self) -> dict:1023"""Gets the current orientation of the device.10241025Example:1026`orientation = driver.orientation`1027"""1028return self.execute(Command.GET_SCREEN_ORIENTATION)["value"]10291030@orientation.setter1031def orientation(self, value) -> None:1032"""Sets the current orientation of the device.10331034Args:1035value: Orientation to set it to.10361037Example:1038`driver.orientation = "landscape"`1039"""1040allowed_values = ["LANDSCAPE", "PORTRAIT"]1041if value.upper() in allowed_values:1042self.execute(Command.SET_SCREEN_ORIENTATION, {"orientation": value})1043else:1044raise WebDriverException("You can only set the orientation to 'LANDSCAPE' and 'PORTRAIT'")10451046def start_devtools(self) -> tuple[Any, WebSocketConnection]:1047global cdp1048import_cdp()1049if self.caps.get("se:cdp"):1050ws_url = self.caps.get("se:cdp")1051cdp_version = self.caps.get("se:cdpVersion")1052if cdp_version is None:1053raise WebDriverException("CDP version not found in capabilities")1054version = cdp_version.split(".")[0]1055else:1056version, ws_url = self._get_cdp_details()10571058if not ws_url:1059raise WebDriverException("Unable to find url to connect to from capabilities")10601061if cdp is None:1062raise WebDriverException("CDP module not loaded")10631064self._devtools = cdp.import_devtools(version)1065if self._websocket_connection:1066return self._devtools, self._websocket_connection1067if self.caps["browserName"].lower() == "firefox":1068raise RuntimeError("CDP support for Firefox has been removed. Please switch to WebDriver BiDi.")1069if not isinstance(self.command_executor, RemoteConnection):1070raise WebDriverException("command_executor must be a RemoteConnection instance for CDP support")1071self._websocket_connection = WebSocketConnection(1072ws_url,1073self.command_executor.client_config.websocket_timeout,1074self.command_executor.client_config.websocket_interval,1075)1076targets = self._websocket_connection.execute(self._devtools.target.get_targets())1077for target in targets:1078if target.target_id == self.current_window_handle:1079target_id = target.target_id1080break1081session = self._websocket_connection.execute(self._devtools.target.attach_to_target(target_id, True))1082self._websocket_connection.session_id = session1083return self._devtools, self._websocket_connection10841085@asynccontextmanager1086async def bidi_connection(self):1087global cdp1088import_cdp()1089if self.caps.get("se:cdp"):1090ws_url = self.caps.get("se:cdp")1091version = self.caps.get("se:cdpVersion").split(".")[0]1092else:1093version, ws_url = self._get_cdp_details()10941095if not ws_url:1096raise WebDriverException("Unable to find url to connect to from capabilities")10971098devtools = cdp.import_devtools(version)1099async with cdp.open_cdp(ws_url) as conn:1100targets = await conn.execute(devtools.target.get_targets())1101for target in targets:1102if target.target_id == self.current_window_handle:1103target_id = target.target_id1104break1105async with conn.open_session(target_id) as session:1106yield BidiConnection(session, cdp, devtools)11071108@property1109def script(self) -> Script:1110if not self._websocket_connection:1111self._start_bidi()11121113if not self._script:1114self._script = Script(self._websocket_connection, self)11151116return self._script11171118def _start_bidi(self) -> None:1119if self.caps.get("webSocketUrl"):1120ws_url = self.caps.get("webSocketUrl")1121else:1122raise WebDriverException("Unable to find url to connect to from capabilities")11231124if not isinstance(self.command_executor, RemoteConnection):1125raise WebDriverException("command_executor must be a RemoteConnection instance for BiDi support")11261127self._websocket_connection = WebSocketConnection(1128ws_url,1129self.command_executor.client_config.websocket_timeout,1130self.command_executor.client_config.websocket_interval,1131)11321133@property1134def network(self) -> Network:1135if not self._websocket_connection:1136self._start_bidi()11371138assert self._websocket_connection is not None1139if not hasattr(self, "_network") or self._network is None:1140assert self._websocket_connection is not None1141self._network = Network(self._websocket_connection)11421143return self._network11441145@property1146def browser(self) -> Browser:1147"""Returns a browser module object for BiDi browser commands.11481149Returns:1150An object containing access to BiDi browser commands.11511152Examples:1153`user_context = driver.browser.create_user_context()`1154`user_contexts = driver.browser.get_user_contexts()`1155`client_windows = driver.browser.get_client_windows()`1156`driver.browser.remove_user_context(user_context)`1157"""1158if not self._websocket_connection:1159self._start_bidi()11601161if self._browser is None:1162self._browser = Browser(self._websocket_connection)11631164return self._browser11651166@property1167def _session(self) -> Session:1168"""Returns the BiDi session object for the current WebDriver session."""1169if not self._websocket_connection:1170self._start_bidi()11711172if self._bidi_session is None:1173self._bidi_session = Session(self._websocket_connection)11741175return self._bidi_session11761177@property1178def browsing_context(self) -> BrowsingContext:1179"""Returns a browsing context module object for BiDi browsing context commands.11801181Returns:1182An object containing access to BiDi browsing context commands.11831184Examples:1185`context_id = driver.browsing_context.create(type="tab")`1186`driver.browsing_context.navigate(context=context_id, url="https://www.selenium.dev")`1187`driver.browsing_context.capture_screenshot(context=context_id)`1188`driver.browsing_context.close(context_id)`1189"""1190if not self._websocket_connection:1191self._start_bidi()11921193if self._browsing_context is None:1194self._browsing_context = BrowsingContext(self._websocket_connection)11951196return self._browsing_context11971198@property1199def storage(self) -> Storage:1200"""Returns a storage module object for BiDi storage commands.12011202Returns:1203An object containing access to BiDi storage commands.12041205Examples:1206```1207cookie_filter = CookieFilter(name="example")1208result = driver.storage.get_cookies(filter=cookie_filter)1209cookie=PartialCookie("name", BytesValue(BytesValue.TYPE_STRING, "value")1210driver.storage.set_cookie(cookie=cookie, "domain"))1211cookie_filter=CookieFilter(name="example")1212driver.storage.delete_cookies(filter=cookie_filter)1213```1214"""1215if not self._websocket_connection:1216self._start_bidi()12171218assert self._websocket_connection is not None1219if self._storage is None:1220self._storage = Storage(self._websocket_connection)12211222return self._storage12231224@property1225def permissions(self) -> Permissions:1226"""Get a permissions module object for BiDi permissions commands.12271228Returns:1229An object containing access to BiDi permissions commands.12301231Examples:1232```1233from selenium.webdriver.common.bidi.permissions import PermissionDescriptor, PermissionState12341235descriptor = PermissionDescriptor("geolocation")1236driver.permissions.set_permission(descriptor, PermissionState.GRANTED, "https://example.com")1237```1238"""1239if not self._websocket_connection:1240self._start_bidi()12411242if self._permissions is None:1243self._permissions = Permissions(self._websocket_connection)12441245return self._permissions12461247@property1248def webextension(self) -> WebExtension:1249"""Get a webextension module object for BiDi webextension commands.12501251Returns:1252An object containing access to BiDi webextension commands.12531254Examples:1255`extension_path = "/path/to/extension"`1256`extension_result = driver.webextension.install(path=extension_path)`1257`driver.webextension.uninstall(extension_result)`1258"""1259if not self._websocket_connection:1260self._start_bidi()12611262if self._webextension is None:1263self._webextension = WebExtension(self._websocket_connection)12641265return self._webextension12661267@property1268def emulation(self) -> Emulation:1269"""Get an emulation module object for BiDi emulation commands.12701271Returns:1272An object containing access to BiDi emulation commands.12731274Examples:1275```1276from selenium.webdriver.common.bidi.emulation import GeolocationCoordinates12771278coordinates = GeolocationCoordinates(37.7749, -122.4194)1279driver.emulation.set_geolocation_override(coordinates=coordinates, contexts=[context_id])1280```1281"""1282if not self._websocket_connection:1283self._start_bidi()12841285assert self._websocket_connection is not None1286if self._emulation is None:1287self._emulation = Emulation(self._websocket_connection)12881289return self._emulation12901291@property1292def input(self) -> Input:1293"""Get an input module object for BiDi input commands.12941295Returns:1296An object containing access to BiDi input commands.12971298Examples:1299```1300from selenium.webdriver.common.bidi.input import KeySourceActions, KeyDownAction, KeyUpAction13011302actions = KeySourceActions(id="keyboard", actions=[KeyDownAction(value="a"), KeyUpAction(value="a")])1303driver.input.perform_actions(driver.current_window_handle, [actions])1304driver.input.release_actions(driver.current_window_handle)1305```1306"""1307if not self._websocket_connection:1308self._start_bidi()13091310if self._input is None:1311self._input = Input(self._websocket_connection)13121313return self._input13141315def _get_cdp_details(self):1316import json13171318import urllib313191320http = urllib3.PoolManager()1321try:1322if self.caps.get("browserName") == "chrome":1323debugger_address = self.caps.get("goog:chromeOptions").get("debuggerAddress")1324elif self.caps.get("browserName") in ("MicrosoftEdge", "webview2"):1325debugger_address = self.caps.get("ms:edgeOptions").get("debuggerAddress")1326except AttributeError:1327raise WebDriverException("Can't get debugger address.")13281329res = http.request("GET", f"http://{debugger_address}/json/version")1330data = json.loads(res.data)13311332browser_version = data.get("Browser")1333websocket_url = data.get("webSocketDebuggerUrl")13341335import re13361337version = re.search(r".*/(\d+)\.", browser_version).group(1)13381339return version, websocket_url13401341# Virtual Authenticator Methods1342def add_virtual_authenticator(self, options: VirtualAuthenticatorOptions) -> None:1343"""Adds a virtual authenticator with the given options.13441345Example:1346```1347from selenium.webdriver.common.virtual_authenticator import VirtualAuthenticatorOptions13481349options = VirtualAuthenticatorOptions(protocol="u2f", transport="usb", device_id="myDevice123")1350driver.add_virtual_authenticator(options)1351```1352"""1353self._authenticator_id = self.execute(Command.ADD_VIRTUAL_AUTHENTICATOR, options.to_dict())["value"]13541355@property1356def virtual_authenticator_id(self) -> str | None:1357"""Returns the id of the virtual authenticator."""1358return self._authenticator_id13591360@required_virtual_authenticator1361def remove_virtual_authenticator(self) -> None:1362"""Removes a previously added virtual authenticator.13631364The authenticator is no longer valid after removal, so no1365methods may be called.1366"""1367self.execute(Command.REMOVE_VIRTUAL_AUTHENTICATOR, {"authenticatorId": self._authenticator_id})1368self._authenticator_id = None13691370@required_virtual_authenticator1371def add_credential(self, credential: Credential) -> None:1372"""Injects a credential into the authenticator.13731374Example:1375```1376from selenium.webdriver.common.credential import Credential13771378credential = Credential(id="[email protected]", password="aPassword")1379driver.add_credential(credential)1380```1381"""1382self.execute(Command.ADD_CREDENTIAL, {**credential.to_dict(), "authenticatorId": self._authenticator_id})13831384@required_virtual_authenticator1385def get_credentials(self) -> list[Credential]:1386"""Returns the list of credentials owned by the authenticator."""1387credential_data = self.execute(Command.GET_CREDENTIALS, {"authenticatorId": self._authenticator_id})1388return [Credential.from_dict(credential) for credential in credential_data["value"]]13891390@required_virtual_authenticator1391def remove_credential(self, credential_id: str | bytearray) -> None:1392"""Removes a credential from the authenticator.13931394Example:1395`credential_id = "[email protected]"`1396`driver.remove_credential(credential_id)`1397"""1398# Check if the credential is bytearray converted to b64 string1399if isinstance(credential_id, bytearray):1400credential_id = urlsafe_b64encode(credential_id).decode()14011402self.execute(1403Command.REMOVE_CREDENTIAL, {"credentialId": credential_id, "authenticatorId": self._authenticator_id}1404)14051406@required_virtual_authenticator1407def remove_all_credentials(self) -> None:1408"""Removes all credentials from the authenticator."""1409self.execute(Command.REMOVE_ALL_CREDENTIALS, {"authenticatorId": self._authenticator_id})14101411@required_virtual_authenticator1412def set_user_verified(self, verified: bool) -> None:1413"""Set whether the authenticator will simulate success or failure on user verification.14141415Args:1416verified: True if the authenticator will pass user verification,1417False otherwise.14181419Example:1420`driver.set_user_verified(True)`1421"""1422self.execute(Command.SET_USER_VERIFIED, {"authenticatorId": self._authenticator_id, "isUserVerified": verified})14231424def get_downloadable_files(self) -> list:1425"""Retrieves the downloadable files as a list of file names."""1426if "se:downloadsEnabled" not in self.capabilities:1427raise WebDriverException("You must enable downloads in order to work with downloadable files.")14281429return self.execute(Command.GET_DOWNLOADABLE_FILES)["value"]["names"]14301431def download_file(self, file_name: str, target_directory: str) -> None:1432"""Download a file with the specified file name to the target directory.14331434Args:1435file_name: The name of the file to download.1436target_directory: The path to the directory to save the downloaded file.14371438Example:1439`driver.download_file("example.zip", "/path/to/directory")`1440"""1441if "se:downloadsEnabled" not in self.capabilities:1442raise WebDriverException("You must enable downloads in order to work with downloadable files.")14431444if not os.path.exists(target_directory):1445os.makedirs(target_directory)14461447contents = self.execute(Command.DOWNLOAD_FILE, {"name": file_name})["value"]["contents"]14481449with tempfile.TemporaryDirectory() as tmp_dir:1450zip_file = os.path.join(tmp_dir, file_name + ".zip")1451with open(zip_file, "wb") as file:1452file.write(base64.b64decode(contents))14531454with zipfile.ZipFile(zip_file, "r") as zip_ref:1455zip_ref.extractall(target_directory)14561457def delete_downloadable_files(self) -> None:1458"""Deletes all downloadable files."""1459if "se:downloadsEnabled" not in self.capabilities:1460raise WebDriverException("You must enable downloads in order to work with downloadable files.")14611462self.execute(Command.DELETE_DOWNLOADABLE_FILES)14631464def fire_session_event(self, event_type: str, payload: dict | None = None) -> dict:1465"""Fire a custom session event to the remote server event bus.14661467This allows test code to trigger server-side utilities that subscribe to1468the event bus.14691470Args:1471event_type: The type of event (e.g., "test:failed", "log:collect", "marker:add").1472payload: Optional data to include with the event.14731474Returns:1475A dictionary containing the response data including success status,1476event type, and timestamp.14771478Raises:1479WebDriverException: If the event cannot be fired.14801481Examples:1482Simple event::14831484driver.fire_session_event("test:started")14851486Event with payload::14871488driver.fire_session_event("test:failed", {"testName": "LoginTest", "error": "Element not found"})1489"""1490params: dict[str, str | dict] = {"eventType": event_type}1491if payload:1492params["payload"] = payload1493return self.execute(Command.FIRE_SESSION_EVENT, params)["value"]14941495@property1496def fedcm(self) -> FedCM:1497"""Get the Federated Credential Management (FedCM) dialog commands.14981499Returns:1500An object providing access to all Federated Credential Management1501(FedCM) dialog commands.15021503Examples:1504`driver.fedcm.title`1505`driver.fedcm.subtitle`1506`driver.fedcm.dialog_type`1507`driver.fedcm.account_list`1508`driver.fedcm.select_account(0)`1509`driver.fedcm.accept()`1510`driver.fedcm.dismiss()`1511`driver.fedcm.enable_delay()`1512`driver.fedcm.disable_delay()`1513`driver.fedcm.reset_cooldown()`1514"""1515return self._fedcm15161517@property1518def supports_fedcm(self) -> bool:1519"""Returns whether the browser supports FedCM capabilities."""1520return self.capabilities.get(ArgOptions.FEDCM_CAPABILITY, False)15211522def _require_fedcm_support(self) -> None:1523"""Raises an exception if FedCM is not supported."""1524if not self.supports_fedcm:1525raise WebDriverException(1526"This browser does not support Federated Credential Management. "1527"Please ensure you're using a supported browser."1528)15291530@property1531def dialog(self) -> Dialog:1532"""Returns the FedCM dialog object for interaction."""1533self._require_fedcm_support()1534return Dialog(self)15351536def fedcm_dialog(self, timeout=5, poll_frequency=0.5, ignored_exceptions=None):1537"""Waits for and returns the FedCM dialog.15381539Args:1540timeout: How long to wait for the dialog.1541poll_frequency: How frequently to poll.1542ignored_exceptions: Exceptions to ignore while waiting.15431544Returns:1545The FedCM dialog object if found.15461547Raises:1548TimeoutException: If dialog doesn't appear.1549WebDriverException: If FedCM not supported.1550"""1551from selenium.common.exceptions import NoAlertPresentException1552from selenium.webdriver.support.wait import WebDriverWait15531554self._require_fedcm_support()15551556if ignored_exceptions is None:1557ignored_exceptions = (NoAlertPresentException,)15581559def _check_fedcm() -> Dialog | None:1560try:1561dialog = Dialog(self)1562return dialog if dialog.type else None1563except NoAlertPresentException:1564return None15651566wait = WebDriverWait(self, timeout, poll_frequency=poll_frequency, ignored_exceptions=ignored_exceptions)1567return wait.until(lambda _: _check_fedcm())156815691570