Automation, C#, Java, Selenium

How to Automate Paginated Tables (C#, Java)

Posted by Kerry

DIFFICULTY: Advanced

Table structures (also known as grids in some circles) are fairly ubiquitous across most applications. And with good reason, as tables tend to be the best way to present data to an end user. Whether you’re dealing with e-commerce, HCM/HRM, financial systems, gaming or a variety of other types of applications, chances are you’re going to run into a table or two during your testing efforts.

First: The Wrong Approach

Say you have the following table structure and you want to grab the last name of the person in the second row.

First NameLast Name
MichaelScott
DwightSchrute
JimHalpert
PamBeesley

Easy. Create an element with the xpath //tbody/tr[3]/td[2]. Done! Right?

Not so fast. While this is definitely a method, it isn’t a very good one. It may be fine to give you results on a static, never-changing table, but it ensures a fragile test (at best) for a dynamic one.

If you have a table structure of any kind, it’s highly likely that data is being parsed from a data source. And that data source is very likely to have dynamic information.

What this means is, while you may be able to pull Dwight’s last name from that column with the above xpath today, tomorrow it may return Jim. Or Pam. Or it could be someone else entirely, or nobody at all, since the data table’s records are constantly added and updated! Add in some dynamic columns that can be switched or hidden, or some pagination, and you’ll see that it can get really complicated, really quickly.

GOT IT. SO HOW CAN I TEST THEM?

You’ve gotta fight fire with fire. If you’re dealing with dynamic tables, then the best approach is to implement a way to search and parse through tables to perform validation (or actions) on table cells and elements within those cells…dynamically!

Step 1: Clone Repository

While I will be walking through the code in C#, I’ve provided the code below in both Java and C#. Regardless of which you choose, both projects are structured the same way.

Step 2: Create Your Page Model Class

First, let’s take a peek at the paginated table we will automate. As you can see, we have three pages, each page containing up to 5 rows. With this in mind, let’s create our page object model.

Paginated table example

The page model class (EmployeeTable.cs or EmployeeTable.java) will contain all of our elements we’ll interact with for our table.

public class EmployeeTable{
        public IWebDriver _driver;
        public By employeeTable = By.Id("myTable");
        public By nextPage = By.ClassName("next_link");
        public By headers = By.XPath("./ancestor::*//th");
        public By rows = By.XPath("./tr[not(contains(@style,'none'))]");

        public EmployeeTable(IWebDriver driver)
        {
            _driver = driver;
        }
}

Step 3: Create Table Model Class

The next thing we do, is we create a class called Employee, which will have public properties (with accompanying getters and setters) that are modeled after the table structure. Notice how I am storing these as IWebElement objects (or WebElement objects, if you’re following along in Java) and not just strings. You will find out why shortly.

public class Employee{
    public IWebElement Checkbox { get; set; }
    public IWebElement ID { get; set; }
    public IWebElement FirstName { get; set; }
    public IWebElement LastName { get; set; }
    public IWebElement Job { get; set; }
 }

Step 4: Parse Table, Get Table Results

In the following getTableData() method, we are gathering all the data within the current table view, depending on which table page we are on. For each row, we are creating a new Employee object and setting each Employee property to its respective column, then storing them in a List of Employee objects.

(NOTE: You’ll notice we are explicitly referencing columns and the object type Employee for the sake of brevity and clarity in this tutorial, but you can and should take it a step further to use generic types to pass to your methods for other table objects, and parse their properties on the fly.)

private List<Employee> getTableData(By employeeTable){
    //Create new list of employees, and list of WebElements for each row, 
    //representing each column's data cell.
    List<Employee> employees = new List<Employee>();
    IList<IWebElement> tableRows = _driver.FindElement(employeeTable).FindElements(rows);

    //Iterate over each row's data WebElement and initialize each respective 
    //Employee property value.
    for (int i = 0; i < tableRows.Count; i++)
    {
        IList<IWebElement> rowCells = tableRows[i].FindElements(By.TagName("td"));
        Employee employee = new Employee();
             for (int j = 0; j < rowCells.Count; j++)
             {
                 employee.Checkbox = rowCells[0];
                 employee.ID = rowCells[1];
                 employee.FirstName = rowCells[2];
                 employee.LastName = rowCells[3];
                 employee.Job = rowCells[4];
              }
        employees.Add(employee);
    }
return employees;
}

Step 5: Query Our Table

Now that we’ve gathered all the items in our current table view, we need to be able to search those results for very specific parameters to perform actions or validation on our table.

In the following method, notice the two highlighted lines. We will be calling the previous getTableData(), which will then query the results based on an explicit columnName and columnValue we passed as parameters. Then we will call the parseResults() method, which I will explain shortly.

As you can see in the while() statement, we continue to progress forward through the paginated table, parsing and querying each page, up until we find the results we want.

private List<Employee> queryDataTable(By employeetable, By nextPage, String columnName, String columnValue){
        //Call the getTableData() method and return all values in current table 
        //view
	List<Employee> tableData = getTableData(employeeTable);
	IWebElement nextPageButton = _driver.FindElement(nextPage);
	List<Employee> results = parseResults(tableData, columnName, columnValue);
	
        //Iterate through all results and return Employee object if search 
        //criteria is met. Otherwise, click that next button and keep looking!
	while (!results.Any()){
		tableData = getTableData(employeeTable);
		results = parseResults(tableData, columnName, columnValue);
		
		if (results.Any() || !nextPageButton.Displayed)
			break;
		nextPageButton.Click();
	}
	return results;
}
private List<Employee> parseResults(List<Employee> tableData, String columnName, String columnValue)
{
        //Search only for the value in the column specified
	switch (columnName.ToLower())
	{
		case ("id"):
			return tableData.Where(x => x.ID.Text.Equals(columnValue)).ToList();
		case ("firstname"):
			return tableData.Where(x => x.FirstName.Text.Equals(columnValue)).ToList();
		case ("lastname"):
			return tableData.Where(x => x.LastName.Text.Equals(columnValue)).ToList();
        case ("job"):
			return tableData.Where(x => x.Job.Text.Equals(columnValue)).ToList();
		default:
			throw new Exception(String.Format("Supplied column name %s does not exist for the table.", columnName));
	}
}

The parseResults() method will query each table page’s data with the supplied Lambda expressions to determine if our search results have been found. Once we have a successful result, the parseResults() will pass the result back to the queryDataTable() method.

Step 6: Get The Results!

Now that we can successfully get a valid Employee object back, we want a public method that we can expose to our test classes, that can run through the above methods and give us the employees we want, and ONLY the employees we want!

public Employee getEmployee(String columnName, String columnValue){
	return searchEmployeeTable(columnName, columnValue).FirstOrDefault();
}

private List<Employee> searchEmployeeTable(String columnName, String columnValue)
{
	return queryDataTable(employeeTable, nextPage, columnName, columnValue);
}

Finally, our logic is complete and we are ready to create our tests. But before we do, here is the EmployeeTable class in its entirety.

using OpenQA.Selenium;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using static TableAutomation.EmployeeTable;

namespace TableAutomation
{
    public class EmployeeTable
    {

        public IWebDriver _driver;
        public By employeeTable = By.Id("myTable");
        public By nextPage = By.ClassName("next_link");
        public By headers = By.XPath("./ancestor::*//th");
        public By rows = By.XPath("./tr[not(contains(@style,'none'))]");

        public EmployeeTable(IWebDriver driver)
        {
            _driver = driver;
        }

        public class Employee
        {
            public IWebElement Checkbox { get; set; }
            public IWebElement ID { get; set; }
            public IWebElement FirstName { get; set; }
            public IWebElement LastName { get; set; }
            public IWebElement Job { get; set; }
        }

        public Employee getEmployee(String columnName, String columnValue)
        {
            return searchEmployeeTable(columnName, columnValue).FirstOrDefault();
        }

        private List<Employee> searchEmployeeTable(String columnName, String columnValue)
        {
            return queryDataTable(employeeTable, nextPage, columnName, columnValue);
        }

        private List<Employee> queryDataTable(By employeetable, By nextPage, String columnName, String columnValue)
        {
            List<Employee> tableData = getTableData(employeeTable);
            IWebElement nextPageButton = _driver.FindElement(nextPage);
            List<Employee> results = parseResults(tableData, columnName, columnValue);

            while (!results.Any())
            {
                tableData = getTableData(employeeTable);
                results = parseResults(tableData, columnName, columnValue);
                if (results.Any() || !nextPageButton.Displayed)
                    break;
                nextPageButton.Click();
            }
            return results;
        }

        private List<Employee> getTableData(By employeeTable)
        {
            List<Employee> employees = new List<Employee>();
            IList<IWebElement> tableRows = _driver.FindElement(employeeTable).FindElements(rows);

            for (int i = 0; i < tableRows.Count; i++)
            {
                IList<IWebElement> rowCells = tableRows[i].FindElements(By.TagName("td"));
                Employee employee = new Employee();
                for (int j = 0; j < rowCells.Count; j++)
                {
                    employee.Checkbox = rowCells[0];
                    employee.ID = rowCells[1];
                    employee.FirstName = rowCells[2];
                    employee.LastName = rowCells[3];
                    employee.Job = rowCells[4];
                }
                employees.Add(employee);
            }
            return employees;
        }

        private List<Employee> parseResults(List<Employee> tableData, String columnName, String columnValue)
        {
            switch (columnName.ToLower())
            {
                case ("id"):
                    return tableData.Where(x => x.ID.Text.Equals(columnValue)).ToList();
                case ("firstname"):
                    return tableData.Where(x => x.FirstName.Text.Equals(columnValue)).ToList();
                case ("lastname"):
                    return tableData.Where(x => x.LastName.Text.Equals(columnValue)).ToList();
                case ("job"):
                    return tableData.Where(x => x.Job.Text.Equals(columnValue)).ToList();
                default:
                    throw new Exception(String.Format("Supplied column name %s does not exist for the table.", columnName));
            }
        }
    }
}

Step 7: TEST!

If you’ll remember, our EmployeeTable logic is gathering the WebElement and not just the String value of our table data. This means we can perform validation on the text, or even perform actions on the Employee properties themselves (e.g. calling Employee.Checkbox.Click() in C# or Employee.getCheckbox().click() in Java will perform a click on the checkbox element for the row specified)! Or you can search for a nested element within each cell and perform actions on those elements. The ultimate flexibility!

This is especially helpful when interacting with tables/grids that contain more than just text values (such as drop down menus, checkboxes, or even nested grids).

Finally, below is our test class, with two test cases: One to search for the first name and verify the correct last name is returned, and one to do the inverse. What other test cases can we perform on this table?

Good luck, and happy automating.

using NUnit.Framework;
using OpenQA.Selenium;
using System.IO;
using System.Linq;
using System.Reflection;
using TableAutomation;

namespace TableAutomation
{
    [TestFixture]
    public class EmployeeTableTests : BaseTestClass
    {
        EmployeeTable _table;

        [SetUp]
        public void refreshEmployeeTable()
        {
            _driver.Navigate().Refresh();
            _table = new EmployeeTable(_driver);
        }

        [Test]
        public void SearchByFirstName()
        {
            var results = _table.getEmployee("FirstName", "Jean");
            IWebElement lastName = results.LastName;
            Assert.AreEqual(lastName.Text, "Gray");
        }

        [Test]
        public void SearchByLastName()
        {
            var results = _table.getEmployee("LastName", "Lebeau");
            IWebElement firstName = results.FirstName;
            Assert.AreEqual(firstName.Text, "Remy");
        }

    }
}
+3

Related Post

2 thoughts on “How to Automate Paginated Tables (C#, Java)

  1. Oleksiy Kyrylenko

    Nice article. In your opinion, when you create a table model, is it better to use Iwebelement type or string type for the properties?

    0
    1. Kerry

      Thanks Oleksiy! I prefer using WebElement types for the table model properties. This way, I can implement functions that perform actions on those elements, or their nested elements. If you’re dealing with tables that only have text values, or you only care about its text values, creating a table model with only string type properties will probably suit your purposes just fine.

      0

Leave A Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.