
Selenium 4 Relative Locators
Relative Locators
Selenium 4 brings Relative Locators (originally named Friendly Locators). This functionality was added to help you locate elements that are nearby other elements.
The available options are:
- above(): sought-after element appears above specified element
- below(): sought-after element appears below specified element
- toLeftOf(): sought-after element appears to the left of specified element
- toRightOf(): sought-after element appears to the right of specified element
- near(): sought-after element is at most 50 pixels away from specified element. There’s also an overloaded method to allow you to specify the distance.
All of these methods are overloaded to accept a By or a WebElement.
Use of Relative Locators
Given this book store application, we may want to verify that the book to the left of “Advanced Selenium in Java” is “Java For Testers”. Relative locators allows us to do this.
Here is the DOM snippet for the books
“Advanced Selenium in Java” is represented in the DOM by id pid6, and “Java For Testers” is pid5.
The WebDriver::findElement method can accept the withTagName() method which returns a RelativeLocator.RelativeBy object (a descendant of By).
1 |
driver.findElement(withTagName("li") |
From here, I can specify relative locators. I know that “Java For Testers” is to the left of “Advanced Selenium in Java” (pid6) and is below “Test Automation in the Real World” (pid1). So, I can specify both of these:
1 2 3 |
driver.findElement(withTagName("li") .toLeftOf(By.id("pid6")) .below(By.id("pid1"))); |
This returns “Java For Testers” (pid5).
1 2 3 4 5 6 7 8 9 |
@Test public void test_book5_is_left_of_book6_and_below_book1(){ String id = driver.findElement(withTagName("li") .toLeftOf(By.id("pid6")) .below(By.id("pid1"))) .getAttribute("id"); assertEquals(id, "pid5"); } |
We can use the above() and toRightOf() methods to locate “Experiences of Test Automation” (pid2):
1 2 3 4 5 6 7 8 9 |
@Test public void test_book2_is_above_book6_and_right_of_book1(){ String id = driver.findElement(withTagName("li") .above(By.id("pid6")) .toRightOf(By.id("pid1"))) .getAttribute("id"); assertEquals(id, "pid2"); } |
How does it work?
I could not identify “Java For Testers” with just a toLeftOf(By.id(“pid6”)) call. This alone would return “Test Automation in the Real World” (pid1). That’s because driver.findElement() searches from the root of the DOM, and the first <li> element that’s to the left of “Advanced Selenium in Java” is “Test Automation in the Real World”.
Selenium uses the JavaScript function getBoundingClientRect() to find the relative elements. This function returns properties of an element such as right, left, bottom, and top.
Looking at the properties of these three books, we see that both “Test Automation in the Real World” (pid1) and “Java For Testers” (pid5) both have the same position on the x-axis.
Therefore, they are both to the left of “Java For Testers”, with “Test Automation in the Real World” (pid1) being the first one found.
Another thing to consider is changes in app viewports. If I ran the passing tests from above on my app in mobile view, then naturally it would fail as the books are no longer to the left/right of other books:
I also ran into an issue with friendly locators when I attempted to use it on this ToDo application.
I tried to use the toLeftOf() method to locate the input toggle next to the ‘goodbye world’ item. Visually, this input toggle is clearly to the left of the ‘goodbye world’ label. Here’s this div in the DOM:
1 2 3 4 5 |
<div class="view" data-reactid=".0.1.2.$645c3b67-884e-4e4f-aecd-8f9367e670f8.0"> <input class="toggle" type="checkbox" data-reactid=".0.1.2.$645c3b67-884e-4e4f-aecd-8f9367e670f8.0.0"> <label data-reactid=".0.1.2.$645c3b67-884e-4e4f-aecd-8f9367e670f8.0.1">goodbye world</label> <button class="destroy" data-reactid=".0.1.2.$645c3b67-884e-4e4f-aecd-8f9367e670f8.0.2"></button> </div> |
Here’s the code I used to locate the input element to the left of the label:
1 2 3 |
driver.findElement(withTagName("input") .toLeftOf(By.xpath("//label[text()='goodbye world']"))) .click(); |
However, I hit an exception:
org.openqa.selenium.NoSuchElementException: Cannot locate an element using [unknown locator]
It seems that even though this <input> is to the left of the <label> visually, it actually is not. I called the getBoundingClientRect() function for both of these elements, and they actually overlap. Notice they both have an x position of 838, so technically the <input> is not to the left of the <label>.
And when I highlight the <label> element, I now see visually that it indeed overlaps the <input> element.
Note: This is an alpha version of Selenium WebDriver. I’ve spoken with Selenium project lead, Simon Stewart, and based on feedback, the implementation could change.
Adea Natchiah-Blay
Thank you for sharing, Angie.
Pratik Jain
Thanks for sharing. Really very informative
Nick Baynham
This is great! The “gotchas” you pointed out are significant as well
Pingback: Five Blogs – 9 October 2019 – 5blogs
pennytests
Thank you Angie
Indy
Hi Angie,
Angie Jones
RelativeLocator.RelativeBy
Stéphane Colson
Zoé Thivet
Great article 🙂 I learned about getBoundingClientRect()
Charlie Pradeep
Thanks for providing the information about relative locators. It is a good one Angie. Waiting for the selenium physical conference to meet you 🙂
Bikash Ranjan
This is very informative and was a good to know article. Thanks Angie, looking forward to more of it
lesa koorsse
Very useful. Thanks Angie
Shreen
Hi Angie, how can I use withTagName() method with page factory ?
eg with something like this –
@FindBy(xpath = “//label[@class=’text-normal’]”)
WebElement abc;