Path: blob/trunk/py/selenium/webdriver/remote/webelement.py
3985 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.1617from __future__ import annotations1819import os20import pkgutil21import warnings22import zipfile23from abc import ABCMeta24from base64 import b64decode, encodebytes25from hashlib import md5 as md5_hash26from io import BytesIO2728from selenium.common.exceptions import JavascriptException, WebDriverException29from selenium.webdriver.common.by import By30from selenium.webdriver.common.utils import keys_to_typing31from selenium.webdriver.remote.command import Command32from selenium.webdriver.remote.shadowroot import ShadowRoot3334# TODO: Use built in importlib_resources.files.35getAttribute_js = None36isDisplayed_js = None373839def _load_js():40global getAttribute_js41global isDisplayed_js42_pkg = ".".join(__name__.split(".")[:-1])43getAttribute_js = pkgutil.get_data(_pkg, "getAttribute.js").decode("utf8")44isDisplayed_js = pkgutil.get_data(_pkg, "isDisplayed.js").decode("utf8")454647class BaseWebElement(metaclass=ABCMeta):48"""Abstract Base Class for WebElement.4950ABC's will allow custom types to be registered as a WebElement to51pass type checks.52"""5354pass555657class WebElement(BaseWebElement):58"""Represents a DOM element.5960Generally, all interesting operations that interact with a document will be61performed through this interface.6263All method calls will do a freshness check to ensure that the element64reference is still valid. This essentially determines whether the65element is still attached to the DOM. If this test fails, then an66`StaleElementReferenceException` is thrown, and all future calls to this67instance will fail.68"""6970def __init__(self, parent, id_) -> None:71self._parent = parent72self._id = id_7374def __repr__(self):75return f'<{type(self).__module__}.{type(self).__name__} (session="{self.session_id}", element="{self._id}")>'7677@property78def session_id(self) -> str:79return self._parent.session_id8081@property82def tag_name(self) -> str:83"""This element's `tagName` property.8485Returns:86The tag name of the element.8788Example:89element = driver.find_element(By.ID, "foo")90"""91return self._execute(Command.GET_ELEMENT_TAG_NAME)["value"]9293@property94def text(self) -> str:95"""The text of the element.9697Returns:98The text of the element.99100Example:101element = driver.find_element(By.ID, "foo")102print(element.text)103"""104return self._execute(Command.GET_ELEMENT_TEXT)["value"]105106def click(self) -> None:107"""Clicks the element.108109Example:110element = driver.find_element(By.ID, "foo")111element.click()112"""113self._execute(Command.CLICK_ELEMENT)114115def submit(self) -> None:116"""Submits a form.117118Example:119form = driver.find_element(By.NAME, "login")120form.submit()121"""122script = (123"/* submitForm */var form = arguments[0];\n"124'while (form.nodeName != "FORM" && form.parentNode) {\n'125" form = form.parentNode;\n"126"}\n"127"if (!form) { throw Error('Unable to find containing form element'); }\n"128"if (!form.ownerDocument) { throw Error('Unable to find owning document'); }\n"129"var e = form.ownerDocument.createEvent('Event');\n"130"e.initEvent('submit', true, true);\n"131"if (form.dispatchEvent(e)) { HTMLFormElement.prototype.submit.call(form) }\n"132)133134try:135self._parent.execute_script(script, self)136except JavascriptException as exc:137raise WebDriverException("To submit an element, it must be nested inside a form element") from exc138139def clear(self) -> None:140"""Clears the text if it's a text entry element.141142Example:143text_field = driver.find_element(By.NAME, "username")144text_field.clear()145"""146self._execute(Command.CLEAR_ELEMENT)147148def get_property(self, name) -> str | bool | WebElement | dict:149"""Gets the given property of the element.150151Args:152name: Name of the property to retrieve.153154Returns:155The value of the property.156157Example:158text_length = target_element.get_property("text_length")159"""160try:161return self._execute(Command.GET_ELEMENT_PROPERTY, {"name": name})["value"]162except WebDriverException:163# if we hit an end point that doesn't understand getElementProperty lets fake it164return self.parent.execute_script("return arguments[0][arguments[1]]", self, name)165166def get_dom_attribute(self, name) -> str:167"""Get the HTML attribute value (not reflected properties) of the element.168169Returns only attributes declared in the element's HTML markup, unlike170`selenium.webdriver.remote.BaseWebElement.get_attribute`.171172Args:173name: Name of the attribute to retrieve.174175Returns:176The value of the attribute.177178Example:179text_length = target_element.get_dom_attribute("class")180"""181return self._execute(Command.GET_ELEMENT_ATTRIBUTE, {"name": name})["value"]182183def get_attribute(self, name) -> str | None:184"""Gets the given attribute or property of the element.185186This method will first try to return the value of a property with the187given name. If a property with that name doesn't exist, it returns the188value of the attribute with the same name. If there's no attribute with189that name, ``None`` is returned.190191Values which are considered truthy, that is equals "true" or "false",192are returned as booleans. All other non-``None`` values are returned193as strings. For attributes or properties which do not exist, ``None``194is returned.195196To obtain the exact value of the attribute or property,197use :func:`~selenium.webdriver.remote.BaseWebElement.get_dom_attribute` or198:func:`~selenium.webdriver.remote.BaseWebElement.get_property` methods respectively.199200Args:201name: Name of the attribute/property to retrieve.202203Returns:204The value of the attribute/property.205206Example:207# Check if the "active" CSS class is applied to an element.208is_active = "active" in target_element.get_attribute("class")209"""210if getAttribute_js is None:211_load_js()212attribute_value = self.parent.execute_script(213f"/* getAttribute */return ({getAttribute_js}).apply(null, arguments);", self, name214)215return attribute_value216217def is_selected(self) -> bool:218"""Returns whether the element is selected.219220This method is generally used on checkboxes, options in a select221and radio buttons.222223Example:224is_selected = element.is_selected()225"""226return self._execute(Command.IS_ELEMENT_SELECTED)["value"]227228def is_enabled(self) -> bool:229"""Returns whether the element is enabled.230231Example:232is_enabled = element.is_enabled()233"""234return self._execute(Command.IS_ELEMENT_ENABLED)["value"]235236def send_keys(self, *value: str) -> None:237"""Simulates typing into the element.238239Use this to send simple key events or to fill out form fields.240This can also be used to set file inputs.241242Args:243value: A string for typing, or setting form fields. For setting244file inputs, this could be a local file path.245246Examples:247To send a simple key event::248249form_textfield = driver.find_element(By.NAME, "username")250form_textfield.send_keys("admin")251252or to set a file input field::253254file_input = driver.find_element(By.NAME, "profilePic")255file_input.send_keys("path/to/profilepic.gif")256# Generally it's better to wrap the file path in one of the methods257# in os.path to return the actual path to support cross OS testing.258# file_input.send_keys(os.path.abspath("path/to/profilepic.gif"))259"""260# transfer file to another machine only if remote driver is used261# the same behaviour as for java binding262if self.parent._is_remote:263local_files = list(264map(265lambda keys_to_send: self.parent.file_detector.is_local_file(str(keys_to_send)),266"".join(map(str, value)).split("\n"),267)268)269if None not in local_files:270remote_files = []271for file in local_files:272remote_files.append(self._upload(file))273value = tuple("\n".join(remote_files))274275self._execute(276Command.SEND_KEYS_TO_ELEMENT, {"text": "".join(keys_to_typing(value)), "value": keys_to_typing(value)}277)278279@property280def shadow_root(self) -> ShadowRoot:281"""Get the shadow root attached to this element if present (Chromium, Firefox, Safari).282283Returns:284The ShadowRoot object.285286Raises:287NoSuchShadowRoot: If no shadow root was attached to element.288289Example:290try:291shadow_root = element.shadow_root292except NoSuchShadowRoot:293print("No shadow root attached to element")294"""295return self._execute(Command.GET_SHADOW_ROOT)["value"]296297# RenderedWebElement Items298def is_displayed(self) -> bool:299"""Whether the element is visible to a user.300301Example:302is_displayed = element.is_displayed()303"""304# Only go into this conditional for browsers that don't use the atom themselves305if isDisplayed_js is None:306_load_js()307return self.parent.execute_script(f"/* isDisplayed */return ({isDisplayed_js}).apply(null, arguments);", self)308309@property310def location_once_scrolled_into_view(self) -> dict:311"""Get the element's location on screen after scrolling it into view.312313This may change without warning and scrolls the element into view314before calculating coordinates for clicking purposes.315316Returns:317The top lefthand corner location on the screen, or zero318coordinates if the element is not visible.319320Example:321loc = element.location_once_scrolled_into_view322"""323old_loc = self._execute(324Command.W3C_EXECUTE_SCRIPT,325{326"script": "arguments[0].scrollIntoView(true); return arguments[0].getBoundingClientRect()",327"args": [self],328},329)["value"]330return {"x": round(old_loc["x"]), "y": round(old_loc["y"])}331332@property333def size(self) -> dict:334"""Get the size of the element.335336Returns:337The width and height of the element.338339Example:340size = element.size341"""342size = self._execute(Command.GET_ELEMENT_RECT)["value"]343new_size = {"height": size["height"], "width": size["width"]}344return new_size345346def value_of_css_property(self, property_name) -> str:347"""Get the value of a CSS property.348349Args:350property_name: The name of the CSS property to get the value of.351352Returns:353The value of the CSS property.354355Example:356value = element.value_of_css_property("color")357"""358return self._execute(Command.GET_ELEMENT_VALUE_OF_CSS_PROPERTY, {"propertyName": property_name})["value"]359360@property361def location(self) -> dict:362"""Get the location of the element in the renderable canvas.363364Returns:365The x and y coordinates of the element.366367Example:368loc = element.location369"""370old_loc = self._execute(Command.GET_ELEMENT_RECT)["value"]371new_loc = {"x": round(old_loc["x"]), "y": round(old_loc["y"])}372return new_loc373374@property375def rect(self) -> dict:376"""Get the size and location of the element.377378Returns:379A dictionary with size and location of the element.380381Example:382rect = element.rect383"""384return self._execute(Command.GET_ELEMENT_RECT)["value"]385386@property387def aria_role(self) -> str:388"""Get the ARIA role of the current web element.389390Returns:391The ARIA role of the element.392393Example:394role = element.aria_role395"""396return self._execute(Command.GET_ELEMENT_ARIA_ROLE)["value"]397398@property399def accessible_name(self) -> str:400"""Get the ARIA Level of the current webelement.401402Returns:403The ARIA Level of the element.404405Example:406name = element.accessible_name407"""408return self._execute(Command.GET_ELEMENT_ARIA_LABEL)["value"]409410@property411def screenshot_as_base64(self) -> str:412"""Get a base64-encoded screenshot of the current element.413414Returns:415The screenshot of the element as a base64 encoded string.416417Example:418img_b64 = element.screenshot_as_base64419"""420return self._execute(Command.ELEMENT_SCREENSHOT)["value"]421422@property423def screenshot_as_png(self) -> bytes:424"""Get the screenshot of the current element as a binary data.425426Returns:427The screenshot of the element as binary data.428429Example:430element_png = element.screenshot_as_png431"""432return b64decode(self.screenshot_as_base64.encode("ascii"))433434def screenshot(self, filename) -> bool:435"""Save a PNG screenshot of the current element to a file.436437Use full paths in your filename.438439Args:440filename: The full path you wish to save your screenshot to. This441should end with a `.png` extension.442443Returns:444True if the screenshot was saved successfully, False otherwise.445446Example:447element.screenshot("/Screenshots/foo.png")448"""449if not filename.lower().endswith(".png"):450warnings.warn(451"name used for saved screenshot does not match file type. It should end with a `.png` extension",452UserWarning,453)454png = self.screenshot_as_png455try:456with open(filename, "wb") as f:457f.write(png)458except OSError:459return False460finally:461del png462return True463464@property465def parent(self):466"""Get the WebDriver instance this element was found from.467468Example:469element = driver.find_element(By.ID, "foo")470parent_element = element.parent471"""472return self._parent473474@property475def id(self) -> str:476"""Get the ID used by selenium.477478This is mainly for internal use. Simple use cases such as checking if 2479webelements refer to the same element, can be done using ``==``::480481Example:482if element1 == element2:483print("These 2 are equal")484"""485return self._id486487def __eq__(self, element):488return hasattr(element, "id") and self._id == element.id489490def __ne__(self, element):491return not self.__eq__(element)492493# Private Methods494def _execute(self, command, params=None):495"""Executes a command against the underlying HTML element.496497Args:498command: The name of the command to _execute as a string.499params: A dictionary of named Parameters to send with the command.500501Returns:502The command's JSON response loaded into a dictionary object.503"""504if not params:505params = {}506params["id"] = self._id507return self._parent.execute(command, params)508509def find_element(self, by: str = By.ID, value: str | None = None) -> WebElement:510"""Find an element given a By strategy and locator.511512Args:513by: The locating strategy to use. Default is `By.ID`. Supported values include:514- By.ID: Locate by element ID.515- By.NAME: Locate by the `name` attribute.516- By.XPATH: Locate by an XPath expression.517- By.CSS_SELECTOR: Locate by a CSS selector.518- By.CLASS_NAME: Locate by the `class` attribute.519- By.TAG_NAME: Locate by the tag name (e.g., "input", "button").520- By.LINK_TEXT: Locate a link element by its exact text.521- By.PARTIAL_LINK_TEXT: Locate a link element by partial text match.522value: The locator value to use with the specified `by` strategy.523524Returns:525The first matching `WebElement` found on the page.526527Example:528element = driver.find_element(By.ID, "foo")529"""530by, value = self._parent.locator_converter.convert(by, value)531return self._execute(Command.FIND_CHILD_ELEMENT, {"using": by, "value": value})["value"]532533def find_elements(self, by: str = By.ID, value: str | None = None) -> list[WebElement]:534"""Find elements given a By strategy and locator.535536Args:537by: The locating strategy to use. Default is `By.ID`. Supported values include:538- By.ID: Locate by element ID.539- By.NAME: Locate by the `name` attribute.540- By.XPATH: Locate by an XPath expression.541- By.CSS_SELECTOR: Locate by a CSS selector.542- By.CLASS_NAME: Locate by the `class` attribute.543- By.TAG_NAME: Locate by the tag name (e.g., "input", "button").544- By.LINK_TEXT: Locate a link element by its exact text.545- By.PARTIAL_LINK_TEXT: Locate a link element by partial text match.546value: The locator value to use with the specified `by` strategy.547548Returns:549List of `WebElements` matching locator strategy found on the page.550551Example:552element = driver.find_elements(By.ID, "foo")553"""554by, value = self._parent.locator_converter.convert(by, value)555return self._execute(Command.FIND_CHILD_ELEMENTS, {"using": by, "value": value})["value"]556557def __hash__(self) -> int:558return int(md5_hash(self._id.encode("utf-8")).hexdigest(), 16)559560def _upload(self, filename):561fp = BytesIO()562zipped = zipfile.ZipFile(fp, "w", zipfile.ZIP_DEFLATED)563zipped.write(filename, os.path.split(filename)[1])564zipped.close()565content = encodebytes(fp.getvalue())566if not isinstance(content, str):567content = content.decode("utf-8")568try:569return self._execute(Command.UPLOAD_FILE, {"file": content})["value"]570except WebDriverException as e:571if "Unrecognized command: POST" in str(e):572return filename573if "Command not found: POST " in str(e):574return filename575if '{"status":405,"value":["GET","HEAD","DELETE"]}' in str(e):576return filename577raise578579580