⬆️ ⬇️

Automated testing of a web application (MS Unit Testing Framework + Selenium WebDriver C #). Part 2.2: Selenium API wrapper - WebElement

Selenium + C #

Introduction


Hello! In the previous part, I described the main problems encountered when working with Selenium WebDriver, and also gave an example of the Browser wrapper. It seems it was not difficult, huh?) Well, let's go further. We must deal with the remaining problems:



In this part we will write the wrapper WebElement, which is entirely aimed at the user, i.e. on autotest developers. I admit that at the time of its writing my task was to create a "framework" that should be used by manual testing engineers for writing autotests. Naturally it was assumed that they have a very modest knowledge of programming. Therefore, it didn’t matter how many tons of code would be in the framework itself and how complex it would be inside. The main thing is that from the outside it is as simple as three letters. I warn you, there will be a lot of code and few pictures =)



Links


Part 1: Introduction

Part 2.1: Selenium API wrapper - Browser

Part 2.2: Selenium API wrapper - WebElement

Part 3: WebPages - describing pages

Part 4: Finally writing tests

Framework Publishing



Forward!


And so, I began to think about how it would be convenient for me, as a developer of autotests, to describe web elements. Some developers solve the first problem by writing getters, it looks like this:

private IWebElement LoginEdit { get { return WebDriver.FindElement(By.Id("Login")); } } 


If there are no unique properties, you will have to search for a set of properties using FindElements, and then filter out using GetAttribute and GetCssValue.

')

In WebDriver.Support there is such a feature as PageFactory and FindsBy attribute:

 [FindsBy(How = How.LinkText, Using = "")] public IWebElement HelpLink { get; set; } 


The description of the properties is done through the attributes - not bad. In addition, it is possible to cache the search (CacheLookup). Cons of such a decision:



In principle, many people stop at this. We will go further. I will formulate some profits that I would like to get at the output.



Idea and examples of use


The basic idea is to fill in the search criteria when describing an element, and perform the element search for any actions with it. In addition, I want to implement caching search results for optimal test performance.



It would also be very convenient to describe the elements in one line, but not to create and pass arrays of properties. And here the pattern “call chain” comes in handy. You also need to be able to search for items by entering parameters.



Well, for complete happiness, you need to implement group methods over elements in the Linq style, for example, so that you can put all checkboxes according to some criterion or get an array of strings from an array of links.



I will try to draw the scheme WebElement:

image



I note that all the same when testing a complex application, you may encounter situations where the element cannot be recognized with the help of Selenium WebDriver. To solve this problem, the Browser.ExecuteJavaScript method is provided (see the previous article), i.e. There is an opportunity to work with elements through JavaScript and jQuery.



Before proceeding to the wrapper code, I will show examples of the description:



Search by id:

 private static readonly WebElement TestElement = new WebElement().ById("StartButton"); 


Search by XPath:

 private static readonly WebElement TestElement = new WebElement().ByXPath("//div[@class='Content']//tr[2]/td[2]"); 


Search for the last item by class:

 private static readonly WebElement TestElement = new WebElement().ByClass("UserAvatar").Last(); 


Search by value entry in attribute:

 private static readonly WebElement TestElement = new WebElement().ByAttribute(TagAttributes.Href, "TagEdit", exactMatch: false); 


Search by several parameters:

 private static readonly WebElement TestElement = new WebElement().ByClass("TimePart").ByName("Day").Index(0); 


Search by tag and by text (entry):

 private static readonly WebElement TestElement = new WebElement().ByTagName(TagNames.Link).ByText("Hello", exactMach); 


Note that a description for a single item is not necessarily stored in TestElement. If there are several elements, then when I try to click, an exception should occur (but in my implementation, the first element I get will be used). We also have the ability to specify the index of an element using Index (...), either First () or Last (), so that one element is guaranteed to be found. In addition, it is not necessary to perform an action with one element; you can perform it with all elements at once (see ForEach in the examples below).



And now I will give examples of use:



Element click

 TestElement.Click(); 


