blob: c2c7970821a239d6f6ea0eac818471e495d85786 [file] [log] [blame]
// 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)
}
}