Path: blob/trunk/py/selenium/webdriver/support/select.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.161718from selenium.common.exceptions import NoSuchElementException, UnexpectedTagNameException19from selenium.webdriver.common.by import By20from selenium.webdriver.remote.webelement import WebElement212223class Select:24def __init__(self, webelement: WebElement) -> None:25"""Constructor. A check is made that the given element is, indeed, a26SELECT tag. If it is not, then an UnexpectedTagNameException is thrown.2728:Args:29- webelement - SELECT element to wrap3031Example:32from selenium.webdriver.support.ui import Select \n33Select(driver.find_element(By.TAG_NAME, "select")).select_by_index(2)34"""35if webelement.tag_name.lower() != "select":36raise UnexpectedTagNameException(f"Select only works on <select> elements, not on {webelement.tag_name}")37self._el = webelement38multi = self._el.get_dom_attribute("multiple")39self.is_multiple = multi and multi != "false"4041@property42def options(self) -> list[WebElement]:43"""Returns a list of all options belonging to this select tag."""44return self._el.find_elements(By.TAG_NAME, "option")4546@property47def all_selected_options(self) -> list[WebElement]:48"""Returns a list of all selected options belonging to this select49tag."""50return [opt for opt in self.options if opt.is_selected()]5152@property53def first_selected_option(self) -> WebElement:54"""The first selected option in this select tag (or the currently55selected option in a normal select)"""56for opt in self.options:57if opt.is_selected():58return opt59raise NoSuchElementException("No options are selected")6061def select_by_value(self, value: str) -> None:62"""Select all options that have a value matching the argument. That is,63when given "foo" this would select an option like:6465<option value="foo">Bar</option>6667:Args:68- value - The value to match against6970throws NoSuchElementException If there is no option with specified value in SELECT71"""72css = f"option[value ={self._escape_string(value)}]"73opts = self._el.find_elements(By.CSS_SELECTOR, css)74matched = False75for opt in opts:76self._set_selected(opt)77if not self.is_multiple:78return79matched = True80if not matched:81raise NoSuchElementException(f"Cannot locate option with value: {value}")8283def select_by_index(self, index: int) -> None:84"""Select the option at the given index. This is done by examining the85"index" attribute of an element, and not merely by counting.8687:Args:88- index - The option at this index will be selected8990throws NoSuchElementException If there is no option with specified index in SELECT91"""92match = str(index)93for opt in self.options:94if opt.get_attribute("index") == match:95self._set_selected(opt)96return97raise NoSuchElementException(f"Could not locate element with index {index}")9899def select_by_visible_text(self, text: str) -> None:100"""Select all options that display text matching the argument. That is,101when given "Bar" this would select an option like:102103<option value="foo">Bar</option>104105:Args:106- text - The visible text to match against107108throws NoSuchElementException If there is no option with specified text in SELECT109"""110xpath = f".//option[normalize-space(.) = {self._escape_string(text)}]"111opts = self._el.find_elements(By.XPATH, xpath)112matched = False113for opt in opts:114if not self._has_css_property_and_visible(opt):115raise NoSuchElementException(f"Invisible option with text: {text}")116self._set_selected(opt)117if not self.is_multiple:118return119matched = True120121if len(opts) == 0 and " " in text:122sub_string_without_space = self._get_longest_token(text)123if sub_string_without_space == "":124candidates = self.options125else:126xpath = f".//option[contains(.,{self._escape_string(sub_string_without_space)})]"127candidates = self._el.find_elements(By.XPATH, xpath)128for candidate in candidates:129if text == candidate.text:130if not self._has_css_property_and_visible(candidate):131raise NoSuchElementException(f"Invisible option with text: {text}")132self._set_selected(candidate)133if not self.is_multiple:134return135matched = True136137if not matched:138raise NoSuchElementException(f"Could not locate element with visible text: {text}")139140def deselect_all(self) -> None:141"""Clear all selected entries.142143This is only valid when the SELECT supports multiple selections.144throws NotImplementedError If the SELECT does not support145multiple selections146"""147if not self.is_multiple:148raise NotImplementedError("You may only deselect all options of a multi-select")149for opt in self.options:150self._unset_selected(opt)151152def deselect_by_value(self, value: str) -> None:153"""Deselect all options that have a value matching the argument. That154is, when given "foo" this would deselect an option like:155156<option value="foo">Bar</option>157158:Args:159- value - The value to match against160161throws NoSuchElementException If there is no option with specified value in SELECT162"""163if not self.is_multiple:164raise NotImplementedError("You may only deselect options of a multi-select")165matched = False166css = f"option[value = {self._escape_string(value)}]"167opts = self._el.find_elements(By.CSS_SELECTOR, css)168for opt in opts:169self._unset_selected(opt)170matched = True171if not matched:172raise NoSuchElementException(f"Could not locate element with value: {value}")173174def deselect_by_index(self, index: int) -> None:175"""Deselect the option at the given index. This is done by examining176the "index" attribute of an element, and not merely by counting.177178:Args:179- index - The option at this index will be deselected180181throws NoSuchElementException If there is no option with specified index in SELECT182"""183if not self.is_multiple:184raise NotImplementedError("You may only deselect options of a multi-select")185for opt in self.options:186if opt.get_attribute("index") == str(index):187self._unset_selected(opt)188return189raise NoSuchElementException(f"Could not locate element with index {index}")190191def deselect_by_visible_text(self, text: str) -> None:192"""Deselect all options that display text matching the argument. That193is, when given "Bar" this would deselect an option like:194195<option value="foo">Bar</option>196197:Args:198- text - The visible text to match against199"""200if not self.is_multiple:201raise NotImplementedError("You may only deselect options of a multi-select")202matched = False203xpath = f".//option[normalize-space(.) = {self._escape_string(text)}]"204opts = self._el.find_elements(By.XPATH, xpath)205for opt in opts:206if not self._has_css_property_and_visible(opt):207raise NoSuchElementException(f"Invisible option with text: {text}")208self._unset_selected(opt)209matched = True210if not matched:211raise NoSuchElementException(f"Could not locate element with visible text: {text}")212213def _set_selected(self, option) -> None:214if not option.is_selected():215if not option.is_enabled():216raise NotImplementedError("You may not select a disabled option")217option.click()218219def _unset_selected(self, option) -> None:220if option.is_selected():221option.click()222223def _escape_string(self, value: str) -> str:224if '"' in value and "'" in value:225substrings = value.split('"')226result = ["concat("]227for substring in substrings:228result.append(f'"{substring}"')229result.append(", '\"', ")230result = result[0:-1]231if value.endswith('"'):232result.append(", '\"'")233return "".join(result) + ")"234235if '"' in value:236return f"'{value}'"237238return f'"{value}"'239240def _get_longest_token(self, value: str) -> str:241items = value.split(" ")242longest = ""243for item in items:244if len(item) > len(longest):245longest = item246return longest247248def _has_css_property_and_visible(self, option) -> bool:249css_value_candidates = ["hidden", "none", "0", "0.0"]250css_property_candidates = ["visibility", "display", "opacity"]251252for property in css_property_candidates:253css_value = option.value_of_css_property(property)254if css_value in css_value_candidates:255return False256return True257258259