Click on an item using Selenium WebDriver or using jQuery:

 TestElement.Click(useJQuery: true); 


Getting text (for example, a link or text field):

 var text = TestElement.Text; 


Text setting:

 TestElement.Text = "Hello!"; 


Dragging an item onto another item:

 TestElement1.DragAndDrop(TestElement2); 


Sending event to item:

 TestElement.FireJQueryEvent(JavaScriptEvents.KeyUp); 


Expand all collapsed elements (click on the plus sign):

 TestElements.ForEach(i => i.Click()); 


Getting the value of all headers:

 var subjects = new WebElement().ByClass("Subject").Select(i => i.Text); 




The applied call chain pattern allows you to simultaneously define an element and perform an action:

 new WebElement().ById("Next").Click(); var text = new WebElement().ById("Help").Text; 


For the end user (the developer of autotests, which will describe the elements of the pages) looks very friendly, is not it? Nothing sticks out. Note that we do not even allow the developer to pass arbitrary attributes and tag names as parameters using the enum TagAttributes and TagNames. This will save code from numerous magic strings.



Unfortunately, to provide such an API, you have to write a lot of code. The WebElement class (partial) will be divided into 5 parts:



As I warned in the previous article, there are no comments in the code, but I will try to comment on the main points under the copy-paste.



