Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/py/selenium/webdriver/support/select.py
1864 views
1
# Licensed to the Software Freedom Conservancy (SFC) under one
2
# or more contributor license agreements. See the NOTICE file
3
# distributed with this work for additional information
4
# regarding copyright ownership. The SFC licenses this file
5
# to you under the Apache License, Version 2.0 (the
6
# "License"); you may not use this file except in compliance
7
# with the License. You may obtain a copy of the License at
8
#
9
# http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing,
12
# software distributed under the License is distributed on an
13
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
# KIND, either express or implied. See the License for the
15
# specific language governing permissions and limitations
16
# under the License.
17
18
19
from selenium.common.exceptions import NoSuchElementException, UnexpectedTagNameException
20
from selenium.webdriver.common.by import By
21
from selenium.webdriver.remote.webelement import WebElement
22
23
24
class Select:
25
def __init__(self, webelement: WebElement) -> None:
26
"""Constructor. A check is made that the given element is, indeed, a
27
SELECT tag. If it is not, then an UnexpectedTagNameException is thrown.
28
29
:Args:
30
- webelement - SELECT element to wrap
31
32
Example:
33
from selenium.webdriver.support.ui import Select \n
34
Select(driver.find_element(By.TAG_NAME, "select")).select_by_index(2)
35
"""
36
if webelement.tag_name.lower() != "select":
37
raise UnexpectedTagNameException(f"Select only works on <select> elements, not on {webelement.tag_name}")
38
self._el = webelement
39
multi = self._el.get_dom_attribute("multiple")
40
self.is_multiple = multi and multi != "false"
41
42
@property
43
def options(self) -> list[WebElement]:
44
"""Returns a list of all options belonging to this select tag."""
45
return self._el.find_elements(By.TAG_NAME, "option")
46
47
@property
48
def all_selected_options(self) -> list[WebElement]:
49
"""Returns a list of all selected options belonging to this select
50
tag."""
51
return [opt for opt in self.options if opt.is_selected()]
52
53
@property
54
def first_selected_option(self) -> WebElement:
55
"""The first selected option in this select tag (or the currently
56
selected option in a normal select)"""
57
for opt in self.options:
58
if opt.is_selected():
59
return opt
60
raise NoSuchElementException("No options are selected")
61
62
def select_by_value(self, value: str) -> None:
63
"""Select all options that have a value matching the argument. That is,
64
when given "foo" this would select an option like:
65
66
<option value="foo">Bar</option>
67
68
:Args:
69
- value - The value to match against
70
71
throws NoSuchElementException If there is no option with specified value in SELECT
72
"""
73
css = f"option[value ={self._escape_string(value)}]"
74
opts = self._el.find_elements(By.CSS_SELECTOR, css)
75
matched = False
76
for opt in opts:
77
self._set_selected(opt)
78
if not self.is_multiple:
79
return
80
matched = True
81
if not matched:
82
raise NoSuchElementException(f"Cannot locate option with value: {value}")
83
84
def select_by_index(self, index: int) -> None:
85
"""Select the option at the given index. This is done by examining the
86
"index" attribute of an element, and not merely by counting.
87
88
:Args:
89
- index - The option at this index will be selected
90
91
throws NoSuchElementException If there is no option with specified index in SELECT
92
"""
93
match = str(index)
94
for opt in self.options:
95
if opt.get_attribute("index") == match:
96
self._set_selected(opt)
97
return
98
raise NoSuchElementException(f"Could not locate element with index {index}")
99
100
def select_by_visible_text(self, text: str) -> None:
101
"""Select all options that display text matching the argument. That is,
102
when given "Bar" this would select an option like:
103
104
<option value="foo">Bar</option>
105
106
:Args:
107
- text - The visible text to match against
108
109
throws NoSuchElementException If there is no option with specified text in SELECT
110
"""
111
xpath = f".//option[normalize-space(.) = {self._escape_string(text)}]"
112
opts = self._el.find_elements(By.XPATH, xpath)
113
matched = False
114
for opt in opts:
115
if not self._has_css_property_and_visible(opt):
116
raise NoSuchElementException(f"Invisible option with text: {text}")
117
self._set_selected(opt)
118
if not self.is_multiple:
119
return
120
matched = True
121
122
if len(opts) == 0 and " " in text:
123
sub_string_without_space = self._get_longest_token(text)
124
if sub_string_without_space == "":
125
candidates = self.options
126
else:
127
xpath = f".//option[contains(.,{self._escape_string(sub_string_without_space)})]"
128
candidates = self._el.find_elements(By.XPATH, xpath)
129
for candidate in candidates:
130
if text == candidate.text:
131
if not self._has_css_property_and_visible(candidate):
132
raise NoSuchElementException(f"Invisible option with text: {text}")
133
self._set_selected(candidate)
134
if not self.is_multiple:
135
return
136
matched = True
137
138
if not matched:
139
raise NoSuchElementException(f"Could not locate element with visible text: {text}")
140
141
def deselect_all(self) -> None:
142
"""Clear all selected entries.
143
144
This is only valid when the SELECT supports multiple selections.
145
throws NotImplementedError If the SELECT does not support
146
multiple selections
147
"""
148
if not self.is_multiple:
149
raise NotImplementedError("You may only deselect all options of a multi-select")
150
for opt in self.options:
151
self._unset_selected(opt)
152
153
def deselect_by_value(self, value: str) -> None:
154
"""Deselect all options that have a value matching the argument. That
155
is, when given "foo" this would deselect an option like:
156
157
<option value="foo">Bar</option>
158
159
:Args:
160
- value - The value to match against
161
162
throws NoSuchElementException If there is no option with specified value in SELECT
163
"""
164
if not self.is_multiple:
165
raise NotImplementedError("You may only deselect options of a multi-select")
166
matched = False
167
css = f"option[value = {self._escape_string(value)}]"
168
opts = self._el.find_elements(By.CSS_SELECTOR, css)
169
for opt in opts:
170
self._unset_selected(opt)
171
matched = True
172
if not matched:
173
raise NoSuchElementException(f"Could not locate element with value: {value}")
174
175
def deselect_by_index(self, index: int) -> None:
176
"""Deselect the option at the given index. This is done by examining
177
the "index" attribute of an element, and not merely by counting.
178
179
:Args:
180
- index - The option at this index will be deselected
181
182
throws NoSuchElementException If there is no option with specified index in SELECT
183
"""
184
if not self.is_multiple:
185
raise NotImplementedError("You may only deselect options of a multi-select")
186
for opt in self.options:
187
if opt.get_attribute("index") == str(index):
188
self._unset_selected(opt)
189
return
190
raise NoSuchElementException(f"Could not locate element with index {index}")
191
192
def deselect_by_visible_text(self, text: str) -> None:
193
"""Deselect all options that display text matching the argument. That
194
is, when given "Bar" this would deselect an option like:
195
196
<option value="foo">Bar</option>
197
198
:Args:
199
- text - The visible text to match against
200
"""
201
if not self.is_multiple:
202
raise NotImplementedError("You may only deselect options of a multi-select")
203
matched = False
204
xpath = f".//option[normalize-space(.) = {self._escape_string(text)}]"
205
opts = self._el.find_elements(By.XPATH, xpath)
206
for opt in opts:
207
if not self._has_css_property_and_visible(opt):
208
raise NoSuchElementException(f"Invisible option with text: {text}")
209
self._unset_selected(opt)
210
matched = True
211
if not matched:
212
raise NoSuchElementException(f"Could not locate element with visible text: {text}")
213
214
def _set_selected(self, option) -> None:
215
if not option.is_selected():
216
if not option.is_enabled():
217
raise NotImplementedError("You may not select a disabled option")
218
option.click()
219
220
def _unset_selected(self, option) -> None:
221
if option.is_selected():
222
option.click()
223
224
def _escape_string(self, value: str) -> str:
225
if '"' in value and "'" in value:
226
substrings = value.split('"')
227
result = ["concat("]
228
for substring in substrings:
229
result.append(f'"{substring}"')
230
result.append(", '\"', ")
231
result = result[0:-1]
232
if value.endswith('"'):
233
result.append(", '\"'")
234
return "".join(result) + ")"
235
236
if '"' in value:
237
return f"'{value}'"
238
239
return f'"{value}"'
240
241
def _get_longest_token(self, value: str) -> str:
242
items = value.split(" ")
243
longest = ""
244
for item in items:
245
if len(item) > len(longest):
246
longest = item
247
return longest
248
249
def _has_css_property_and_visible(self, option) -> bool:
250
css_value_candidates = ["hidden", "none", "0", "0.0"]
251
css_property_candidates = ["visibility", "display", "opacity"]
252
253
for property in css_property_candidates:
254
css_value = option.value_of_css_property(property)
255
if css_value in css_value_candidates:
256
return False
257
return True
258
259