Path: blob/trunk/dotnet/src/support/UI/SelectElement.cs
2885 views
// <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(); } } }