Path: blob/trunk/py/selenium/webdriver/remote/webelement.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.16from __future__ import annotations1718import os19import pkgutil20import warnings21import zipfile22from abc import ABCMeta23from base64 import b64decode, encodebytes24from hashlib import md5 as md5_hash25from io import BytesIO2627from selenium.common.exceptions import JavascriptException, WebDriverException28from selenium.webdriver.common.by import By29from selenium.webdriver.common.utils import keys_to_typing3031from .command import Command32from .shadowroot import ShadowRoot3334# TODO: When moving to supporting python 3.9 as the minimum version we can35# use built in importlib_resources.files.36getAttribute_js = None37isDisplayed_js = None383940def _load_js():41global getAttribute_js42global isDisplayed_js43_pkg = ".".join(__name__.split(".")[:-1])44getAttribute_js = pkgutil.get_data(_pkg, "getAttribute.js").decode("utf8")45isDisplayed_js = pkgutil.get_data(_pkg, "isDisplayed.js").decode("utf8")464748class BaseWebElement(metaclass=ABCMeta):49"""Abstract Base Class for WebElement.5051ABC's will allow custom types to be registered as a WebElement to52pass type checks.53"""5455pass565758class WebElement(BaseWebElement):59"""Represents a DOM element.6061Generally, all interesting operations that interact with a document will be62performed through this interface.6364All method calls will do a freshness check to ensure that the element65reference is still valid. This essentially determines whether the66element is still attached to the DOM. If this test fails, then an67``StaleElementReferenceException`` is thrown, and all future calls to this68instance will fail.69"""7071def __init__(self, parent, id_) -> None:72self._parent = parent73self._id = id_7475def __repr__(self):76return f'<{type(self).__module__}.{type(self).__name__} (session="{self.session_id}", element="{self._id}")>'7778@property79def session_id(self) -> str:80return self._parent.session_id8182@property83def tag_name(self) -> str:84"""This element's ``tagName`` property.8586Returns:87--------88str : The tag name of the element.8990Example:91--------92>>> element = driver.find_element(By.ID, "foo")93"""94return self._execute(Command.GET_ELEMENT_TAG_NAME)["value"]9596@property97def text(self) -> str:98"""The text of the element.99100Returns:101--------102str : The text of the element.103104Example:105--------106>>> element = driver.find_element(By.ID, "foo")107>>> print(element.text)108"""109return self._execute(Command.GET_ELEMENT_TEXT)["value"]110111def click(self) -> None:112"""Clicks the element.113114Example:115--------116>>> element = driver.find_element(By.ID, "foo")117>>> element.click()118"""119self._execute(Command.CLICK_ELEMENT)120121def submit(self) -> None:122"""Submits a form.123124Example:125--------126>>> form = driver.find_element(By.NAME, "login")127>>> form.submit()128"""129script = (130"/* submitForm */var form = arguments[0];\n"131'while (form.nodeName != "FORM" && form.parentNode) {\n'132" form = form.parentNode;\n"133"}\n"134"if (!form) { throw Error('Unable to find containing form element'); }\n"135"if (!form.ownerDocument) { throw Error('Unable to find owning document'); }\n"136"var e = form.ownerDocument.createEvent('Event');\n"137"e.initEvent('submit', true, true);\n"138"if (form.dispatchEvent(e)) { HTMLFormElement.prototype.submit.call(form) }\n"139)140141try:142self._parent.execute_script(script, self)143except JavascriptException as exc:144raise WebDriverException("To submit an element, it must be nested inside a form element") from exc145146def clear(self) -> None:147"""Clears the text if it's a text entry element.148149Example:150--------151>>> text_field = driver.find_element(By.NAME, "username")152>>> text_field.clear()153"""154self._execute(Command.CLEAR_ELEMENT)155156def get_property(self, name) -> str | bool | WebElement | dict:157"""Gets the given property of the element.158159Parameters:160-----------161name : str162- Name of the property to retrieve.163164Returns:165-------166str | bool | WebElement | dict : The value of the property.167168Example:169-------170>>> text_length = target_element.get_property("text_length")171"""172try:173return self._execute(Command.GET_ELEMENT_PROPERTY, {"name": name})["value"]174except WebDriverException:175# if we hit an end point that doesn't understand getElementProperty lets fake it176return self.parent.execute_script("return arguments[0][arguments[1]]", self, name)177178def get_dom_attribute(self, name) -> str:179"""Gets the given attribute of the element. Unlike180:func:`~selenium.webdriver.remote.BaseWebElement.get_attribute`, this181method only returns attributes declared in the element's HTML markup.182183Parameters:184-----------185name : str186- Name of the attribute to retrieve.187188Returns:189-------190str : The value of the attribute.191192Example:193-------194>>> text_length = target_element.get_dom_attribute("class")195"""196return self._execute(Command.GET_ELEMENT_ATTRIBUTE, {"name": name})["value"]197198def get_attribute(self, name) -> str | None:199"""Gets the given attribute or property of the element.200201This method will first try to return the value of a property with the202given name. If a property with that name doesn't exist, it returns the203value of the attribute with the same name. If there's no attribute with204that name, ``None`` is returned.205206Values which are considered truthy, that is equals "true" or "false",207are returned as booleans. All other non-``None`` values are returned208as strings. For attributes or properties which do not exist, ``None``209is returned.210211To obtain the exact value of the attribute or property,212use :func:`~selenium.webdriver.remote.BaseWebElement.get_dom_attribute` or213:func:`~selenium.webdriver.remote.BaseWebElement.get_property` methods respectively.214215Parameters:216-----------217name : str218- Name of the attribute/property to retrieve.219220Returns:221-------222str | bool | None : The value of the attribute/property.223224Example:225--------226>>> # Check if the "active" CSS class is applied to an element.227>>> is_active = "active" in target_element.get_attribute("class")228"""229if getAttribute_js is None:230_load_js()231attribute_value = self.parent.execute_script(232f"/* getAttribute */return ({getAttribute_js}).apply(null, arguments);", self, name233)234return attribute_value235236def is_selected(self) -> bool:237"""Returns whether the element is selected.238239Example:240--------241>>> is_selected = element.is_selected()242243Notes:244------245- This method is generally used on checkboxes, options in a select246and radio buttons.247"""248return self._execute(Command.IS_ELEMENT_SELECTED)["value"]249250def is_enabled(self) -> bool:251"""Returns whether the element is enabled.252253Example:254--------255>>> is_enabled = element.is_enabled()256"""257return self._execute(Command.IS_ELEMENT_ENABLED)["value"]258259def send_keys(self, *value: str) -> None:260"""Simulates typing into the element.261262Parameters:263-----------264value : str265- A string for typing, or setting form fields. For setting266file inputs, this could be a local file path.267268Notes:269------270- Use this to send simple key events or to fill out form fields271- This can also be used to set file inputs.272273Examples:274--------275To send a simple key event::276>>> form_textfield = driver.find_element(By.NAME, "username")277>>> form_textfield.send_keys("admin")278279or to set a file input field::280>>> file_input = driver.find_element(By.NAME, "profilePic")281>>> file_input.send_keys("path/to/profilepic.gif")282>>> # Generally it's better to wrap the file path in one of the methods283>>> # in os.path to return the actual path to support cross OS testing.284>>> # file_input.send_keys(os.path.abspath("path/to/profilepic.gif"))285>>> # When using Cygwin, the path need to be provided in Windows format.286>>> # file_input.send_keys(f"C:/cygwin{os.path.abspath('path/to/profilepic.gif').replace('/', '\\')}")287"""288# transfer file to another machine only if remote driver is used289# the same behaviour as for java binding290if self.parent._is_remote:291local_files = list(292map(293lambda keys_to_send: self.parent.file_detector.is_local_file(str(keys_to_send)),294"".join(map(str, value)).split("\n"),295)296)297if None not in local_files:298remote_files = []299for file in local_files:300remote_files.append(self._upload(file))301value = tuple("\n".join(remote_files))302303self._execute(304Command.SEND_KEYS_TO_ELEMENT, {"text": "".join(keys_to_typing(value)), "value": keys_to_typing(value)}305)306307@property308def shadow_root(self) -> ShadowRoot:309"""Returns a shadow root of the element if there is one or an error.310Only works from Chromium 96, Firefox 96, and Safari 16.4 onwards.311312Returns:313-------314ShadowRoot : object315316Raises:317-------318NoSuchShadowRoot - if no shadow root was attached to element319320Example:321--------322>>> try:323... shadow_root = element.shadow_root324>>> except NoSuchShadowRoot:325... print("No shadow root attached to element")326"""327return self._execute(Command.GET_SHADOW_ROOT)["value"]328329# RenderedWebElement Items330def is_displayed(self) -> bool:331"""Whether the element is visible to a user.332333Example:334--------335>>> is_displayed = element.is_displayed()336"""337# Only go into this conditional for browsers that don't use the atom themselves338if isDisplayed_js is None:339_load_js()340return self.parent.execute_script(f"/* isDisplayed */return ({isDisplayed_js}).apply(null, arguments);", self)341342@property343def location_once_scrolled_into_view(self) -> dict:344"""THIS PROPERTY MAY CHANGE WITHOUT WARNING. Use this to discover where345on the screen an element is so that we can click it. This method should346cause the element to be scrolled into view.347348Returns:349--------350dict: the top lefthand corner location on the screen, or zero351coordinates if the element is not visible.352353Example:354--------355>>> loc = element.location_once_scrolled_into_view356"""357old_loc = self._execute(358Command.W3C_EXECUTE_SCRIPT,359{360"script": "arguments[0].scrollIntoView(true); return arguments[0].getBoundingClientRect()",361"args": [self],362},363)["value"]364return {"x": round(old_loc["x"]), "y": round(old_loc["y"])}365366@property367def size(self) -> dict:368"""The size of the element.369370Returns:371--------372dict: The width and height of the element.373374Example:375--------376>>> size = element.size377"""378size = self._execute(Command.GET_ELEMENT_RECT)["value"]379new_size = {"height": size["height"], "width": size["width"]}380return new_size381382def value_of_css_property(self, property_name) -> str:383"""The value of a CSS property.384385Parameters:386-----------387property_name : str388- The name of the CSS property to get the value of.389390Returns:391--------392str : The value of the CSS property.393394Example:395--------396>>> value = element.value_of_css_property("color")397"""398return self._execute(Command.GET_ELEMENT_VALUE_OF_CSS_PROPERTY, {"propertyName": property_name})["value"]399400@property401def location(self) -> dict:402"""The location of the element in the renderable canvas.403404Returns:405--------406dict: The x and y coordinates of the element.407408Example:409--------410>>> loc = element.location411"""412old_loc = self._execute(Command.GET_ELEMENT_RECT)["value"]413new_loc = {"x": round(old_loc["x"]), "y": round(old_loc["y"])}414return new_loc415416@property417def rect(self) -> dict:418"""A dictionary with the size and location of the element.419420Returns:421--------422dict: The size and location of the element.423424Example:425--------426>>> rect = element.rect427"""428return self._execute(Command.GET_ELEMENT_RECT)["value"]429430@property431def aria_role(self) -> str:432"""Returns the ARIA role of the current web element.433434Returns:435--------436str : The ARIA role of the element.437438Example:439--------440>>> role = element.aria_role441"""442return self._execute(Command.GET_ELEMENT_ARIA_ROLE)["value"]443444@property445def accessible_name(self) -> str:446"""Returns the ARIA Level of the current webelement.447448Returns:449--------450str : The ARIA Level of the element.451452Example:453--------454>>> name = element.accessible_name455"""456return self._execute(Command.GET_ELEMENT_ARIA_LABEL)["value"]457458@property459def screenshot_as_base64(self) -> str:460"""Gets the screenshot of the current element as a base64 encoded461string.462463Returns:464--------465str : The screenshot of the element as a base64 encoded string.466467Example:468--------469>>> img_b64 = element.screenshot_as_base64470"""471return self._execute(Command.ELEMENT_SCREENSHOT)["value"]472473@property474def screenshot_as_png(self) -> bytes:475"""Gets the screenshot of the current element as a binary data.476477Returns:478--------479bytes : The screenshot of the element as binary data.480481Example:482--------483>>> element_png = element.screenshot_as_png484"""485return b64decode(self.screenshot_as_base64.encode("ascii"))486487def screenshot(self, filename) -> bool:488"""Saves a screenshot of the current element to a PNG image file.489Returns False if there is any IOError, else returns True. Use full490paths in your filename.491492Returns:493--------494bool : True if the screenshot was saved successfully, False otherwise.495496Parameters:497-----------498filename : str499The full path you wish to save your screenshot to. This500should end with a `.png` extension.501502Element:503--------504>>> element.screenshot("/Screenshots/foo.png")505"""506if not filename.lower().endswith(".png"):507warnings.warn(508"name used for saved screenshot does not match file type. It should end with a `.png` extension",509UserWarning,510)511png = self.screenshot_as_png512try:513with open(filename, "wb") as f:514f.write(png)515except OSError:516return False517finally:518del png519return True520521@property522def parent(self):523"""Internal reference to the WebDriver instance this element was found524from.525526Example:527--------528>>> element = driver.find_element(By.ID, "foo")529>>> parent_element = element.parent530"""531return self._parent532533@property534def id(self) -> str:535"""Internal ID used by selenium.536537This is mainly for internal use. Simple use cases such as checking if 2538webelements refer to the same element, can be done using ``==``::539540Example:541--------542>>> if element1 == element2:543... print("These 2 are equal")544"""545return self._id546547def __eq__(self, element):548return hasattr(element, "id") and self._id == element.id549550def __ne__(self, element):551return not self.__eq__(element)552553# Private Methods554def _execute(self, command, params=None):555"""Executes a command against the underlying HTML element.556557Parameters:558-----------559command : any560The name of the command to _execute as a string.561562params : dict563A dictionary of named Parameters to send with the command.564565Returns:566-------567The command's JSON response loaded into a dictionary object.568"""569if not params:570params = {}571params["id"] = self._id572return self._parent.execute(command, params)573574def find_element(self, by=By.ID, value=None) -> WebElement:575"""Find an element given a By strategy and locator.576577Parameters:578-----------579by : selenium.webdriver.common.by.By580The locating strategy to use. Default is `By.ID`. Supported values include:581- By.ID: Locate by element ID.582- By.NAME: Locate by the `name` attribute.583- By.XPATH: Locate by an XPath expression.584- By.CSS_SELECTOR: Locate by a CSS selector.585- By.CLASS_NAME: Locate by the `class` attribute.586- By.TAG_NAME: Locate by the tag name (e.g., "input", "button").587- By.LINK_TEXT: Locate a link element by its exact text.588- By.PARTIAL_LINK_TEXT: Locate a link element by partial text match.589- RelativeBy: Locate elements relative to a specified root element.590591Example:592--------593element = driver.find_element(By.ID, 'foo')594595Returns:596-------597WebElement598The first matching `WebElement` found on the page.599"""600by, value = self._parent.locator_converter.convert(by, value)601return self._execute(Command.FIND_CHILD_ELEMENT, {"using": by, "value": value})["value"]602603def find_elements(self, by=By.ID, value=None) -> list[WebElement]:604"""Find elements given a By strategy and locator.605606Parameters:607-----------608by : selenium.webdriver.common.by.By609The locating strategy to use. Default is `By.ID`. Supported values include:610- By.ID: Locate by element ID.611- By.NAME: Locate by the `name` attribute.612- By.XPATH: Locate by an XPath expression.613- By.CSS_SELECTOR: Locate by a CSS selector.614- By.CLASS_NAME: Locate by the `class` attribute.615- By.TAG_NAME: Locate by the tag name (e.g., "input", "button").616- By.LINK_TEXT: Locate a link element by its exact text.617- By.PARTIAL_LINK_TEXT: Locate a link element by partial text match.618- RelativeBy: Locate elements relative to a specified root element.619620Example:621--------622>>> element = driver.find_elements(By.ID, "foo")623624Returns:625-------626List[WebElement]627list of `WebElements` matching locator strategy found on the page.628"""629by, value = self._parent.locator_converter.convert(by, value)630return self._execute(Command.FIND_CHILD_ELEMENTS, {"using": by, "value": value})["value"]631632def __hash__(self) -> int:633return int(md5_hash(self._id.encode("utf-8")).hexdigest(), 16)634635def _upload(self, filename):636fp = BytesIO()637zipped = zipfile.ZipFile(fp, "w", zipfile.ZIP_DEFLATED)638zipped.write(filename, os.path.split(filename)[1])639zipped.close()640content = encodebytes(fp.getvalue())641if not isinstance(content, str):642content = content.decode("utf-8")643try:644return self._execute(Command.UPLOAD_FILE, {"file": content})["value"]645except WebDriverException as e:646if "Unrecognized command: POST" in str(e):647return filename648if "Command not found: POST " in str(e):649return filename650if '{"status":405,"value":["GET","HEAD","DELETE"]}' in str(e):651return filename652raise653654655