thirtyfour_sync 0.27.1

Thirtyfour is a Selenium / WebDriver library for Rust, for automated website UI testing. This crate is the synchronous version only. For async, see the `thirtyfour` crate instead.
Documentation
// This wrapper is a fairly direct port of the Select class from the python
// selenium library at:
// https://github.com/SeleniumHQ/selenium/blob/trunk/py/selenium/webdriver/support/select.py

// Copyright 2021 Stephen Pryde and the thirtyfour_sync contributors
// Derived (and modified) from the Selenium project at https://github.com/SeleniumHQ/selenium.
//
// Copyright 2011-2020 Software Freedom Conservancy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::error::{no_such_element, WebDriverError, WebDriverResult};
use crate::{By, WebElement};

/// Set the selection state of the specified element.
fn set_selected(element: &WebElement<'_>, select: bool) -> WebDriverResult<()> {
    if element.is_selected()? != select {
        element.click()?;
    }
    Ok(())
}

/// Escape the specified string for use in Css or XPath selector.
pub fn escape_string(value: &str) -> String {
    let contains_single = value.contains('\'');
    let contains_double = value.contains('\"');
    if contains_single && contains_double {
        let mut result = vec![String::from("concat(")];
        for substring in value.split('\"') {
            result.push(format!("\"{}\"", substring));
            result.push(String::from(", '\"', "));
        }
        result.pop();
        if value.ends_with('\"') {
            result.push(String::from(", '\"'"));
        }
        return result.join("") + ")";
    }

    if contains_double {
        format!("'{}'", value)
    } else {
        format!("\"{}\"", value)
    }
}

/// Get the longest word in the specified string.
fn get_longest_token(value: &str) -> &str {
    let mut longest = "";
    for item in value.split(' ') {
        if item.len() > longest.len() {
            longest = item;
        }
    }
    longest
}

/// Convenience wrapper for `<select>` elements.
pub struct SelectElement<'a> {
    element: WebElement<'a>,
    multiple: bool,
}

