Automate Telerik Kendo Grid with WebDriver and JavaScript

WebDriver
29 Shares
Automate Telerik Kendo Grid

Have you had this problem to try to automate custom-tuned web controls? Probably, your team has purchased these from some dedicated UI controls vendor. There are a lot of them on the market. I am working at Progress, one of the leading providers of UI controls. My team develops the backend systems of our company and for the UI parts we are using the Telerik’s controls. As you can guess, I needed to automate most of these custom controls. The approach that I am going to share with you in this article applies to all tuned controls no matter the vendor. Here I am going to show you how to automate Telerik Kendo Grid control.

Automate Custom-Tuned Web Controls WebDriver + JavaScript

To automate these controls I used Selenium WebDriver and the JavaScript API, provided by the controls’ vendor. Most of the web controls on the market expose their UI JavaScript API. Another approach is to try to automate them through the regular WebDriver functions, but usually this never works because these type of controls depend heavily on JavaScript.

Automation Use Case

In the examples, I am going to automate the Telerik Kendo Grid control.
Telerik Kendo Grid

Automate Telerik Kendo Grid

Create Kendo Grid Element

First you need to find the JavaScript API documentation of the control you are trying to automate. You can find the documentation for the Telerik Kendo Grid on the following URL.
Next you need to test some of the methods through the browsers’ JavaScript Console.
Open the Kendo Grid’s demo in Chrome and open developers tools. Open the Console.
Kendo Grid Demo Chrome JavaScript Console
Test all API’s methods that you are going to use in the automation process. The next step is to wrap all of them in C# code.
Here is the skeleton of the Kendo Grid Element.

public class KendoGrid
{
private readonly string gridId;
private readonly IJavaScriptExecutor driver;
public KendoGrid(IWebDriver driver, IWebElement gridDiv)
{
this.gridId = gridDiv.GetAttribute(id);
this.driver = (IJavaScriptExecutor)driver;
}
public void NavigateToPage(int pageNumber)
{
string jsToBeExecuted = this.GetGridReference();
jsToBeExecuted = string.Concat(jsToBeExecuted, grid.dataSource.page(, pageNumber, ););
this.driver.ExecuteScript(jsToBeExecuted);
}
private string GetGridReference()
{
string initializeKendoGrid = string.Format(var grid = $(‘#{0}’).data(‘kendoGrid’);, this.gridId);
return initializeKendoGrid;
}
}

All operations are executed through the JavaScript executor of WebDriver, because of that the first parameter of the new element is the WebDriver’s instance. It is cast in the constructor to IJavaScriptExecutor. The second parameter is the wrapper DIV element of the grid. We pass it to the constructor only to get its ID.
Kendo Grid Div
With the following JS code, we can get an instance of Kendo Grid.

var grid = $(#grid).data(kendoGrid);

This code is wrapped in the GetGridReference method. It is later called before any other API’s methods.

grid.dataSource.page(5);

The above JS code is used to navigate to a particular grid page. It is wrapped in the NavigateToPage method.

Add More Methods to Kendo Grid Element

This is the skeleton of the Kendo Grid element. You can add as many methods as you need. Below I am going to show you the most interesting ones and their usage in tests.

Sort

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);
}

Just pass the name of the column that you want to sort and choose between Asc and Desc sorting.

Change Page Size

public void ChangePageSize(int newSize)
{
string jsToBeExecuted = this.GetGridReference();
jsToBeExecuted = string.Concat(jsToBeExecuted, grid.dataSource.pageSize(, newSize, ););
this.driver.ExecuteScript(jsToBeExecuted);
}

You can show more items in the grid by just passing the new number as a parameter.

GetItems

One of the most unusual methods is GetItems. The JavaScript method returns a JSON representation of the data items in the grid. After that, we deserialize it to a list of C# objects. Next, you can perform different actions and validations on them.

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;
}

Something specific to the WebDriver’s IJavaScriptExecutor is that to get the JS method’s response you need to add the return keyword (in other automation frameworks this is working out of the box). Secondly in order the result to be formatted as JSON you need to use the JavaScript’s method JSON.stringify.
To deserialize the JSON to C#, I installed the Newtonsoft.Json NuGet that provides the JsonConvert.DeserializeObject method.

Filter

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);
}
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;
}

Another interesting and a little bit complicated logic is the filtering. There are two overloads of the Filter C# wrapper. The first applies a single filter. The second one can be used to apply multiple ones. If dates need to be filtered the code creates new JS date objects otherwise the JS’s filter function won’t work. You can use the FilterOperator enum to choose the filter type. Later it is converted to the API’s version.

