In the Previous Tutorial, we learned to locate elements and perform the action on them. In this tutorial, we will learn some advanced ways to locate elements.
Getting the list of elements with the matching locator
Sometimes we may need to find all elements on the page that matches a particular locator. For instance, get all links on the page.
WebDriver can query the browser to get the list of elements with the matching locator in the page. Once we have the list we can perform any operation on them like get the count of the returned elements or get the text of each element etc.
List<WebElement> elements = driver.findElements(By.tagName("a"));
//Print the count of returned elements
System.out.println(links.size());
//Iterate over each element
for(int i=0; i < links.size(); i++){
String text = links.get(i).getText();
System.out.println(text);
}
It is findElements not findElement.
The findElements()
method returns a list of all elements that have the matching locator passed to it. If no element is found, it returns an empty list. While the findElement()
version returns the first matching element with the given locator.
#Get the list of elements with the matching locator
links = driver.find_elements_by_tag_name("a")
# Print the count of elemets returned
print(len(elements))
# Iterate over each element
for link in links:
print(link.getText())
Did you notice? Its find_elements, not find_element
The find_elements_by_tag_name() method return a list of all elements that have the matching tag
. If no element is found, it returns an empty list. While the find_element_by_tag_name()
returns the first matching element with the given tag
.
Similarly for all other find_element methods (except find_element_by_id()
) there is a find_elements version:
- find_elements_by_name
- find_elements_by_class_name
- find_elements_by_link_text
- find_elements_by_partial_link_text
- find_elements_by_tag_name
- find_elements_by_xpath
- find_elements_by_css_selector
However, there is no find_elements_by_id. The id
attribute is supposed to have the unique value in any webpage. While technically two elements in a web page can have duplicate ‘id’, it is considered a very bad practice.
Finding a child element inside another element
In the Previous Tutorial, we learned to find an element inside the whole page. Sometimes we may need to find an element inside another element. For instance, let us say both header and footer has the Contact us
link and we want to get the one that is in the header.
We can use the same locators that we tried so far. The only difference is we won’t use the WebDriver
instance to find the element, but we’ll use the WebElement
WebElement header = driver.findElement(By.id("header"));
header.findElement(By.id("contactus")).click();
header = driver.find_element_by_id('header'))
header.find_element_by_id('contactus').click()
In the above example, we have first get the header
element and then tried to find the contactus
element inside that header only. We are not using WebDriver
because so not want to find it inside the whole page.
Tip: If you are using XPath locator to find an element inside another element, you must start the XPath query with a dot
.
.
If you start the XPath query with the usual //
, the WebDriver will try to locate the element in the whole page. The .
denotes the current node in xpath.
WebElement header = driver.findElement(By.id("header"));
header.findElement(By.xpath(".//a[text()='Contact us']")).click();
header = driver.find_element_by_id('header')
header.find_element_by_xpath(".//a[text()='Contact us']")).click()
In the above example, the .
denotes the current node which is the header
.
Interacting with a Web Table
Let us take a use case to automate:
- Navigate to https://cosmocode.io/automation-practice-webtable/
- This page has some data in a Web Table. There are hundreds of rows.
- We need to select the checkbox against the countries we’ve visited and print the country name, capital, currency and primary languages
What so big deal about it? Each checkbox must be having distinct properties like id, class etc. I can use that property to locate the desired checkbox.
Let us inspect one of the checkboxes in Google Chrome, let’s say for Australia –
As we can see it has an input
tag and a class
attribute whose value is hasVisited
.
If we inspect the next row’s checkbox, we’ll find it also has the exact same properties. In fact, to add more to your confusion, every checkbox in that web table is having exactly the same properties.
Hang on… I’ve read that XPath Tutorial. I know I can use XPath indexing to locate different rows.
Using XPath indexing to target rows/columns
Well, it’s true that we can construct an XPath such that we can differentiate different rows by playing with the indexing. But here is a catch. We’ll first try to locate elements by using this approach and then talk about the problem with it.
If we inspect the HTML snippet carefully we’ll find that –
- The table is inside <table> tag and it has an id attribute whose value is ‘countries’
- Each row is under
<tr>
tag - Each col is under
<td>
tag - The first row i.e the first
<tr>
tag refers to the table column header that we can ignore. So we need to start from the second row.
The country Afganistan is in the 2nd row (1st after the headers). So our XPath will look like this:
//table[@id='countries']//tr[2]
To locate the 1st column under the 2nd row, we need to write XPath –
//table[@id='countries']//tr[2]/td[1]
Similarly, to locate 5th row and 3rd column –
//table[@id='countries']//tr[5]/td[3]
Dynamically getting the list of rows and columns
What if the table is dynamic? We don’t know on which row our target element will be present the next time we reload this page.
In this scenario, we need to find all elements with the <tr>
tag in one go, iterate over the rows and write some conditional statement to find the desired column inside each row. We don’t care what is the order of rows or how many rows are there.
We can use the knowledge we gained in the previous section to get the list of rows. We have also learned how we can get an element inside another element.
Once we have the list of rows we’ll iterate over that list and work on a single row at a time. We’ll get columns (checkbox, country name etc) inside each row:
List <WebElement> rows = driver.findElements(By.xpath("//table[@id='countries']//tr"));
//Iterate over all rows
for(WebElement row: rows){
//Access a single row at a time
//Get all columns
List<WebElements> cols = row.findElements(By.tagName("td"));
//Access each element of the List by using an index, starting from 0.
//The first column is checkbox so we'll skip it
String countryName = cols[1].getText();
System.out.println("Country: " + countryName);
System.out.println("Capital: " + cols[2].getText());
System.out.println("Currency: " + cols[3].getText());
System.out.println("Languages: " + cols[4].getText());
if(countryName == 'United States'){
//This is the country we want to work with
}
}
# Get all rows
rows = driver.find_elements_by_xpath("//table[@id='countries']//tr"))
//Iterate over all rows
for row in rows:
# Access a single row at a time
# Get all columns of that row
cols = row.find_elements_by_tag_name("td")
# Access each element of the list by using an index, starting from 0
country = cols[1].getText()
print("Country: " + country)
print("Capital: " + cols[2].text)
print("Currency: " + cols[3].text)
print("Languages: " + cols[4].text)
if(countryName == 'United States'):
print("Found United States")
Once we found the correct row all we need is to access the checkbox inside the first column. If we inspect the first column we’ll find that it has an input
tag inside the first td
tag and the input
tag has a class
attribute with value hasVisited
. We can use this information to find the checkbox.
List <WebElement> rows = driver.findElements(By.xpath("//table[@id='countries']//tr"));
//Iterate over all rows
for(WebElement row: rows){
//Access a single row at a time
//Get all columns
List<WebElements> cols = row.findElements(By.tagName("td"));
//Access each element of the List by using an index, starting from 0.
//The first column (0th index) is checkbox so we'll skip it. We need the second column (1st index)
String countryName = cols[1].getText();
System.out.println(countryName);
if(countryName == 'United States'){
System.out.println("Found United States");
WebElement checkbox = cols.get(0).findElement(By.className("hasVisited"));
checkbox.click();
//Exit from the loop
break;
}
}
# Get all rows
rows = driver.find_elements_by_xpath("//table[@id='countries']//tr"))
//Iterate over all rows
for row in rows:
# Access a single row at a time
# Get all columns of that row
cols = row.find_elements_by_tag_name("td")
# Access each element of the list by using an index, starting from 0
country = cols[1].text
print(country)
if(country == 'United States'):
print("Found United States")
checkbox = cols[0].find_element_by.class_name("hasVisited"))
checkbox.click();
//Exit from the loop
break
Complete Code
//Update your path to chromedriver accordingly
System.setProperty("webdriver.chrome.driver", "path-to-chrome-driver/chromedriver");
WebDriver driver = new ChromeDriver();
driver.manage().window().maximize();
driver.get("https://cosmocode.io/automation-practice-webtable/");
List <WebElement> rows = driver.findElements(By.xpath("//table[@id='countries']//tr"));
for(WebElement row: rows){
//Access a single row at a time and work on it
List<WebElement> cols = row.findElements(By.tagName("td"));
//Access each element of the List by using an index, starting from 0
String countryName = cols.get(1).getText();
System.out.println("Country: " + countryName);
System.out.println("Capital: " + cols.get(2).getText());
System.out.println("Currency: " + cols.get(3).getText());
System.out.println("Languages: " + cols.get(4).getText());
if(countryName.equals("United States")){
//This is the country we want to work with
//Click the checkbox
WebElement checkbox = cols.get(0).findElement(By.className("hasVisited"));
checkbox.click();
//Exit from the loop
break;
}
}
driver.quit();
from selenium import webdriver
# Change the path accordingly
chrome_driver_path = "C:\selenium\chromedriver.exe"
driver.get("https://cosmocode.io/automation-practice-webtable")
# Get all rows
rows = driver.find_elements_by_xpath("//table[@id='countries']//tr"))
//Iterate over all rows
for row in rows:
# Access a single row at a time
# Get all columns of that row
cols = row.find_elements_by_tag_name("td")
# Access each element of the list by using an index, starting from 0
country = cols[1].text
print(country)
if(country == 'United States'):
print("Found United States")
checkbox = cols[0].find_element_by.class_name("hasVisited"))
checkbox.click();
//Exit from the loop
break
driver.quit()
Assignment
What if you have visited multiple countries? Why don’t you modify the code to select the checkbox against all countries from the list? Please post your solution in the comments. Even if it is not the right solution, please post what have you tried and I’ll help you improve your solution.
In the Next Tutorial, we’ll explore how to write a robust script by using different wait techniques.