Odds are at some point you’ve come across the use of tables in a web application to display data or information to a user, giving them the option to sort and manipulate it. Depending on your application it can be quite common and something you will want to write an automated test for.
But when the table has no helpful, semantic markup (e.g. easy to use id
or class
attributes) it quickly becomes more difficult to work with and write tests against it. And if you’re able to pull something together, it will likely not work against older browsers.
Solution
You can easily traverse a table through the use of CSS Pseudo-classes.
A quick primer on Tables and CSS Pseudo-classes
Understanding the broad strokes of an HTML table’s structure goes a long way in writing effective automation against it. So here’s a quick primer.
A table has…
- a header (e.g.
<thead>
) - a body (e.g.
<tbody>
). - rows (e.g.
<tr>
) — horizontal slats of data - columns — vertical slats of data
Columns are made up of cells which are…
- a header (e.g.,
<th>
) - one or more standard cells (e.g.,
<td>
— which is short for table data)
CSS Pseudo-classes work by walking through the structure of an object and targeting a specific part of it based on a relative number (e.g. the third <td>
cell from a row in the table body). This works well with tables since we can grab all instances of a target (e.g. the third <td>
cell from each <tr>
in the table body) and use it in our test — which would give us all of the data for the third column.
Let’s step through some examples for a common set of table functionality like sorting columns in ascending and descending order.
Example
NOTE: You can see the application under test here. It’s an example from the-internet.
Java
// filename: Tables.java
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
import java.util.LinkedList;
import java.util.List;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.number.OrderingComparison.lessThan;
import static org.hamcrest.number.OrderingComparison.lessThanOrEqualTo;
import static org.hamcrest.number.OrderingComparison.greaterThanOrEqualTo;
public class Tables {
WebDriver driver;
@Before
public void setUp() throws Exception {
driver = new FirefoxDriver();
}
@After
public void tearDown() throws Exception {
driver.quit();
}
@Test
public void withoutHelpfulMarkupDuesAscending() {
{
driver.get("http://the-internet.herokuapp.com/tables");
driver.findElement(By.cssSelector("#table1 thead tr th:nth-of-type(4)")).click();
List<WebElement> dues = driver.findElements(By.cssSelector("#table1 tbody tr td:nth-of-type(4)"));
List<Double> dueValues = new LinkedList<Double>();
for(WebElement element : dues){
dueValues.add(Double.parseDouble(element.getText().replace("$", "")));
}
for(int counter = 0; counter < dueValues.size() - 1; counter++){
assertThat(dueValues.get(counter), is(lessThanOrEqualTo(dueValues.get(counter + 1))));
}
}
@Test
public void withoutHelpfulMarkupDuesDescending() {
driver.get("http://the-internet.herokuapp.com/tables");
driver.findElement(By.cssSelector("#table1 thead tr th:nth-of-type(4)")).click();
driver.findElement(By.cssSelector("#table1 thead tr th:nth-of-type(4)")).click();
List<WebElement> dues = driver.findElements(By.cssSelector("#table1 tbody tr td:nth-of-type(4)"));
List<Double> dueValues = new LinkedList<Double>();
for (WebElement element : dues) {
dueValues.add(Double.parseDouble(element.getText().replace("$", "")));
}
for (int counter = 0; counter < dueValues.size() - 1; counter++) {
assertThat(dueValues.get(counter), is(greaterThanOrEqualTo(dueValues.get(counter + 1))));
}
}
@Test
public void withoutHelpfulMarkupEmailAscending() {
driver.get("http://the-internet.herokuapp.com/tables");
driver.findElement(By.cssSelector("#table1 thead tr th:nth-of-type(3)")).click();
List<WebElement> emails = driver.findElements(By.cssSelector("#table1 tbody tr td:nth-of-type(3)"));
for(int counter = 0; counter < emails.size() -1; counter++){
assertThat(
emails.get(counter).getText().compareTo(emails.get(counter + 1).getText()),
is(lessThan(0)));
}
}
}
Python
# filename: tables.py
import unittest
from selenium import webdriver
class Tables(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
def tearDown(self):
self.driver.quit()
def test_sort_number_column_in_ascending_order_with_limited_locators(self):
driver = self.driver
driver.get('http://the-internet.herokuapp.com/tables')
driver.find_element_by_css_selector('#table1 thead tr th:nth-of-type(4)').click()
due_column = driver.find_elements_by_css_selector('#table1 tbody tr td:nth-of-type(4)')
dues = [float(due.text.replace('$','')) for due in due_column]
assert dues == sorted(dues)
def test_sort_number_column_in_descending_order_with_limited_locators(self):
driver = self.driver
driver.get('http://the-internet.herokuapp.com/tables')
driver.find_element_by_css_selector('#table1 thead tr th:nth-of-type(4)').click()
driver.find_element_by_css_selector('#table1 thead tr th:nth-of-type(4)').click()
due_column = driver.find_elements_by_css_selector('#table1 tbody tr td:nth-of-type(4)')
dues = [float(due.text.replace('$','')) for due in due_column]
assert dues == sorted(dues, reverse=True)
def test_sort_text_column_in_ascending_order_with_limited_locators(self):
driver = self.driver
driver.get('http://the-internet.herokuapp.com/tables')
driver.find_element_by_css_selector('#table1 thead tr th:nth-of-type(3)').click()
email_column = driver.find_elements_by_css_selector('#table1 tbody tr td:nth-of-type(3)')
emails = [email.text for email in email_column]
assert emails == sorted(emails)
if __name__ == "__main__":
unittest.main()
Ruby
# filename: table_sort.rb
require 'selenium-webdriver'
require 'rspec/expectations'
include RSpec::Matchers
def setup
@driver = Selenium::WebDriver.for :firefox
end
def teardown
@driver.quit
end
def run
setup
yield
teardown
end
run do
@driver.get 'http://the-internet.herokuapp.com/tables'
@driver.find_element(css: '#table1 thead tr th:nth-of-type(4)').click
dues = @driver.find_elements(css: '#table1 tbody tr td:nth-of-type(4)')
due_values = dues.map { |due| due.text.delete('$').to_f }
expect(due_values).to eql due_values.sort
end
run do
@driver.get 'http://the-internet.herokuapp.com/tables'
@driver.find_element(css: '#table1 thead tr th:nth-of-type(4)').click
@driver.find_element(css: '#table1 thead tr th:nth-of-type(4)').click
dues = @driver.find_elements(css: '#table1 tbody tr td:nth-of-type(4)')
due_values = dues.map { |due| due.text.delete('$').to_f }
expect(due_values).to eql due_values.sort.reverse
end
run do
@driver.get 'http://the-internet.herokuapp.com/tables'
@driver.find_element(css: '#table1 thead tr th:nth-of-type(3)').click
emails = @driver.find_elements(css: '#table1 tbody tr td:nth-of-type(3)')
email_values = emails.map { |email| email.text }
expect(email_values).to eql email_values.sort
end
C#
// filename: Tables.cs
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;
using System;
using System.Collections.Generic;
public class Tables
{
IWebDriver Driver;
[SetUp]
public void SetUp()
{
Driver = new FirefoxDriver();
}
[TearDown]
public void TearDown()
{
Driver.Quit();
}
[Test]
public void TableWithNoHelpfulMarkup1()
{
Driver.Navigate().GoToUrl("http://the-internet.herokuapp.com/tables");
Driver.FindElement(By.CssSelector("#table1 thead tr th:nth-of-type(4)")).Click();
IReadOnlyCollection<IWebElement> Dues = Driver.FindElements(By.CssSelector("#table1 tbody tr td:nth-of-type(4)"));
List<double> FormattedDues = new List<double>();
foreach(IWebElement Due in Dues)
{
FormattedDues.Add(double.Parse(Due.Text.Replace("$", "")));
}
Assert.That(FormattedDues, Is.Ordered);
}
[Test]
public void TableWithNoHelpfulMarkup2()
{
Driver.Navigate().GoToUrl("http://the-internet.herokuapp.com/tables");
Driver.FindElement(By.CssSelector("#table1 thead tr th:nth-of-type(4)")).Click();
Driver.FindElement(By.CssSelector("#table1 thead tr th:nth-of-type(4)")).Click();
IReadOnlyCollection<IWebElement> Dues = Driver.FindElements(By.CssSelector("#table1 tbody tr td:nth-of-type(4)"));
List<double> FormattedDues = new List<double>();
foreach(IWebElement Due in Dues)
{
FormattedDues.Add(double.Parse(Due.Text.Replace("$", "")));
}
Assert.That(FormattedDues, Is.Ordered.Descending);
}
[Test]
public void TableWithNoHelpfulMarkup3()
{
Driver.Navigate().GoToUrl("http://the-internet.herokuapp.com/tables");
Driver.FindElement(By.CssSelector("#table1 thead tr th:nth-of-type(3)")).Click();
IReadOnlyCollection<IWebElement> Emails = Driver.FindElements(By.CssSelector("#table1 tbody tr td:nth-of-type(3)"));
List<string> FormattedEmails = new List<string>();
foreach(IWebElement Email in Emails)
{
FormattedEmails.Add(Email.Text);
}
Assert.That(FormattedEmails, Is.Ordered);
}
}
If you save this file and run it here is what will happen:
- Open the browser
- Load the page
- Click the column heading
- Grab the values for that column
- Assert that the columns are sorted in the correct order (ascending or descending)
- Close the browser