public enum FilterOperator
{
EqualTo,
NotEqualTo,
LessThan,
LessThanOrEqualTo,
GreaterThan,
GreaterThanOrEqualTo,
StartsWith,
EndsWith,
Contains,
NotContains,
IsAfter,
IsAfterOrEqualTo,
IsBefore,
IsBeforeOrEqualTo
}

Kendo Grid Element Full Source Code

public class KendoGrid
{
private readonly string gridId;
private readonly IJavaScriptExecutor driver;
public KendoGrid(IWebDriver driver, IWebElement gridDiv)
{
this.gridId = gridDiv.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;
}
}

Kendo Grid Element’s Usage in Tests

[TestClass]
public class KendoGridTests
{
private IWebDriver driver;
[TestInitialize]
public void SetupTest()
{
this.driver = new FirefoxDriver();
this.driver.Manage().Timeouts().SetPageLoadTimeout(TimeSpan.FromSeconds(5));
}
[TestCleanup]
public void TeardownTest()
{
this.driver.Quit();
}
[TestMethod]
public void FilterContactName()
{
this.driver.Navigate().GoToUrl(@”http://demos.telerik.com/kendo-ui/grid/index”);
var kendoGrid = new KendoGrid(this.driver, this.driver.FindElement(By.Id(grid)));
kendoGrid.Filter(ContactName, Enums.FilterOperator.Contains, Thomas);
var items = kendoGrid.GetItems<GridItem>();
Assert.AreEqual(1, items.Count);
}
[TestMethod]
public void SortContactTitleDesc()
{
this.driver.Navigate().GoToUrl(@”http://demos.telerik.com/kendo-ui/grid/index”);
var kendoGrid = new KendoGrid(this.driver, this.driver.FindElement(By.Id(grid)));
kendoGrid.Sort(ContactTitle, SortType.Desc);
var items = kendoGrid.GetItems<GridItem>();
Assert.AreEqual(Sales Representative, items[0]);
Assert.AreEqual(Sales Representative, items[1]);
}
[TestMethod]
public void TestCurrentPage()
{
this.driver.Navigate().GoToUrl(@”http://demos.telerik.com/kendo-ui/grid/index”);
var kendoGrid = new KendoGrid(this.driver, this.driver.FindElement(By.Id(grid)));
var pageNumber = kendoGrid.GetCurrentPageNumber();
Assert.AreEqual(1, pageNumber);
}
[TestMethod]
public void GetPageSize()
{
this.driver.Navigate().GoToUrl(@”http://demos.telerik.com/kendo-ui/grid/index”);
var kendoGrid = new KendoGrid(this.driver, this.driver.FindElement(By.Id(grid)));
var pageNumber = kendoGrid.GetPageSize();
Assert.AreEqual(20, pageNumber);
}
[TestMethod]
public void GetAllItems()
{
this.driver.Navigate().GoToUrl(@”http://demos.telerik.com/kendo-ui/grid/index”);
var kendoGrid = new KendoGrid(this.driver, this.driver.FindElement(By.Id(grid)));
var items = kendoGrid.GetItems<GridItem>();
Assert.AreEqual(91, items.Count);
}
}

Above you can find sample test usages of the most compelling Kendo Grid Element’s methods.

  • Nomme

    This was very interesting and complicated! I know this is specific to the Kendo Grid control but the line below has always(read this a few times) troubled me! How did you know to put ‘kendoGrid’ inside of the data “method”? Why kendoGrid as opposed to anything else?

    var grid = $(‘#{0}’).data(‘kendoGrid’);

    • Hey, thank you for your question. Here is the definition of the data method – “Store arbitrary data associated with the matched elements or return the value at the named data store for the first element in the set of matched elements.”. So we get the data associated with the kendoGrid object. https://uploads.disquscdn.com/images/e6c3576f6feacdc4838643b3dc476fc0051a946c23517a05e8d772c433cda373.png

      • Nomme

        Thank you for such a quick reply! Sorry but I still don’t get it. Not that I am asking for support so feel free to not answer. I’ve used your/this article as a general guide to do something similar with the Telerik RAD AJAX grid control. That was with a previous employer and it worked decently well. I am now trying to do something similar to this but with this employer they use DevExpress controls. But basically trying to create a Page Object for a Grid that sorts, filters, and columns are swappable. I had already read about the .data() method on jQuery but I still didn’t get it.

        So, in your case, you knew to use ‘kendoGrid’ because by inspecting the HTML page source you could tell the name of the data source was ‘kendoGrid’?

        • Yeah, exactly I inspected it in the HTML, also it was a couple of times mentioned in the documentation. The data method is not working in your case? Can you give me a link to check it out?

          • Nomme

            How can I send you instructions and credentials to log in to our application? Your email is?