From my experience, most web applications incorporate a table on one or more of their pages in order to organize and display data. Sometimes these tables only display data, but often there’s a button or link in each table row. When you’re testing a web application, you will need to interact with a table in your Selenium tests. It might be for scraping the data from each row to compare that data against an API response or a database query. It might be to click an “Edit” button for updating data.
Generic Table Class
The code below is my generic table class. “Header” contains a list of header cells by getting the “th” elements from the first “tr” row. There are other ways to get the header row, and in some cases, you will have to. The commented code on line 18 gets the headerRow by the “thread” element.
The Rows list is populated by getting all of the “td” elements from the “tr” elements, ignoring the first one, which contains the headers.
public class Table
{
public List<TableCell> Header { get; } = new List<TableCell>();
public List<List<TableCell>> Rows { get; } = new List<List<TableCell>>();
private By _tableElementBy;
private IWebDriver _driver;
public Table(IWebDriver driver, By tableElementBy)
{
_driver = driver;
_tableElementBy = tableElementBy;
var rows = TableElement.FindElements(By.TagName("tr"));
//Get the Header
//Not used
//var headerRow = TableElement.FindElements(By.TagName("thead"));
if (rows.Count == 0)
{
throw new ArgumentException("No rows found in the table element.");
}
var headerRow = rows[0].FindElements(By.TagName("th"));
Header = headerRow
.Select(cell => new TableCell(driver, cell))
.ToList();
//Get the Rows
foreach (var row in rows.Skip(1))
{
var rowCells = row.FindElements(By.TagName("td"))
.Select(cell => new TableCell(driver, cell))
.ToList();
if (rowCells.Count > 0)
Rows.Add(rowCells);
}
}
private IWebElement TableElement
{
get
{
return _driver.WaitForElementToExist(_tableElementBy);
}
}
}
TableCell Class
The TableCell class wraps an IWebElement. For a simple table, the class isn’t really needed. However, on one application I was testing, the last column in the table contained two buttons. I added logic in the class to get the names of the buttons and to click on a specific one.
public class TableCell
{
public IWebElement Element { get; set; }
private IWebDriver _driver;
public TableCell() { }
public TableCell(IWebDriver driver, IWebElement element)
{
_driver = driver;
Element = element;
}
//Implement specific logic here based on your application
}
Modifications to Table Class
There are changes and additions that could be applied to this table class: Error handling, efficient loading for large tables, dynamic loading, and additional features like sorting. Again, this is just a generic class, and I use it as a starting point.
Using Table Class in a Selenium Test
Let’s use the table on www.blazedemo.com. (The link goes to the reserve.php page.)
In the code below on line 10, it creates the table object by getting the element with a class name “table”.
Lines 12 – 19 test the header values against the expected values.
Lines 21 – 34 test the data against expected data from a database call. (The NotImplemented test on line 34 is there because I did not create the database table, but also as an example of how to use the TM.NotImplemented() method.)
Lines 36 – 48 clicks on a button, navigates back, and clicks on another button. The Sleeps() are in there to slow down the test to see the button clicks. It looks like canned data on those pages, so going to one page and back was too fast!
[TestMethod]
public void VerifyTable()
{
try
{
_driver.Url = "https://blazedemo.com/reserve.php"; //Go straight to the page with the table
_driver.WaitForPageToLoad();
//Get the table
Table t = new Table(_driver, By.ClassName("table"));
//Validate the headers
string expectedHeaders = "Choose Flight # Airline Departs: Arrives: Price ";
string actualHeaders = "";
foreach (TableCell header in t.Header)
{
actualHeaders += header.Element.Text + " ";
}
TM.AreEqual(expectedHeaders, actualHeaders);
//Validate the table data
//Can get database data (based on requirements):
List<List<object>> expectedFlights = DatabaseQueries.GetFlightResults(TM._connection, "Select * from Flights");
foreach (var row in t.Rows)
{
//Validate the table data somewhere in these for loops based on requirements
foreach (TableCell tableCell in row)
{
string text = tableCell.Element.Text;
}
}
//for now, add a NotImplemented test
TM.NotImplemented("Table validation logic not implemented");
//Click the "United Airlines" flight button
t.Rows[1][0].Element.Click();
_driver.WaitForPageToLoad();
Thread.Sleep(2000); //Slow down the test for visual confirmation
//Go back a page
_driver.Navigate().Back();
Thread.Sleep(2000); //Slow down the test for visual confirmation
//Click the 4th row's button
t.Rows[3][0].Element.Click();
//Continue test here....
}
catch (Exception ex)
{
//If exception, then add a log entry for debugging
TM.AddLog("Test failed.", ex.ToString());
}
}
Conclusion
As I discussed earlier, wrapping the HTML elements for use in your Selenium test makes your tests cleaner. This Table class is another example of this in action.