Path: blob/trunk/py/selenium/webdriver/support/select.py
3991 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 a SELECT tag.2627Args:28webelement: SELECT element to wrap2930Example:31from selenium.webdriver.support.ui import Select32Select(driver.find_element(By.TAG_NAME, "select")).select_by_index(2)3334Raises:35UnexpectedTagNameException: If the element is not a SELECT tag36"""37if webelement.tag_name.lower() != "select":38raise UnexpectedTagNameException(f"Select only works on <select> elements, not on {webelement.tag_name}")39self._el = webelement40multi = self._el.get_dom_attribute("multiple")41self.is_multiple = multi and multi != "false"4243@property44def options(self) -> list[WebElement]:45"""Returns a list of all options belonging to this select tag."""46return self._el.find_elements(By.TAG_NAME, "option")4748@property49def all_selected_options(self) -> list[WebElement]:50"""Return a list of all selected options belonging to this select tag."""51return [opt for opt in self.options if opt.is_selected()]5253@property54def first_selected_option(self) -> WebElement:55"""Return the first selected option or the currently selected option."""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.6364Example:65When given "foo" this would select an option like:6667`<option value="foo">Bar</option>`6869Args:70value: The value to match against7172Raises:73NoSuchElementException: If there is no option with specified value in SELECT74"""75css = f"option[value ={self._escape_string(value)}]"76opts = self._el.find_elements(By.CSS_SELECTOR, css)77matched = False78for opt in opts:79self._set_selected(opt)80if not self.is_multiple:81return82matched = True83if not matched:84raise NoSuchElementException(f"Cannot locate option with value: {value}")8586def select_by_index(self, index: int) -> None:87"""Select the option at the given index by examining the "index" attribute.8889Args:90index: The option at this index will be selected9192Raises:93NoSuchElementException: If there is no option with specified index in SELECT94"""95match = str(index)96for opt in self.options:97if opt.get_attribute("index") == match:98self._set_selected(opt)99return100raise NoSuchElementException(f"Could not locate element with index {index}")101102def select_by_visible_text(self, text: str) -> None:103"""Select all options that display text matching the argument.104105Example:106When given "Bar" this would select an option like:107108`<option value="foo">Bar</option>`109110Args:111text: The visible text to match against112113Raises:114NoSuchElementException: If there is no option with specified text in SELECT115"""116xpath = f".//option[normalize-space(.) = {self._escape_string(text)}]"117opts = self._el.find_elements(By.XPATH, xpath)118matched = False119for opt in opts:120if not self._has_css_property_and_visible(opt):121raise NoSuchElementException(f"Invisible option with text: {text}")122self._set_selected(opt)123if not self.is_multiple:124return125matched = True126127if len(opts) == 0 and " " in text:128sub_string_without_space = self._get_longest_token(text)129if sub_string_without_space == "":130candidates = self.options131else:132xpath = f".//option[contains(.,{self._escape_string(sub_string_without_space)})]"133candidates = self._el.find_elements(By.XPATH, xpath)134for candidate in candidates:135if text == candidate.text:136if not self._has_css_property_and_visible(candidate):137raise NoSuchElementException(f"Invisible option with text: {text}")138self._set_selected(candidate)139if not self.is_multiple:140return141matched = True142143if not matched:144raise NoSuchElementException(f"Could not locate element with visible text: {text}")145146def deselect_all(self) -> None:147"""Clear all selected entries.148149This is only valid when the SELECT supports multiple selections.150throws NotImplementedError If the SELECT does not support151multiple selections152"""153if not self.is_multiple:154raise NotImplementedError("You may only deselect all options of a multi-select")155for opt in self.options:156self._unset_selected(opt)157158def deselect_by_value(self, value: str) -> None:159"""Deselect all options that have a value matching the argument.160161Example:162When given "foo" this would deselect an option like:163164`<option value="foo">Bar</option>`165166Args:167value: The value to match against168169Raises:170NoSuchElementException: If there is no option with specified value in SELECT171"""172if not self.is_multiple:173raise NotImplementedError("You may only deselect options of a multi-select")174matched = False175css = f"option[value = {self._escape_string(value)}]"176opts = self._el.find_elements(By.CSS_SELECTOR, css)177for opt in opts:178self._unset_selected(opt)179matched = True180if not matched:181raise NoSuchElementException(f"Could not locate element with value: {value}")182183def deselect_by_index(self, index: int) -> None:184"""Deselect the option at the given index by examining the "index" attribute.185186Args:187index: The option at this index will be deselected188189Raises:190NoSuchElementException: If there is no option with specified index in SELECT191"""192if not self.is_multiple:193raise NotImplementedError("You may only deselect options of a multi-select")194for opt in self.options:195if opt.get_attribute("index") == str(index):196self._unset_selected(opt)197return198raise NoSuchElementException(f"Could not locate element with index {index}")199200def deselect_by_visible_text(self, text: str) -> None:201"""Deselect all options that display text matching the argument.202203Example:204when given "Bar" this would deselect an option like:205206`<option value="foo">Bar</option>`207208Args:209text: The visible text to match against210"""211if not self.is_multiple:212raise NotImplementedError("You may only deselect options of a multi-select")213matched = False214xpath = f".//option[normalize-space(.) = {self._escape_string(text)}]"215opts = self._el.find_elements(By.XPATH, xpath)216for opt in opts:217if not self._has_css_property_and_visible(opt):218raise NoSuchElementException(f"Invisible option with text: {text}")219self._unset_selected(opt)220matched = True221if not matched:222raise NoSuchElementException(f"Could not locate element with visible text: {text}")223224def _set_selected(self, option) -> None:225if not option.is_selected():226if not option.is_enabled():227raise NotImplementedError("You may not select a disabled option")228option.click()229230def _unset_selected(self, option) -> None:231if option.is_selected():232option.click()233234def _escape_string(self, value: str) -> str:235if '"' in value and "'" in value:236substrings = value.split('"')237result = ["concat("]238for substring in substrings:239result.append(f'"{substring}"')240result.append(", '\"', ")241result = result[0:-1]242if value.endswith('"'):243result.append(", '\"'")244return "".join(result) + ")"245246if '"' in value:247return f"'{value}'"248249return f'"{value}"'250251def _get_longest_token(self, value: str) -> str:252items = value.split(" ")253longest = ""254for item in items:255if len(item) > len(longest):256longest = item257return longest258259def _has_css_property_and_visible(self, option) -> bool:260css_value_candidates = ["hidden", "none", "0", "0.0"]261css_property_candidates = ["visibility", "display", "opacity"]262263for css_property in css_property_candidates:264css_value = option.value_of_css_property(css_property)265if css_value in css_value_candidates:266return False267return True268269270