WebElement.cs


 namespace Autotests.Utilities.WebElement { public partial class WebElement : ICloneable { private By _firstSelector; private IList<IWebElement> _searchCache; private IWebElement FindSingle() { return TryFindSingle(); } private IWebElement TryFindSingle() { Contract.Ensures(Contract.Result<IWebElement>() != null); try { return FindSingleIWebElement(); } catch (StaleElementReferenceException) { ClearSearchResultCache(); return FindSingleIWebElement(); } catch (InvalidSelectorException) { throw; } catch (WebDriverException) { throw; } catch (WebElementNotFoundException) { throw; } catch { throw WebElementNotFoundException; } } private IWebElement FindSingleIWebElement() { var elements = FindIWebElements(); if (!elements.Any()) throw WebElementNotFoundException; var element = elements.Count() == 1 ? elements.Single() : _index == -1 ? elements.Last() : elements.ElementAt(_index); // ReSharper disable UnusedVariable var elementAccess = element.Enabled; // ReSharper restore UnusedVariable return element; } private IList<IWebElement> FindIWebElements() { if (_searchCache != null) { return _searchCache; } Browser.WaitReadyState(); Browser.WaitAjax(); var resultEnumerable = Browser.FindElements(_firstSelector); try { resultEnumerable = FilterByVisibility(resultEnumerable).ToList(); resultEnumerable = FilterByTagNames(resultEnumerable).ToList(); resultEnumerable = FilterByText(resultEnumerable).ToList(); resultEnumerable = FilterByTagAttributes(resultEnumerable).ToList(); resultEnumerable = resultEnumerable.ToList(); } catch (Exception e) { Console.WriteLine(e); return new List<IWebElement>(); } var resultList = resultEnumerable.ToList(); return resultList; } private WebElementNotFoundException WebElementNotFoundException { get { CheckConnectionFailure(); return new WebElementNotFoundException(string.Format("Can't find single element with given search criteria: {0}.", SearchCriteriaToString())); } } private static void CheckConnectionFailure() { const string connectionFailure = "connectionFailure"; Contract.Assert(!Browser.PageSource.Contains(connectionFailure), "Connection can't be established."); } object ICloneable.Clone() { return Clone(); } public WebElement Clone() { return (WebElement)MemberwiseClone(); } } } 


Here the main attention should be paid to FindIWebElements, FindSingleIWebElement and exception handling in TryFindSingle. In FindIWebElements, we wait for the browser to complete all its affairs (WaitReadyState and WaitAjax), search for items (FindElements), and then filter them according to various criteria. Also in the code _searchCache appears, this is just our cache (the search is not automatically cached, the element needs to call the CacheSearchResult method).



WebElementActions.cs


 namespace Autotests.Utilities.WebElement { internal enum SelectTypes { ByValue, ByText } public partial class WebElement { #region Common properties public int Count { get { return FindIWebElements().Count; } } public bool Enabled { get { return FindSingle().Enabled; } } public bool Displayed { get { return FindSingle().Displayed; } } public bool Selected { get { return FindSingle().Selected; } } public string Text { set { var element = FindSingle(); if (element.TagName == EnumHelper.GetEnumDescription(TagNames.Input) || element.TagName == EnumHelper.GetEnumDescription(TagNames.TextArea)) { element.Clear(); } else { element.SendKeys(Keys.LeftControl + "a"); element.SendKeys(Keys.Delete); } if (string.IsNullOrEmpty(value)) return; Browser.ExecuteJavaScript(string.Format("arguments[0].value = \"{0}\";", value), element); Executor.Try(() => FireJQueryEvent(JavaScriptEvents.KeyUp)); } get { var element = FindSingle(); return !string.IsNullOrEmpty(element.Text) ? element.Text : element.GetAttribute(EnumHelper.GetEnumDescription(TagAttributes.Value)); } } public int TextInt { set { Text = value.ToString(CultureInfo.InvariantCulture); } get { return Text.ToInt(); } } public string InnerHtml { get { return Browser.ExecuteJavaScript("return arguments[0].innerHTML;", FindSingle()).ToString(); } } #endregion #region Common methods public bool Exists() { return FindIWebElements().Any(); } public bool Exists(TimeSpan timeSpan) { return Executor.SpinWait(Exists, timeSpan, TimeSpan.FromMilliseconds(200)); } public bool Exists(int seconds) { return Executor.SpinWait(Exists, TimeSpan.FromSeconds(seconds), TimeSpan.FromMilliseconds(200)); } public void Click(bool useJQuery = true) { var element = FindSingle(); Contract.Assert(element.Enabled); if (useJQuery && element.TagName != EnumHelper.GetEnumDescription(TagNames.Link)) { FireJQueryEvent(element, JavaScriptEvents.Click); } else { try { element.Click(); } catch (InvalidOperationException e) { if (e.Message.Contains("Element is not clickable")) { Thread.Sleep(2000); element.Click(); } } } } public void SendKeys(string keys) { FindSingle().SendKeys(keys); } public void SetCheck(bool value, bool useJQuery = true) { var element = FindSingle(); Contract.Assert(element.Enabled); const int tryCount = 10; for (var i = 0; i < tryCount; i++) { element = FindSingle(); Set(value, useJQuery); if (element.Selected == value) { return; } } Contract.Assert(element.Selected == value); } public void Select(string optionValue) { SelectCommon(optionValue, SelectTypes.ByValue); } public void Select(int optionValue) { SelectCommon(optionValue.ToString(CultureInfo.InvariantCulture), SelectTypes.ByValue); } public void SelectByText(string optionText) { SelectCommon(optionText, SelectTypes.ByText); } public string GetAttribute(TagAttributes tagAttribute) { return FindSingle().GetAttribute(EnumHelper.GetEnumDescription(tagAttribute)); } #endregion #region Additional methods public void SwitchContext() { var element = FindSingle(); Browser.SwitchToFrame(element); } public void CacheSearchResult() { _searchCache = FindIWebElements(); } public void ClearSearchResultCache() { _searchCache = null; } public void DragAndDrop(WebElement destination) { var source = FindSingle(); var dest = destination.FindSingle(); Browser.DragAndDrop(source, dest); } public void FireJQueryEvent(JavaScriptEvents javaScriptEvent) { var element = FindSingle(); FireJQueryEvent(element, javaScriptEvent); } public void ForEach(Action<WebElement> action) { Contract.Requires(action != null); CacheSearchResult(); Enumerable.Range(0, Count).ToList().ForEach(i => action(ByIndex(i))); ClearSearchResultCache(); } public List<T> Select<T>(Func<WebElement, T> action) { Contract.Requires(action != null); var result = new List<T>(); ForEach(e => result.Add(action(e))); return result; } public List<WebElement> Where(Func<WebElement, bool> action) { Contract.Requires(action != null); var result = new List<WebElement>(); ForEach(e => { if (action(e)) result.Add(e); }); return result; } public WebElement Single(Func<WebElement, bool> action) { return Where(action).Single(); } #endregion #region Helpers private void Set(bool value, bool useJQuery = true) { if (Selected ^ value) { Click(useJQuery); } } private void SelectCommon(string option, SelectTypes selectType) { Contract.Requires(!string.IsNullOrEmpty(option)); var element = FindSingle(); Contract.Assert(element.Enabled); switch (selectType) { case SelectTypes.ByValue: new SelectElement(element).SelectByValue(option); return; case SelectTypes.ByText: new SelectElement(element).SelectByText(option); return; default: throw new Exception(string.Format("Unknown select type: {0}.", selectType)); } } private void FireJQueryEvent(IWebElement element, JavaScriptEvents javaScriptEvent) { var eventName = EnumHelper.GetEnumDescription(javaScriptEvent); Browser.ExecuteJavaScript(string.Format("$(arguments[0]).{0}();", eventName), element); } #endregion } public enum JavaScriptEvents { [Description("keyup")] KeyUp, [Description("click")] Click } } 


Flat list of properties and methods defined for elements. Some accept the useJQuery parameter, which tells the method that the action should be performed using jQuery (done for complex cases and the ability to perform an action in all three browsers). In addition, JavaScript execution is much faster. In some methods, “crutches” are arranged, for example, a cycle with tryCount in SetCheck. Of course, for each product tested there will be its own special cases.



WebElementByCriteria.cs


 namespace Autotests.Utilities.WebElement { internal class SearchProperty { public string AttributeName { get; set; } public string AttributeValue { get; set; } public bool ExactMatch { get; set; } } internal class TextSearchData { public string Text { get; set; } public bool ExactMatch { get; set; } } public partial class WebElement { private readonly IList<SearchProperty> _searchProperties = new List<SearchProperty>(); private readonly IList<TagNames> _searchTags = new List<TagNames>(); private bool _searchHidden; private int _index; private string _xPath; private TextSearchData _textSearchData; public WebElement ByAttribute(TagAttributes tagAttribute, string attributeValue, bool exactMatch = true) { return ByAttribute(EnumHelper.GetEnumDescription(tagAttribute), attributeValue, exactMatch); } public WebElement ByAttribute(TagAttributes tagAttribute, int attributeValue, bool exactMatch = true) { return ByAttribute(EnumHelper.GetEnumDescription(tagAttribute), attributeValue.ToString(), exactMatch); } public WebElement ById(string id, bool exactMatch = true) { return ByAttribute(TagAttributes.Id, id, exactMatch); } public WebElement ById(int id, bool exactMatch = true) { return ByAttribute(TagAttributes.Id, id.ToString(), exactMatch); } public WebElement ByName(string name, bool exactMatch = true) { return ByAttribute(TagAttributes.Name, name, exactMatch); } public WebElement ByClass(string className, bool exactMatch = true) { return ByAttribute(TagAttributes.Class, className, exactMatch); } public WebElement ByTagName(TagNames tagName) { var selector = By.TagName(EnumHelper.GetEnumDescription(tagName)); _firstSelector = _firstSelector ?? selector; _searchTags.Add(tagName); return this; } public WebElement ByXPath(string xPath) { Contract.Assume(_firstSelector == null, "XPath can be only the first search criteria."); _firstSelector = By.XPath(xPath); _xPath = xPath; return this; } public WebElement ByIndex(int index) { _index = index; return this; } public WebElement First() { _index = 0; return this; } public WebElement Last() { _index = -1; return this; } public WebElement IncludeHidden() { _searchHidden = true; return this; } public WebElement ByText(string text, bool exactMatch = true) { var selector = exactMatch ? By.XPath(string.Format("//*[text()=\"{0}\"]", text)) : By.XPath(string.Format("//*[contains(text(), \"{0}\")]", text)); _firstSelector = _firstSelector ?? selector; _textSearchData = new TextSearchData { Text = text, ExactMatch = exactMatch }; return this; } private WebElement ByAttribute(string tagAttribute, string attributeValue, bool exactMatch = true) { var xPath = exactMatch ? string.Format("//*[@{0}=\"{1}\"]", tagAttribute, attributeValue) : string.Format("//*[contains(@{0}, \"{1}\")]", tagAttribute, attributeValue); var selector = By.XPath(xPath); _firstSelector = _firstSelector ?? selector; _searchProperties.Add(new SearchProperty { AttributeName = tagAttribute, AttributeValue = attributeValue, ExactMatch = exactMatch }); return this; } private string SearchCriteriaToString() { var result = _searchProperties.Select(searchProperty => string.Format("{0}: {1} ({2})", searchProperty.AttributeName, searchProperty.AttributeValue, searchProperty.ExactMatch ? "exact" : "contains")).ToList(); result.AddRange(_searchTags.Select(searchTag => string.Format("tag: {0}", searchTag))); if (_xPath != null) { result.Add(string.Format("XPath: {0}", _xPath)); } if (_textSearchData != null) { result.Add(string.Format("text: {0} ({1})", _textSearchData.Text, _textSearchData.ExactMatch ? "exact" : "contains")); } return string.Join(", ", result); } } } 


Most of the functions are public, with their help, developers will describe the elements in their tests. For almost all criteria, it is possible to search by occurrence (exactMatch). As you can see, in the end, it all comes down to XPath (and I do not rule out that XPath is a bit slower than normal search, but I personally did not notice).



WebElementExceptions.cs


 namespace Autotests.Utilities.WebElement { public class WebElementNotFoundException : Exception { public WebElementNotFoundException(string message) : base(message) { } } } 


Well, there is just one custom exception.



WebElementFilters.cs


 namespace Autotests.Utilities.WebElement { public partial class WebElement { private IEnumerable<IWebElement> FilterByVisibility(IEnumerable<IWebElement> result) { return !_searchHidden ? result.Where(item => item.Displayed) : result; } private IEnumerable<IWebElement> FilterByTagNames(IEnumerable<IWebElement> elements) { return _searchTags.Aggregate(elements, (current, tag) => current.Where(item => item.TagName == EnumHelper.GetEnumDescription(tag))); } private IEnumerable<IWebElement> FilterByText(IEnumerable<IWebElement> result) { if (_textSearchData != null) { result = _textSearchData.ExactMatch ? result.Where(item => item.Text == _textSearchData.Text) : result.Where(item => item.Text.Contains(_textSearchData.Text, StringComparison.InvariantCultureIgnoreCase)); } return result; } private IEnumerable<IWebElement> FilterByTagAttributes(IEnumerable<IWebElement> elements) { return _searchProperties.Aggregate(elements, FilterByTagAttribute); } private static IEnumerable<IWebElement> FilterByTagAttribute(IEnumerable<IWebElement> elements, SearchProperty searchProperty) { return searchProperty.ExactMatch ? elements.Where(item => item.GetAttribute(searchProperty.AttributeName) != null && item.GetAttribute(searchProperty.AttributeName).Equals(searchProperty.AttributeValue)) : elements.Where(item => item.GetAttribute(searchProperty.AttributeName) != null && item.GetAttribute(searchProperty.AttributeName).Contains(searchProperty.AttributeValue)); } } } 


Filters that are called in FindIWebElements (WebElement.cs file) to filter out items. I note only that with large datasets Linq works much longer than for and foreach, so it may be worth rewriting this code using classic loops.



Conclusion


I will see in the article the mistakes made in the article, as well as any questions in the comments.



Remarks


- The article does not provide the code for enum, EnumHelper and Executor. I will post the full code in the final part.

- the string method used. Contains this extension:

 public static bool Contains(this string source, string target, StringComparison stringComparison) { return source.IndexOf(target, stringComparison) >= 0; } 

Source: https://habr.com/ru/post/180357/



All Articles