Verify text in HTML table

For any automation tester, this is a common task/problem to solve. Almost every automation project will have a verification point, which involves verifying a value in a table

Before we move on to the solution for our problem, let us create a sample HTML table, so that the work is not just theoretical.

Hosted table for testing here and HTML Table source here

Assuming that you already know which column to loop through, and all you need to check is that whether the identifier exist, the normal approach would be to get the column that you need to check the value for, and then loop through the values until you find the expected value that you are looking for. Pretty straight forward.

However, sometimes it gets a little complex where you need to find the index of the header and then find the value in that column. Something like below

WebElement table1HeaderRow = driver.findElement(By.xpath("//table/thead/tr"));
List<WebElement> headerValues = table1HeaderRow.findElements(By.tagName("th"));

String expectedHeader = "Clicks";
String expectedValue = "2961";

int i = 1;
for(WebElement e : headerValues) {
  if(e.getText() == expectedHeader)
    break;
  i++;
}
// variable i will have the matching header here
// now verify value in all rows in this column

List<WebElement> values = driver.findElements(By.xpath("//table/tbody/tr/td[" + i + "]")); // manipulate xpath of the matching column and get all values from the matching column
boolean match = false; // default match to false, so that no match will be considered failure

for(WebElement e : values) {
  if(e.getText() == expectedValue) {
    match = true;
    break;
  }
}
if (match) {
  // Do something after test step is passed
} else {
  // Do something after test step is failed
}

Let’s see the problem with the above code, before we move on to the possibly different solution. One of the significant problem with the above code is the execution time. Though we know the header and value, we still are verifying each element in the column (header) and row (value) to verify its existence, instead of directly searching the DOM for the presence of the element.

Facts and Assumptions

We have two values here. One is the header and the other is the value in the header column. In most of the cases, the value will be dynamic, which means, we would not be able to derive the xpath before execution, and has to be derived during runtime. Therefore, we will assume that the value is always dynamic and the header may/may-not be dynamic. Let’s look at both the cases one-step at a time.

Static Header and Dynamic Value

Here, let’s assume that the header column is something that we already know. In this case where we already know the index of the header column (column number of header) we can do something like below

List<WebElement> values = driver.findElements(By.xpath("//table/tbody/tr/td[3]"));

Since, we do not have to search for the header column; we already skipped one loop in the code. So what’s next?

Let’s see if we can skip the next loop. During the execution, once we find the value (2961) to be verified, the xpath could now be manipulated as

//table/tbody/tr/td[3 and .='2961']

The code can now be changed as

boolean isElementPresent = driver.findElements(By.xpath("//table/tbody/tr/td[3 and .='2961']")).size() > 0;

As you can see, the variable isElementPresent will now have the validation result without looping through all the values

Dynamic Header and Dynamic Value

What happens if we do not know the index of the header column and we need to find the header column number dynamically? Here is where things get interesting! As xpath does not directly support indexing, we would need to tweak our xpath with interesting features that xpath already provides.

Let’s start by tackling the index. Xpath provides an option to find the index of the current node by manipulating the count() method. We can get the index of the node that we are searching for using the below xpath.

count(//table/thead/tr/th[.='Clicks']/preceding-sibling::th)+1

The above xpath will give the index of the Clicks column. Since, the index starts with 0, we are incrementing the index by 1 to find out the actual index (3).

We can now get the derived xpath by using the xpath as

//tbody/tr/td[count(//table/thead/tr/th[.='Clicks']/preceding-sibling::th)+1 and .='2961']

However, there is a caveat in this xpath. The index of the first header will be returned as 1 and the index of a non-existing header will also return 1 i.e.,

count(//table/thead/tr/th[.='Sites']/preceding-sibling::th)+1

returns 1 and

count(//table/thead/tr/th[.='Rank']/preceding-sibling::th)+1

will also return 1

In other words, the below xpath’s

//tbody/tr/td[count(//table/thead/tr/th[.='Sites']/preceding-sibling::th)+1 and .='LinkedIn']

//tbody/tr/td[count(//table/thead/tr/th[.='Rank']/preceding-sibling::th)+1 and .='LinkedIn']

Will return the same node (row # 4 in column 1), however, the second xpath (with Rank header) does not exist

That brings us to the next problem which is to verify whether the actual heading exist before deriving the index. Now, how do we do that?

The trick is to find the header column first, then navigate to it’s ancestor, then to the ancestor’s sibling and derive actual index (above index manipulation code) appended by value. Sounds confusing, but it’s worth a try!

The new xpath would be

//table/thead/tr/th[.='Clicks']/ancestor::thead/following-sibling::tbody/tr/td[count(//table/thead/tr/th[.='Clicks']/preceding-sibling::th)+1 and .='2961']

The above xpath first searches for the existence of the actual heading that we are searching for (Clicks column), then moves up to the ’thead’ tag (ancestor) and then moves down to the ’tbody’ sibling (following-sibling), then takes the header index by manipulating the count() operator followed by the condition with the value (2961)

To check the correctness of the xpath, let’s validate the above xpath for a different value for a different header.

To validate row # 4 in column 1, the xpath would be

//table/thead/tr/th[.='Sites']/ancestor::thead/following-sibling::tbody/tr/td[count(//table/thead/tr/th[.='Sites']/preceding-sibling::th)+1 and .='LinkedIn']

Now changing the header value to Rank which does not exist, let’s try again with the below xpath

//table/thead/tr/th[.='Rank']/ancestor::thead/following-sibling::tbody/tr/td[count(//table/thead/tr/th[.='Rank']/preceding-sibling::th)+1 and .='LinkedIn']

The above xpath will not return any node as Rank header does not exist.

Finally, the new code would now be

String expectedHeader = "Clicks";
String expectedValue = "2961";

String derivedXpath = "//table/thead/tr/th[.='" + expectedHeader + "']/ancestor::thead/following-sibling::tbody/tr/td[count(//table/thead/tr/th[.='" + expectedHeader + "']/preceding-sibling::th)+1 and .='" + expectedValue + "']";

boolean isElementPresent = driver.findElements(By.xpath(derivedXpath)).size() > 0;

if (isElementPresent) {
  // Do something after test step is passed
} else {
  // Do something after test step is failed
}

And that’s how we deal with dynamic headers and dynamic values in HTML table.


Ending Note:

If you have to validate specific values in a table, just remember that you don’t have to loop through the entire table!

Hungry for more. Bon appetit!

Get The Best Of My Hands Delivered To Your Inbox

Subscribe to my newsletter and stay updated.