thirtyfour/components/
select.rs

1// This wrapper is a fairly direct port of the Select class from the python
2// selenium library at:
3// https://github.com/SeleniumHQ/selenium/blob/trunk/py/selenium/webdriver/support/select.py
4
5// Copyright 2021 Stephen Pryde and the thirtyfour contributors
6// Derived (and modified) from the Selenium project at https://github.com/SeleniumHQ/selenium.
7//
8// Copyright 2011-2020 Software Freedom Conservancy
9//
10// Licensed under the Apache License, Version 2.0 (the "License");
11// you may not use this file except in compliance with the License.
12// You may obtain a copy of the License at
13//
14//     http://www.apache.org/licenses/LICENSE-2.0
15//
16// Unless required by applicable law or agreed to in writing, software
17// distributed under the License is distributed on an "AS IS" BASIS,
18// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19// See the License for the specific language governing permissions and
20// limitations under the License.
21
22use crate::error::{no_such_element, WebDriverErrorInner, WebDriverResult};
23use crate::{By, WebElement};
24use std::fmt::{Display, Formatter};
25
26/// Set the selection state of the specified element.
27async fn set_selected(element: &WebElement, select: bool) -> WebDriverResult<()> {
28    if element.is_selected().await? != select {
29        element.click().await?;
30    }
31    Ok(())
32}
33
34struct Escaped<'a>(&'a str);
35
36impl Display for Escaped<'_> {
37    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
38        for (i, substring) in self.0.split('\"').enumerate() {
39            if i != 0 {
40                f.write_str(", '\"', ")?
41            }
42            write!(f, "\"{}\"", substring)?;
43        }
44        if self.0.ends_with('\"') {
45            f.write_str(", '\"'")?;
46        }
47        Ok(())
48    }
49}
50
51/// Escape the specified string for use in Css or XPath selector.
52pub fn escape_string(value: &str) -> String {
53    let contains_single = value.contains('\'');
54    let contains_double = value.contains('\"');
55    if contains_single && contains_double {
56        format!("concat({})", Escaped(value))
57    } else if contains_double {
58        format!("'{}'", value)
59    } else {
60        format!("\"{}\"", value)
61    }
62}
63
64/// Get the longest word in the specified string.
65fn get_longest_token(value: &str) -> &str {
66    value.split(' ').max_by_key(|x| x.len()).unwrap_or("")
67}
68
69/// Convenience wrapper for `<select>` elements.
70#[derive(Debug)]
71pub struct SelectElement {
72    element: WebElement,
73    multiple: bool,
74}
75
76impl SelectElement {
77    /// Instantiate a new SelectElement struct. The specified element must be a `<select>` element.
78    pub async fn new(element: &WebElement) -> WebDriverResult<SelectElement> {
79        let multiple = element.attr("multiple").await?.filter(|x| x != "false").is_some();
80        let element = element.clone();
81        Ok(SelectElement {
82            element,
83            multiple,
84        })
85    }
86
87    /// Return a vec of all options belonging to this select tag.
88    pub async fn options(&self) -> WebDriverResult<Vec<WebElement>> {
89        self.element.find_all(By::Tag("option")).await
90    }
91
92    /// Return a vec of all selected options belonging to this select tag.
93    pub async fn all_selected_options(&self) -> WebDriverResult<Vec<WebElement>> {
94        let mut selected = Vec::new();
95        for option in self.options().await? {
96            if option.is_selected().await? {
97                selected.push(option);
98            }
99        }
100        Ok(selected)
101    }
102
103    /// Return the first selected option in this select tag.
104    pub async fn first_selected_option(&self) -> WebDriverResult<WebElement> {
105        for option in self.options().await? {
106            if option.is_selected().await? {
107                return Ok(option);
108            }
109        }
110        Err(no_such_element("No options are selected".to_string()))
111    }
112
113    /// Set selection state for all options.
114    async fn set_selection_all(&self, select: bool) -> WebDriverResult<()> {
115        for option in self.options().await? {
116            set_selected(&option, select).await?;
117        }
118        Ok(())
119    }
120
121    /// Set the selection state of options matching the specified value.
122    async fn set_selection_by_value(&self, value: &str, select: bool) -> WebDriverResult<()> {
123        let selector = format!("option[value={}]", escape_string(value));
124        let options = self.element.find_all(By::Css(&*selector)).await?;
125        for option in options {
126            set_selected(&option, select).await?;
127            if !self.multiple {
128                break;
129            }
130        }
131        Ok(())
132    }
133
134    /// Set the selection state of the option at the specified index.
135    async fn set_selection_by_index(&self, index: u32, select: bool) -> WebDriverResult<()> {
136        let selector = format!("option:nth-of-type({})", index + 1);
137        let option = self.element.find(By::Css(&*selector)).await?;
138        set_selected(&option, select).await?;
139        Ok(())
140    }
141
142    /// Set the selection state of options that display text matching the specified text.
143    /// That is, when given "Bar" this would select an option like:
144    ///
145    /// `<option value="foo">Bar</option>`
146    ///
147    /// NOTE: This will attempt to first select by exact match.
148    ///       However, if no exact match was found,
149    ///       it will attempt to select options that contain the longest word in the
150    ///       specified search text.
151    ///       This particular behaviour is patterned after the python selenium library.
152    async fn set_selection_by_visible_text(&self, text: &str, select: bool) -> WebDriverResult<()> {
153        let mut xpath = format!(".//option[normalize-space(.) = {}]", escape_string(text));
154        let options = match self.element.find_all(By::XPath(&*xpath)).await {
155            Ok(elems) => elems,
156            Err(e) if matches!(*e, WebDriverErrorInner::NoSuchElement(_)) => Vec::new(),
157            Err(e) => return Err(e),
158        };
159
160        let mut matched = false;
161        for option in &options {
162            set_selected(option, select).await?;
163            if !self.multiple {
164                return Ok(());
165            }
166            matched = true;
167        }
168
169        if options.is_empty() && text.contains(' ') {
170            let substring_without_space = get_longest_token(text);
171            let candidates = if substring_without_space.is_empty() {
172                self.options().await?
173            } else {
174                xpath =
175                    format!(".//option[contains(.,{})]", escape_string(substring_without_space));
176                self.element.find_all(By::XPath(&*xpath)).await?
177            };
178            for candidate in candidates {
179                if text == candidate.text().await? {
180                    set_selected(&candidate, select).await?;
181                    if !self.multiple {
182                        return Ok(());
183                    }
184                    matched = true;
185                }
186            }
187        }
188
189        if !matched {
190            Err(no_such_element(format!("Could not locate element with visible text: {}", text)))
191        } else {
192            Ok(())
193        }
194    }
195
196    /// Set the selection state of options that match the specified XPath condition.
197    async fn set_selection_by_xpath_condition(
198        &self,
199        condition: &str,
200        select: bool,
201    ) -> WebDriverResult<()> {
202        let xpath = format!(".//option[{}]", condition);
203        let options = self.element.find_all(By::XPath(&*xpath)).await?;
204        if options.is_empty() {
205            return Err(no_such_element(format!(
206                "Could not locate element matching XPath condition: {:?}",
207                xpath
208            )));
209        }
210
211        for option in &options {
212            set_selected(option, select).await?;
213            if !self.multiple {
214                break;
215            }
216        }
217
218        Ok(())
219    }
220
221    /// Set the selection state of options that display text exactly matching the specified text.
222    async fn set_selection_by_exact_text(&self, text: &str, select: bool) -> WebDriverResult<()> {
223        let condition = format!("text() = {}", escape_string(text));
224        self.set_selection_by_xpath_condition(&condition, select).await
225    }
226
227    /// Set the selection state of options that display text containing the specified substring.
228    async fn set_selection_by_partial_text(&self, text: &str, select: bool) -> WebDriverResult<()> {
229        let condition = format!("contains(text(), {})", escape_string(text));
230        self.set_selection_by_xpath_condition(&condition, select).await
231    }
232
233    /// Select all options for this select tag.
234    pub async fn select_all(&self) -> WebDriverResult<()> {
235        assert!(self.multiple, "You may only select all options of a multi-select");
236        self.set_selection_all(true).await
237    }
238
239    /// Select options matching the specified value.
240    pub async fn select_by_value(&self, value: &str) -> WebDriverResult<()> {
241        self.set_selection_by_value(value, true).await
242    }
243
244    /// Select the option matching the specified index. This is done by examining
245    /// the "index" attribute of an element and not merely by counting.
246    pub async fn select_by_index(&self, index: u32) -> WebDriverResult<()> {
247        self.set_selection_by_index(index, true).await
248    }
249
250    /// Select options with visible text matching the specified text.
251    /// That is, when given "Bar" this would select an option like:
252    ///
253    /// `<option value="foo">Bar</option>`
254    ///
255    /// This will attempt to select by exact match, but if no option is found it will also
256    /// attempt to select based on the longest contiguous word in the text.
257    /// See also `select_by_exact_text()` and `select_by_partial_text()`.
258    pub async fn select_by_visible_text(&self, text: &str) -> WebDriverResult<()> {
259        self.set_selection_by_visible_text(text, true).await
260    }
261
262    /// Select options matching the specified XPath condition.
263    /// E.g. The specified condition replaces `{}` in this XPath: `.//option[{}]`
264    ///
265    /// The following example would match `.//option[starts-with(text(), 'pre')]`:
266    /// ```ignore
267    /// select_by_xpath_condition("starts-with(text(), 'pre')").await?;
268    /// ```
269    /// For multi-select, all options matching the condition will be selected.
270    /// For single-select, only the first matching option will be selected.
271    pub async fn select_by_xpath_condition(&self, condition: &str) -> WebDriverResult<()> {
272        self.set_selection_by_xpath_condition(condition, true).await
273    }
274
275    /// Select options with visible text exactly matching the specified text.
276    /// For multi-select, all options whose text exactly matches will be selected.
277    /// For single-select, only the first exact match will be selected.
278    pub async fn select_by_exact_text(&self, text: &str) -> WebDriverResult<()> {
279        self.set_selection_by_exact_text(text, true).await
280    }
281
282    /// Select options with visible text partially matching the specified text.
283    /// For multi-select, all options whose text contains the specified substring will be selected.
284    /// For single-select, only the first option containing the substring will be selected.
285    pub async fn select_by_partial_text(&self, text: &str) -> WebDriverResult<()> {
286        self.set_selection_by_partial_text(text, true).await
287    }
288
289    /// Deselect all options for this select tag.
290    pub async fn deselect_all(&self) -> WebDriverResult<()> {
291        assert!(self.multiple, "You may only deselect all options of a multi-select");
292        self.set_selection_all(false).await
293    }
294
295    /// Deselect options matching the specified value.
296    pub async fn deselect_by_value(&self, value: &str) -> WebDriverResult<()> {
297        assert!(self.multiple, "You may only deselect options of a multi-select");
298        self.set_selection_by_value(value, false).await
299    }
300
301    /// Deselect the option matching the specified index. This is done by examining
302    /// the "index" attribute of an element and not merely by counting.
303    pub async fn deselect_by_index(&self, index: u32) -> WebDriverResult<()> {
304        assert!(self.multiple, "You may only deselect options of a multi-select");
305        self.set_selection_by_index(index, false).await
306    }
307
308    /// Deselect options with visible text matching the specified text.
309    /// That is, when given "Bar" this would deselect an option like:
310    ///
311    /// `<option value="foo">Bar</option>`
312    ///
313    /// See also `deselect_by_exact_text()` and `deselect_by_partial_text()`.
314    pub async fn deselect_by_visible_text(&self, text: &str) -> WebDriverResult<()> {
315        assert!(self.multiple, "You may only deselect options of a multi-select");
316        self.set_selection_by_visible_text(text, false).await
317    }
318
319    /// Deselect options matching the specified XPath condition.
320    /// E.g. The specified condition replaces `{}` in this XPath: `.//option[{}]`
321    ///
322    /// The following example would match `.//option[starts-with(text(), 'pre')]`:
323    /// ```ignore
324    /// deselect_by_xpath_condition("starts-with(text(), 'pre')").await?;
325    /// ```
326    /// For multi-select, all options matching the condition will be deselected.
327    /// For single-select, only the first matching option will be deselected.
328    pub async fn deselect_by_xpath_condition(&self, condition: &str) -> WebDriverResult<()> {
329        self.set_selection_by_xpath_condition(condition, false).await
330    }
331
332    /// Deselect all options with visible text exactly matching the specified text.
333    pub async fn deselect_by_exact_text(&self, text: &str) -> WebDriverResult<()> {
334        assert!(self.multiple, "You may only deselect options of a multi-select");
335        self.set_selection_by_exact_text(text, false).await
336    }
337
338    /// Deselect all options with visible text partially matching the specified text.
339    pub async fn deselect_by_partial_text(&self, text: &str) -> WebDriverResult<()> {
340        assert!(self.multiple, "You may only deselect options of a multi-select");
341        self.set_selection_by_partial_text(text, false).await
342    }
343}