Introduction
The third publication from the Design & Architecture Series is going to be dedicated to the creation of Hybrid Test Framework Controls and their Selenium WebDriver implementation. First, I am going to show you how to create several controls' interfaces that will be part of the Hybrid Test Framework Core project. After that, I am going to guide you through the process of their concrete implementation via Selenium WebDriver. Finally, you will see a fully working test example.
Hybrid Test Framework Controls
IContentElement Interface
public interface IContentElement : IElement
{
new string Content { get; }
void Hover();
}
This is the interface for all elements that can have an inner text. You can get their text through the Content
property and Hover on them.
ContentElement Selenium Driver Implementation
public class ContentElement : Element, IContentElement
{
public ContentElement(
IWebDriver driver,
IWebElement webElement,
IUnityContainer container) : base(driver, webElement, container)
{
}
public new string Content
{
get
{
return this.webElement.Text;
}
}
public void Hover()
{
Actions action = new Actions(driver);
action.MoveToElement(this.webElement).Perform();
}
}
The concrete Selenium Driver implementation inherits from the Element
base class. You can find its code in my previous article Create Hybrid Test Automation Framework - Selenium Driver Implementation. Also, to be able to use the control in the hybrid mode, we need to implement the IContentElement
interface.To get the text of the WebDriver
element, we use the Text
property of the IWebElement
interface. To simulate hover over the item, we use the Selenium Actions class.
IAnchor Interface
public interface IAnchor : IContentElement
{
string Url { get; }
}
The only particular part of the interface is the Url
property.
Anchor Selenium Driver Implementation
public class Anchor : ContentElement, IAnchor
{
public Anchor(
IWebDriver driver,
IWebElement webElement,
IUnityContainer container) : base(driver, webElement, container)
{
}
public string Url
{
get
{
return this.webElement.GetAttribute("href");
}
}
}
The Anchor
class inherits from the ContentElement
base class instead from the Element
class. We use the GetAttribute
IWebElement
's method to get the HREF of the item.
ICheckbox Interface
public interface ICheckbox : IContentElement
{
bool IsEnabled { get; }
bool IsChecked { get; }
void Check();
void Uncheck();
}
Checkbox Selenium Driver Implementation
public class Checkbox : ContentElement, ICheckbox
{
public Checkbox(
IWebDriver driver,
IWebElement webElement,
IUnityContainer container) : base(driver, webElement, container)
{
}
public bool IsChecked
{
get
{
return this.webElement.Selected;
}
}
public void Check()
{
if (!this.webElement.Selected)
{
this.webElement.Click();
}
}
public void Uncheck()
{
if (this.webElement.Selected)
{
this.webElement.Click();
}
}
}
We can use the Selected
property of the IWebElement
interface to find out if the checkbox is checked. To check or uncheck the item, we just use the Click
method. It is possible to have only a single Check
method with a boolean parameter carrying the information for the check status.
IComboBox Interface
public interface IComboBox : IContentElement
{
string SelectedValue { get; }
void SelectValue(string value);
}
Through the SelectedValue
property, we get a string
representation of the currently selected item. SelectValue
method selects an option by its text value. If we need, we can add more variations of this method (select by index).
ComboBox Selenium Driver Implementation
public class ComboBox : ContentElement, IComboBox
{
public ComboBox(
IWebDriver driver,
IWebElement webElement,
IUnityContainer container) : base(driver, webElement, container)
{
}
public string SelectedValue
{
get
{
var select = new SelectElement(this.webElement);
return select.SelectedOption.Text;
}
}
public void SelectValue(string value)
{
var select = new SelectElement(this.webElement);
select.SelectByValue(value);
}
}
The pure WebDriver
doesn't provide an easy way to work with comboBox
es. However, we use the SelectElement
wrapper, part of the Selenium
.Support
NuGet. Through its SelectedOption
property, we can get the currently selected item. Also, it provides a couple of different methods to select options. You can read more about it in my article - Getting Started with WebDriver C# in 10 Minutes.
ITextBox Interface
public interface ITextBox : IContentElement
{
string Text { get; set; }
bool IsEnabled { get; }
void Focus();
void SimulateRealTyping(string valueToBeTyped);
}
To simplify the API, we can get or set the text box's text though the Text
property. Also, you can check if the item is disabled or enabled via the IsEnabled
property. The Focus
method is identical to Hover
.
TextBox Selenium Driver Implementation
public class TextBox : ContentElement, ITextBox
{
public TextBox(
IWebDriver driver,
IWebElement webElement,
IUnityContainer container) : base(driver, webElement, container)
{
}
public string Text
{
get
{
return this.webElement.GetAttribute("value");
}
set
{
this.webElement.Clear();
this.webElement.SendKeys(value);
}
}
public void Focus()
{
Actions action = new Actions(driver);
action.MoveToElement(this.webElement).Perform();
}
}
To get the text of the text box, we can use again the GetAttribute
method but this time we get the value attribute. The Focus
method's implementation is identical to the one of the Ho<span class="bold_text">ver</span>
method.
IDiv Interface
public interface IDiv : IContentElement
{
}
Div Selenium Driver Implementation
public class Div : ContentElement, IDiv
{
public Div(
IWebDriver driver,
IWebElement webElement,
IUnityContainer container) : base(driver, webElement, container)
{
}
}
The Div
element is nothing more than a content
element. However, to be able to use it in a hybrid manner, we need to have a separate class.
IKendoGrid Interface
public interface IKendoGrid : IElement
{
void RemoveFilters();
int TotalNumberRows();
void Reload();
int GetPageSize();
int GetCurrentPageNumber();
void ChangePageSize(int newSize);
void NavigateToPage(int pageNumber);
void Sort(string columnName, SortType sortType);
void Filter(
string columnName,
FilterOperator filterOperator,
string filterValue);
void Filter(params GridFilter[] gridFilters);
List<T> GetItems<T>() where T : class;
}
Sometimes, there are more complex controls such as the Kendo Grid. The interface contains the most common usages of the control wrapped as C# methods.
KendoGrid Selenium Driver Implementation
public class KendoGrid : ContentElement, IKendoGrid
{
private readonly string gridId;
private readonly IJavaScriptExecutor driver;
public KendoGrid(
IWebDriver driver,
IWebElement webElement,
IUnityContainer container) : base(driver, webElement, container)
{
this.gridId = webElement.GetAttribute("id");
this.driver = (IJavaScriptExecutor)driver;
}
public void RemoveFilters()
{
string jsToBeExecuted = this.GetGridReference();
jsToBeExecuted = string.Concat(jsToBeExecuted, "grid.dataSource.filter([]);");
this.driver.ExecuteScript(jsToBeExecuted);
}
public int TotalNumberRows()
{
string jsToBeExecuted = this.GetGridReference();
jsToBeExecuted = string.Concat(jsToBeExecuted, "grid.dataSource.total();");
var jsResult = this.driver.ExecuteScript(jsToBeExecuted);
return int.Parse(jsResult.ToString());
}
public void Reload()
{
string jsToBeExecuted = this.GetGridReference();
jsToBeExecuted = string.Concat(jsToBeExecuted, "grid.dataSource.read();");
this.driver.ExecuteScript(jsToBeExecuted);
}
public int GetPageSize()
{
string jsToBeExecuted = this.GetGridReference();
jsToBeExecuted = string.Concat(jsToBeExecuted, "return grid.dataSource.pageSize();");
var currentResponse = this.driver.ExecuteScript(jsToBeExecuted);
int pageSize = int.Parse(currentResponse.ToString());
return pageSize;
}
public void ChangePageSize(int newSize)
{
string jsToBeExecuted = this.GetGridReference();
jsToBeExecuted = string.Concat
(jsToBeExecuted, "grid.dataSource.pageSize(", newSize, ");");
this.driver.ExecuteScript(jsToBeExecuted);
}
public void NavigateToPage(int pageNumber)
{
string jsToBeExecuted = this.GetGridReference();
jsToBeExecuted = string.Concat
(jsToBeExecuted, "grid.dataSource.page(", pageNumber, ");");
this.driver.ExecuteScript(jsToBeExecuted);
}
public void Sort(string columnName, SortType sortType)
{
string jsToBeExecuted = this.GetGridReference();
jsToBeExecuted =
string.Concat(jsToBeExecuted,
"grid.dataSource.sort({field: '",
columnName, "', dir: '",
sortType.ToString().ToLower(), "'});");
this.driver.ExecuteScript(jsToBeExecuted);
}
public List<T> GetItems<T>() where T : class
{
string jsToBeExecuted = this.GetGridReference();
jsToBeExecuted =
string.Concat(
jsToBeExecuted,
"return JSON.stringify(grid.dataSource.data());");
var jsResults = this.driver.ExecuteScript(jsToBeExecuted);
var items = JsonConvert.DeserializeObject<List<T>>(jsResults.ToString());
return items;
}
public void Filter(
string columnName,
FilterOperator filterOperator,
string filterValue)
{
this.Filter(new GridFilter(columnName, filterOperator, filterValue));
}
public void Filter(params GridFilter[] gridFilters)
{
string jsToBeExecuted = this.GetGridReference();
StringBuilder sb = new StringBuilder();
sb.Append(jsToBeExecuted);
sb.Append("grid.dataSource.filter({ logic: \"and\", filters: [");
foreach (var currentFilter in gridFilters)
{
DateTime filterDateTime;
bool isFilterDateTime = DateTime.TryParse(currentFilter.FilterValue, out filterDateTime);
string filterValueToBeApplied =
isFilterDateTime ?
string.Format("new Date({0}, {1}, {2})",
filterDateTime.Year,
filterDateTime.Month - 1,
filterDateTime.Day) :
string.Format("\"{0}\"", currentFilter.FilterValue);
string kendoFilterOperator =
this.ConvertFilterOperatorToKendoOperator(currentFilter.FilterOperator);
sb.Append(string.Concat("{ field: \"",
currentFilter.ColumnName,
"\", operator: \"",
kendoFilterOperator,
"\", value: ",
filterValueToBeApplied,
" },"));
}
sb.Append("] });");
jsToBeExecuted = sb.ToString().Replace(",]", "]");
this.driver.ExecuteScript(jsToBeExecuted);
}
public int GetCurrentPageNumber()
{
string jsToBeExecuted = this.GetGridReference();
jsToBeExecuted = string.Concat(jsToBeExecuted, "return grid.dataSource.page();");
var result = this.driver.ExecuteScript(jsToBeExecuted);
int pageNumber = int.Parse(result.ToString());
return pageNumber;
}
private string GetGridReference()
{
string initializeKendoGrid =
string.Format
("var grid = $('#{0}').data('kendoGrid');", this.gridId);
return initializeKendoGrid;
}
private string ConvertFilterOperatorToKendoOperator(FilterOperator filterOperator)
{
string kendoFilterOperator = string.Empty;
switch (filterOperator)
{
case FilterOperator.EqualTo:
kendoFilterOperator = "eq";
break;
case FilterOperator.NotEqualTo:
kendoFilterOperator = "neq";
break;
case FilterOperator.LessThan:
kendoFilterOperator = "lt";
break;
case FilterOperator.LessThanOrEqualTo:
kendoFilterOperator = "lte";
break;
case FilterOperator.GreaterThan:
kendoFilterOperator = "gt";
break;
case FilterOperator.GreaterThanOrEqualTo:
kendoFilterOperator = "gte";
break;
case FilterOperator.StartsWith:
kendoFilterOperator = "startswith";
break;
case FilterOperator.EndsWith:
kendoFilterOperator = "endswith";
break;
case FilterOperator.Contains:
kendoFilterOperator = "contains";
break;
case FilterOperator.NotContains:
kendoFilterOperator = "doesnotcontain";
break;
case FilterOperator.IsAfter:
kendoFilterOperator = "gt";
break;
case FilterOperator.IsAfterOrEqualTo:
kendoFilterOperator = "gte";
break;
case FilterOperator.IsBefore:
kendoFilterOperator = "lt";
break;
case FilterOperator.IsBeforeOrEqualTo:
kendoFilterOperator = "lte";
break;
default:
throw new ArgumentException("The specified filter operator is not supported.");
}
return kendoFilterOperator;
}
}
You can read a lot more in my dedicated article - Automate Telerik Kendo Grid with WebDriver and JavaScript.
Hybrid Test Framework Controls in Test
Page Object
public partial class BingMainPage : BasePage
{
public BingMainPage(
IElementFinder elementFinder,
INavigationService navigationService) :
base(elementFinder, navigationService)
{
}
public void Navigate()
{
this.NavigationService.NavigateByAbsoluteUrl(@"http://www.bing.com/");
}
public void Search(string textToType)
{
this.SearchBox.Text = textToType;
this.GoButton.Click();
}
public int GetResultsCount()
{
int resultsCount = default(int);
resultsCount = int.Parse(this.ResultsCountDiv.Content);
return resultsCount;
}
}
Here, there is nothing different compared to the examples from the previous articles. We just pass the IElementFinder
and INavigationService
interfaces to the constructor of the base page class. You can read more about this type of page objects in my dedicated article - Page Objects That Make Code More Maintainable.
Page Object Map
public partial class BingMainPage
{
public ITextBox SearchBox
{
get
{
return this.ElementFinder.Find<ITextBox>(By.Id("sb_form_q"));
}
}
public IButton GoButton
{
get
{
return this.ElementFinder.Find<IButton>(By.Id("sb_form_go"));
}
}
public IDiv ResultsCountDiv
{
get
{
return this.ElementFinder.Find<IDiv>(By.Id("b_tween"));
}
}
}
The most interesting part is situated here. All properties do not return the WebDriver
's IWebElement
interface. To support different driver's implementations, the page object map now uses the different hybrid test framework's interfaces. Their concrete implementation will be resolved at runtime through Unity IoC container.
Page Object Asserter
public static class BingMainPageAsserter
{
public static void AssertResultsCountIsAsExpected(
this BingMainPage page,
int expectedCount)
{
Assert.AreEqual(
page.ResultsCountDiv.Content,
expectedCount,
"The results count is not as expected.");
}
}
Test Example
[TestClass]
public class BingTests
{
private IDriver driver;
private IUnityContainer container;
[TestInitialize]
public void SetupTest()
{
this.container = new UnityContainer();
this.container.RegisterType<IDriver, SeleniumDriver>();
this.container.RegisterType<INavigationService, SeleniumDriver>();
this.container.RegisterType<IBrowser, SeleniumDriver>();
this.container.RegisterType<ICookieService, SeleniumDriver>();
this.container.RegisterType<IDialogService, SeleniumDriver>();
this.container.RegisterType<IElementFinder, SeleniumDriver>();
this.container.RegisterType<IJavaScriptInvoker, SeleniumDriver>();
this.container.RegisterType<IElement, Element>();
this.container.RegisterType<IButton, Button>();
this.container.RegisterType<ITextBox, TextBox>();
this.container.RegisterType<IDiv, Div>();
this.container.RegisterType<IContentElement, ContentElement>();
this.container.RegisterInstance<IUnityContainer>(this.container);
this.container.RegisterInstance<BrowserSettings>(
BrowserSettings.DefaultFirefoxSettings);
this.driver = this.container.Resolve<IDriver>();
}
[TestMethod]
public void SearchForAutomateThePlanet()
{
var bingMainPage = this.container.Resolve<BingMainPage>();
bingMainPage.Navigate();
bingMainPage.Search("Automate The Planet");
bingMainPage.AssertResultsCountIsAsExpected(264);
}
}
As in the previous article's test code, we first register all driver instances. They will be resolved as their Selenium Driver implementation. The same is valid for all web controls. So when you resolve the BingMainPage
, the SeleniumDriver
is passed as parameter to the page object's constructor. Also, when the ElementFinderService
tries to determine the different controls' interfaces, it will get their WebDriver
's implementation.
Design & Architecture
CodeProject
The post Create Hybrid Test Framework – Selenium Driver Controls appeared first on Automate The Planet.
All images are purchased from DepositPhotos.com and cannot be downloaded and used for free.
License Agreement