| // <copyright file="SelectElement.cs" company="Selenium Committers"> |
| // Licensed to the Software Freedom Conservancy (SFC) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The SFC licenses this file |
| // to you 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. |
| // </copyright> |
| |
| using System; |
| using System.Collections.Generic; |
| using System.Collections.ObjectModel; |
| using System.Globalization; |
| using System.Text; |
| |
| namespace OpenQA.Selenium.Support.UI; |
| |
| /// <summary> |
| /// Provides a convenience method for manipulating selections of options in an HTML select element. |
| /// </summary> |
| public class SelectElement : IWrapsElement |
| { |
| /// <summary> |
| /// Initializes a new instance of the <see cref="SelectElement"/> class. |
| /// </summary> |
| /// <param name="element">The element to be wrapped</param> |
| /// <exception cref="ArgumentNullException">Thrown when the <see cref="IWebElement"/> object is <see langword="null"/></exception> |
| /// <exception cref="UnexpectedTagNameException">Thrown when the element wrapped is not a <select> element.</exception> |
| public SelectElement(IWebElement element) |
| { |
| if (element is null) |
| { |
| throw new ArgumentNullException(nameof(element), "element cannot be null"); |
| } |
| |
| string tagName = element.TagName; |
| |
| if (string.IsNullOrEmpty(tagName) || !string.Equals(tagName, "select", StringComparison.OrdinalIgnoreCase)) |
| { |
| throw new UnexpectedTagNameException("select", tagName); |
| } |
| |
| this.WrappedElement = element; |
| |
| // let check if it's a multiple |
| string? attribute = element.GetAttribute("multiple"); |
| this.IsMultiple = attribute != null && !attribute.Equals("false", StringComparison.OrdinalIgnoreCase); |
| } |
| |
| /// <summary> |
| /// Gets the <see cref="IWebElement"/> wrapped by this object. |
| /// </summary> |
| public IWebElement WrappedElement { get; } |
| |
| /// <summary> |
| /// Gets a value indicating whether the parent element supports multiple selections. |
| /// </summary> |
| public bool IsMultiple { get; } |
| |
| /// <summary> |
| /// Gets the list of options for the select element. |
| /// </summary> |
| public IList<IWebElement> Options => this.WrappedElement.FindElements(By.TagName("option")); |
| |
| /// <summary> |
| /// Gets the selected item within the select element. |
| /// </summary> |
| /// <remarks>If more than one item is selected this will return the first item.</remarks> |
| /// <exception cref="NoSuchElementException">Thrown if no option is selected.</exception> |
| public IWebElement SelectedOption |
| { |
| get |
| { |
| foreach (IWebElement option in this.Options) |
| { |
| if (option.Selected) |
| { |
| return option; |
| } |
| } |
| |
| throw new NoSuchElementException("No option is selected"); |
| } |
| } |
| |
| /// <summary> |
| /// Gets all of the selected options within the select element. |
| /// </summary> |
| public IList<IWebElement> AllSelectedOptions |
| { |
| get |
| { |
| List<IWebElement> returnValue = new List<IWebElement>(); |
| foreach (IWebElement option in this.Options) |
| { |
| if (option.Selected) |
| { |
| returnValue.Add(option); |
| } |
| } |
| |
| return returnValue; |
| } |
| } |
| |
| /// <summary> |
| /// Select all options by the text displayed. |
| /// </summary> |
| /// <param name="text">The text of the option to be selected.</param> |
| /// <param name="partialMatch">Default value is false. If true a partial match on the Options list will be performed, otherwise exact match.</param> |
| /// <remarks>When given "Bar" this method would select an option like: |
| /// <para> |
| /// <option value="foo">Bar</option> |
| /// </para> |
| /// </remarks> |
| /// <exception cref="ArgumentNullException">If <paramref name="text"/> is <see langword="null"/>.</exception> |
| /// <exception cref="NoSuchElementException">Thrown if there is no element with the given text present.</exception> |
| public void SelectByText(string text, bool partialMatch = false) |
| { |
| if (text is null) |
| { |
| throw new ArgumentNullException(nameof(text), "text must not be null"); |
| } |
| |
| bool matched = false; |
| ReadOnlyCollection<IWebElement> options; |
| |
| if (!partialMatch) |
| { |
| // try to find the option via XPATH ... |
| options = this.WrappedElement.FindElements(By.XPath(".//option[normalize-space(.) = " + EscapeQuotes(text) + "]")); |
| } |
| else |
| { |
| options = this.WrappedElement.FindElements(By.XPath(".//option[contains(normalize-space(.), " + EscapeQuotes(text) + ")]")); |
| } |
| |
| foreach (IWebElement option in options) |
| { |
| SetSelected(option, true); |
| if (!this.IsMultiple) |
| { |
| return; |
| } |
| |
| matched = true; |
| } |
| |
| if (options.Count == 0 && text.Contains(" ")) |
| { |
| string substringWithoutSpace = GetLongestSubstringWithoutSpace(text); |
| IList<IWebElement> candidates; |
| if (string.IsNullOrEmpty(substringWithoutSpace)) |
| { |
| // hmm, text is either empty or contains only spaces - get all options ... |
| candidates = this.WrappedElement.FindElements(By.TagName("option")); |
| } |
| else |
| { |
| // get candidates via XPATH ... |
| candidates = this.WrappedElement.FindElements(By.XPath(".//option[contains(., " + EscapeQuotes(substringWithoutSpace) + ")]")); |
| } |
| |
| foreach (IWebElement option in candidates) |
| { |
| if (text == option.Text) |
| { |
| SetSelected(option, true); |
| if (!this.IsMultiple) |
| { |
| return; |
| } |
| |
| matched = true; |
| } |
| } |
| } |
| |
| if (!matched) |
| { |
| throw new NoSuchElementException("Cannot locate element with text: " + text); |
| } |
| } |
| |
| /// <summary> |
| /// Select an option by the value. |
| /// </summary> |
| /// <param name="value">The value of the option to be selected.</param> |
| /// <remarks>When given "foo" this method will select an option like: |
| /// <para> |
| /// <option value="foo">Bar</option> |
| /// </para> |
| /// </remarks> |
| /// <exception cref="NoSuchElementException">Thrown when no element with the specified value is found.</exception> |
| public void SelectByValue(string value) |
| { |
| StringBuilder builder = new StringBuilder(".//option[@value = "); |
| builder.Append(EscapeQuotes(value)); |
| builder.Append("]"); |
| IList<IWebElement> options = this.WrappedElement.FindElements(By.XPath(builder.ToString())); |
| |
| bool matched = false; |
| foreach (IWebElement option in options) |
| { |
| SetSelected(option, true); |
| if (!this.IsMultiple) |
| { |
| return; |
| } |
| |
| matched = true; |
| } |
| |
| if (!matched) |
| { |
| throw new NoSuchElementException("Cannot locate option with value: " + value); |
| } |
| } |
| |
| /// <summary> |
| /// Select the option by the index, as determined by the "index" attribute of the element. |
| /// </summary> |
| /// <param name="index">The value of the index attribute of the option to be selected.</param> |
| /// <exception cref="NoSuchElementException">Thrown when no element exists with the specified index attribute.</exception> |
| public void SelectByIndex(int index) |
| { |
| string match = index.ToString(CultureInfo.InvariantCulture); |
| |
| foreach (IWebElement option in this.Options) |
| { |
| if (option.GetAttribute("index") == match) |
| { |
| SetSelected(option, true); |
| return; |
| } |
| } |
| |
| throw new NoSuchElementException("Cannot locate option with index: " + index); |
| } |
| |
| /// <summary> |
| /// Clear all selected entries. This is only valid when the SELECT supports multiple selections. |
| /// </summary> |
| /// <exception cref="WebDriverException">Thrown when attempting to deselect all options from a SELECT |
| /// that does not support multiple selections.</exception> |
| public void DeselectAll() |
| { |
| if (!this.IsMultiple) |
| { |
| throw new InvalidOperationException("You may only deselect all options if multi-select is supported"); |
| } |
| |
| foreach (IWebElement option in this.Options) |
| { |
| SetSelected(option, false); |
| } |
| } |
| |
| /// <summary> |
| /// Deselect the option by the text displayed. |
| /// </summary> |
| /// <exception cref="InvalidOperationException">Thrown when attempting to deselect option from a SELECT |
| /// that does not support multiple selections.</exception> |
| /// <exception cref="NoSuchElementException">Thrown when no element exists with the specified test attribute.</exception> |
| /// <param name="text">The text of the option to be deselected.</param> |
| /// <remarks>When given "Bar" this method would deselect an option like: |
| /// <para> |
| /// <option value="foo">Bar</option> |
| /// </para> |
| /// </remarks> |
| public void DeselectByText(string text) |
| { |
| if (!this.IsMultiple) |
| { |
| throw new InvalidOperationException("You may only deselect option if multi-select is supported"); |
| } |
| |
| bool matched = false; |
| StringBuilder builder = new StringBuilder(".//option[normalize-space(.) = "); |
| builder.Append(EscapeQuotes(text)); |
| builder.Append("]"); |
| IList<IWebElement> options = this.WrappedElement.FindElements(By.XPath(builder.ToString())); |
| foreach (IWebElement option in options) |
| { |
| SetSelected(option, false); |
| matched = true; |
| } |
| |
| if (!matched) |
| { |
| throw new NoSuchElementException("Cannot locate option with text: " + text); |
| } |
| } |
| |
| /// <summary> |
| /// Deselect the option having value matching the specified text. |
| /// </summary> |
| /// <exception cref="InvalidOperationException">Thrown when attempting to deselect option from a SELECT |
| /// that does not support multiple selections.</exception> |
| /// <exception cref="NoSuchElementException">Thrown when no element exists with the specified value attribute.</exception> |
| /// <param name="value">The value of the option to deselect.</param> |
| /// <remarks>When given "foo" this method will deselect an option like: |
| /// <para> |
| /// <option value="foo">Bar</option> |
| /// </para> |
| /// </remarks> |
| public void DeselectByValue(string value) |
| { |
| if (!this.IsMultiple) |
| { |
| throw new InvalidOperationException("You may only deselect option if multi-select is supported"); |
| } |
| |
| bool matched = false; |
| StringBuilder builder = new StringBuilder(".//option[@value = "); |
| builder.Append(EscapeQuotes(value)); |
| builder.Append("]"); |
| IList<IWebElement> options = this.WrappedElement.FindElements(By.XPath(builder.ToString())); |
| foreach (IWebElement option in options) |
| { |
| SetSelected(option, false); |
| matched = true; |
| } |
| |
| if (!matched) |
| { |
| throw new NoSuchElementException("Cannot locate option with value: " + value); |
| } |
| } |
| |
| /// <summary> |
| /// Deselect the option by the index, as determined by the "index" attribute of the element. |
| /// </summary> |
| /// <exception cref="InvalidOperationException">Thrown when attempting to deselect option from a SELECT |
| /// that does not support multiple selections.</exception> |
| /// <exception cref="NoSuchElementException">Thrown when no element exists with the specified index attribute.</exception> |
| /// <param name="index">The value of the index attribute of the option to deselect.</param> |
| public void DeselectByIndex(int index) |
| { |
| if (!this.IsMultiple) |
| { |
| throw new InvalidOperationException("You may only deselect option if multi-select is supported"); |
| } |
| |
| string match = index.ToString(CultureInfo.InvariantCulture); |
| foreach (IWebElement option in this.Options) |
| { |
| if (match == option.GetAttribute("index")) |
| { |
| SetSelected(option, false); |
| return; |
| } |
| } |
| |
| throw new NoSuchElementException("Cannot locate option with index: " + index); |
| } |
| |
| private static string EscapeQuotes(string toEscape) |
| { |
| // Convert strings with both quotes and ticks into: foo'"bar -> concat("foo'", '"', "bar") |
| if (toEscape.IndexOf("\"", StringComparison.OrdinalIgnoreCase) > -1 && toEscape.IndexOf("'", StringComparison.OrdinalIgnoreCase) > -1) |
| { |
| bool quoteIsLast = false; |
| if (toEscape.LastIndexOf("\"", StringComparison.OrdinalIgnoreCase) == toEscape.Length - 1) |
| { |
| quoteIsLast = true; |
| } |
| |
| List<string> substrings = new List<string>(toEscape.Split('\"')); |
| if (quoteIsLast && string.IsNullOrEmpty(substrings[substrings.Count - 1])) |
| { |
| // If the last character is a quote ('"'), we end up with an empty entry |
| // at the end of the list, which is unnecessary. We don't want to split |
| // ignoring *all* empty entries, since that might mask legitimate empty |
| // strings. Instead, just remove the empty ending entry. |
| substrings.RemoveAt(substrings.Count - 1); |
| } |
| |
| StringBuilder quoted = new StringBuilder("concat("); |
| for (int i = 0; i < substrings.Count; i++) |
| { |
| quoted.Append("\"").Append(substrings[i]).Append("\""); |
| if (i == substrings.Count - 1) |
| { |
| if (quoteIsLast) |
| { |
| quoted.Append(", '\"')"); |
| } |
| else |
| { |
| quoted.Append(")"); |
| } |
| } |
| else |
| { |
| quoted.Append(", '\"', "); |
| } |
| } |
| |
| return quoted.ToString(); |
| } |
| |
| // Escape string with just a quote into being single quoted: f"oo -> 'f"oo' |
| if (toEscape.IndexOf("\"", StringComparison.OrdinalIgnoreCase) > -1) |
| { |
| return string.Format(CultureInfo.InvariantCulture, "'{0}'", toEscape); |
| } |
| |
| // Otherwise return the quoted string |
| return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", toEscape); |
| } |
| |
| private static string GetLongestSubstringWithoutSpace(string s) |
| { |
| string result = string.Empty; |
| foreach (string substring in s.Split(' ')) |
| { |
| if (substring.Length > result.Length) |
| { |
| result = substring; |
| } |
| } |
| |
| return result; |
| } |
| |
| private static void SetSelected(IWebElement option, bool select) |
| { |
| if (select && !option.Enabled) |
| { |
| throw new InvalidOperationException("You may not select a disabled option"); |
| } |
| |
| bool isSelected = option.Selected; |
| if ((!isSelected && select) || (isSelected && !select)) |
| { |
| option.Click(); |
| } |
| } |
| } |