impl<'a> SelectElement<'a> {
    /// Instantiate a new SelectElement struct. The specified element must be a `<select>` element.
    pub fn new(element: &WebElement<'a>) -> WebDriverResult<SelectElement<'a>> {
        let multiple = element.get_attribute("multiple")?.filter(|x| x != "false").is_some();
        let element = element.clone();
        Ok(SelectElement {
            element,
            multiple,
        })
    }

    /// Return a vec of all options belonging to this select tag.
    pub fn options(&self) -> WebDriverResult<Vec<WebElement>> {
        self.element.find_elements(By::Tag("option"))
    }

    /// Return a vec of all selected options belonging to this select tag.
    pub fn all_selected_options(&self) -> WebDriverResult<Vec<WebElement>> {
        let mut selected = Vec::new();
        for option in self.options()? {
            if option.is_selected()? {
                selected.push(option);
            }
        }
        Ok(selected)
    }

    /// Return the first selected option in this select tag.
    pub fn first_selected_option(&self) -> WebDriverResult<WebElement> {
        for option in self.options()? {
            if option.is_selected()? {
                return Ok(option);
            }
        }
        Err(no_such_element("No options are selected"))
    }

    /// Set selection state for all options.
    fn set_selection_all(&self, select: bool) -> WebDriverResult<()> {
        for option in self.options()? {
            set_selected(&option, select)?;
        }
        Ok(())
    }

    /// Set the selection state of options matching the specified value.
    fn set_selection_by_value(&self, value: &str, select: bool) -> WebDriverResult<()> {
        let selector = format!("option[value={}]", escape_string(value));
        let options = self.element.find_elements(By::Css(&selector))?;
        for option in options {
            set_selected(&option, select)?;
            if !self.multiple {
                break;
            }
        }
        Ok(())
    }

    /// Set the selection state of the option at the specified index. This is done by examining
    /// the "index" attribute of an element and not merely by counting.
    fn set_selection_by_index(&self, index: u32, select: bool) -> WebDriverResult<()> {
        let str_index: String = index.to_string();
        for option in self.options()? {
            if option.get_attribute("index")?.filter(|i| i == &str_index).is_some() {
                set_selected(&option, select)?;
                return Ok(());
            }
        }
        Err(no_such_element(&format!("Could not locate element with index {}", index)))
    }

    /// Set the selection state of options that display text matching the specified text.
    /// That is, when given "Bar" this would select an option like:
    ///
    /// `<option value="foo">Bar</option>`
    fn set_selection_by_visible_text(&self, text: &str, select: bool) -> WebDriverResult<()> {
        let mut xpath = format!(".//option[normalize-space(.) = {}]", escape_string(text));
        let options = match self.element.find_elements(By::XPath(&xpath)) {
            Ok(elems) => elems,
            Err(WebDriverError::NoSuchElement(_)) => Vec::new(),
            Err(e) => return Err(e),
        };

        let mut matched = false;
        for option in &options {
            set_selected(option, select)?;
            if !self.multiple {
                return Ok(());
            }
            matched = true;
        }

        if options.is_empty() && text.contains(' ') {
            let substring_without_space = get_longest_token(text);
            let candidates = if substring_without_space.is_empty() {
                self.options()?
            } else {
                xpath =
                    format!(".//option[contains(.,{})]", escape_string(substring_without_space));
                self.element.find_elements(By::XPath(&xpath))?
            };
            for candidate in candidates {
                if text == candidate.text()? {
                    set_selected(&candidate, select)?;
                    if !self.multiple {
                        return Ok(());
                    }
                    matched = true;
                }
            }
        }

        if !matched {
            Err(no_such_element(&format!("Could not locate element with visible text: {}", text)))
        } else {
            Ok(())
        }
    }

    /// Set the selection state of options that match the specified XPath condition.
    fn set_selection_by_xpath_condition(
        &self,
        condition: &str,
        select: bool,
    ) -> WebDriverResult<()> {
        let xpath = format!(".//option[{}]", condition);
        let options = self.element.find_elements(By::XPath(&xpath))?;
        if options.is_empty() {
            return Err(no_such_element(&format!(
                "Could not locate element matching XPath condition: {:?}",
                xpath
            )));
        }

        for option in &options {
            set_selected(option, select)?;
            if !self.multiple {
                break;
            }
        }

        Ok(())
    }

    /// Set the selection state of options that display text exactly matching the specified text.
    fn set_selection_by_exact_text(&self, text: &str, select: bool) -> WebDriverResult<()> {
        let condition = format!("text() = {}", escape_string(text));
        self.set_selection_by_xpath_condition(&condition, select)
    }

    /// Set the selection state of options that display text containing the specified substring.
    fn set_selection_by_partial_text(&self, text: &str, select: bool) -> WebDriverResult<()> {
        let condition = format!("contains(text(), {})", escape_string(text));
        self.set_selection_by_xpath_condition(&condition, select)
    }

    /// Select all options for this select tag.
    pub fn select_all(&self) -> WebDriverResult<()> {
        assert!(self.multiple, "You may only select all options of a multi-select");
        self.set_selection_all(true)
    }

    /// Select options matching the specified value.
    pub fn select_by_value(&self, value: &str) -> WebDriverResult<()> {
        self.set_selection_by_value(value, true)
    }

    /// Select the option matching the specified index. This is done by examining
    /// the "index" attribute of an element and not merely by counting.
    pub fn select_by_index(&self, index: u32) -> WebDriverResult<()> {
        self.set_selection_by_index(index, true)
    }

    /// Select options with visible text matching the specified text.
    /// That is, when given "Bar" this would select an option like:
    ///
    /// `<option value="foo">Bar</option>`
    pub fn select_by_visible_text(&self, text: &str) -> WebDriverResult<()> {
        self.set_selection_by_visible_text(text, true)
    }

    /// Select options matching the specified XPath condition.
    /// E.g. The specified condition replaces `{}` in this XPath: `.//option[{}]`
    ///
    /// The following example would match `.//option[starts-with(text(), 'pre')]`:
    /// ```ignore
    /// select_by_xpath_condition("starts-with(text(), 'pre')").await?;
    /// ```
    /// For multi-select, all options matching the condition will be selected.
    /// For single-select, only the first matching option will be selected.
    pub fn select_by_xpath_condition(&self, condition: &str) -> WebDriverResult<()> {
        self.set_selection_by_xpath_condition(condition, true)
    }

    /// Select options with visible text exactly matching the specified text.
    /// For multi-select, all options whose text exactly matches will be selected.
    /// For single-select, only the first exact match will be selected.
    pub fn select_by_exact_text(&self, text: &str) -> WebDriverResult<()> {
        self.set_selection_by_exact_text(text, true)
    }

    /// Select options with visible text partially matching the specified text.
    /// For multi-select, all options whose text contains the specified substring will be selected.
    /// For single-select, only the first option containing the substring will be selected.
    pub fn select_by_partial_text(&self, text: &str) -> WebDriverResult<()> {
        self.set_selection_by_partial_text(text, true)
    }

    /// Deselect all options for this select tag.
    pub fn deselect_all(&self) -> WebDriverResult<()> {
        assert!(self.multiple, "You may only deselect all options of a multi-select");
        self.set_selection_all(false)
    }

    /// Deselect options matching the specified value.
    pub fn deselect_by_value(&self, value: &str) -> WebDriverResult<()> {
        assert!(self.multiple, "You may only deselect options of a multi-select");
        self.set_selection_by_value(value, false)
    }

    /// Deselect the option matching the specified index. This is done by examining
    /// the "index" attribute of an element and not merely by counting.
    pub fn deselect_by_index(&self, index: u32) -> WebDriverResult<()> {
        assert!(self.multiple, "You may only deselect options of a multi-select");
        self.set_selection_by_index(index, false)
    }

    /// Deselect options with visible text matching the specified text.
    /// That is, when given "Bar" this would select an option like:
    ///
    /// `<option value="foo">Bar</option>`
    pub fn deselect_by_visible_text(&self, text: &str) -> WebDriverResult<()> {
        assert!(self.multiple, "You may only deselect options of a multi-select");
        self.set_selection_by_visible_text(text, false)
    }

    /// Deselect options matching the specified XPath condition.
    /// E.g. The specified condition replaces `{}` in this XPath: `.//option[{}]`
    ///
    /// The following example would match `.//option[starts-with(text(), 'pre')]`:
    /// ```ignore
    /// deselect_by_xpath_condition("starts-with(text(), 'pre')").await?;
    /// ```
    /// For multi-select, all options matching the condition will be deselected.
    /// For single-select, only the first matching option will be deselected.
    pub fn deselect_by_xpath_condition(&self, condition: &str) -> WebDriverResult<()> {
        self.set_selection_by_xpath_condition(condition, false)
    }

    /// Deselect all options with visible text exactly matching the specified text.
    pub fn deselect_by_exact_text(&self, text: &str) -> WebDriverResult<()> {
        assert!(self.multiple, "You may only deselect options of a multi-select");
        self.set_selection_by_exact_text(text, false)
    }

    /// Deselect all options with visible text partially matching the specified text.
    pub fn deselect_by_partial_text(&self, text: &str) -> WebDriverResult<()> {
        assert!(self.multiple, "You may only deselect options of a multi-select");
        self.set_selection_by_partial_text(text, false)
    }
}