Case Study: Locator Chaining in Selenium WebDriver

Cleaner and more readable locators in Selenium tests with Locator Chaining

Courtney Zhan
5 min readJan 14, 2023

Locator Chaining is a way to find an element relative to another one. I will illustrate how to use locator chaining with a real example in this article.

· One Selenium Test Failed
·
Analyse
·
The Problem — Multiple Elements
·
The Solution — Locator Chaining
1. Find all matching elements
2. Filter for displayed elements
3. Get the first displayable notification badge
∘ 4.
Use Locator Chaining to go back to the parent elements

One Selenium Test Failed

This test (for my father’s WhenWise app) is to verify if a group training is available on Saturday, marked with the number in a red circle.

The test passed yesterday. After a code change, the test started to fail with the below comparison error.

Failure/Error: try_for(2) { expect(select_training_class_page.first_available_date_label_regardless_displayed).to include("SAT") }
RuntimeError:
Timeout after 2 seconds with error: expected "FRI\n30/12" to include "SAT"
Diff:
@@ -1,2 +1,3 @@
-SAT
+FRI
+30/12

It seemed to select the “FRI” tab instead of “SAT”. What went wrong here?

Analysis

It could be a bad code change. However, there was actually no visible change on the week selection page.

Visually, the notification badge (red circle) was still on Saturday, and there was nothing on Friday. Visually, exactly how it was before. But something has changed since yesterday, as we got a good run in the BuildWise CT server yesterday.

This means that we need to update the test script.

The Problem — Multiple Elements

Here is the method in the test script to select the label’s text, first_available_date_label:

def first_available_date_label
driver.find_element(:xpath,
"//div[@class='carousel-item carousel-fixed-item active']//li/div/div[@class='noti-count' and text()='1']/../..").text
end

It retrieves the text for the tab which contains a div with a class noti-count where the notification count is 1.

I used TestWise’s attach-to-browser functionality to check how many divs’ have a class noti-count where the notification count is 1.

elems = driver.find_elements(:xpath,
"//div[@class='carousel-item carousel-fixed-item active']//li/div/div[@class='noti-count' and text()='1']")
puts elems.count

The result of this is 2!

I extracted the HTML source and pasted it into a text editor to verify this.

Sure enough, Friday did have a notification badge too, but not a visible one. It had a style of display:none.

The Cause: a recent change removed one step of filtering, not-matching-criteria group lessons would still be kept on the page, but their notification counts were hidden.

Knowing why the test was failing, I moved on to fixing the first_available_date_label method.

The Solution — Locator Chaining

Starting where the debugging left off — we now have two notification elements on Friday and Saturday. We just want to retrieve one that is visible.

Previously, I used driver.find_element(:xpath, “//div[@class=’carousel-item carousel-fixed-item active’]//li/div/div[@class=’noti-count’ and text()=’1']/../..”).text to find the tab with the notification element. As you can see, it is quite a long Xpath expression, and adding more constraints to find the correct one might be complex.

I used a different approach:

  1. Finding all elements with a notification badge (regardless of whether they are displayed or not)
  2. Filter out non-displayable ones
  3. Get the first displayed notification badge
  4. Somehow get the tab text based on the notification badge

1. Find all matching elements

elems = driver.find_elements(:xpath, "//div[@class='carousel-item carousel-fixed-item active']//li/div/div[@class='noti-count' and text()='1']")

The returned is a list of Elements.

2. Filter for displayed elements

Keep the elements that are displayed (without the style of display:none).

To filter by a list in Ruby, we can use the select function. Selenium has an elem.displayed? method, which checks if the element is visible and returns a boolean.

# only keep elements that are visible
display_elems = elems.select{|x| x.displayed? }

At this point, we have the correct tabs in display_elems.

3. Get the first displayable notification badge

the_notification_badge_element = display_elems.first

However, this contains the notification element, it is relative to the tab, but not the tab (i.e. the text here is 1 not FRI 30/12).

4. Use Locator Chaining to go back to the parent elements

Finally, we move to the core part: getting the tab text. One way is to customize the Xpath, by adding ../../ to get the Tab element. However, it is a bit complicated in this case because we filter out the non-displayed one.

Now we narrow down to the notification badge, one good solution is to use locator chaining. This means using the current element as the starting point and navigating to its relative parent/child elements.

Here, we want to go up to the parent div's parent div, i.e., grandparent. HTML illustrated here:

<!-- parent 2 - goal-->
<div>
<!-- parent 1 -->
<div class="noti-count">
1 <!-- starting point -->
</div>
Sat
</div>

Selecting the first display_elems and using find_element on it. Then, with XPath, go up by two parents and take the text.

the_notification_badge_element.find_element(:xpath, "../..").text

I verified in TestWise debugging mode,
puts the_notification_badge_element.find_element(:xpath, “../..”).text ,
it returns “SAT 31/12”, good!

Run test steps to return tab text, in TestWise Debugging mode.

The final updated first_available_date_label method is:

def first_available_date_label
elems = driver.find_elements(:xpath, "//div[@class='carousel-item carousel-fixed-item active']//li/div/div[@class='noti-count' and text()='1']")
display_elems = elems.select{|x| x.displayed? }
the_notification_badge_element = display_elems.first
the_notification_badge_element.find_element(:xpath, "../..").text
end

And with that, the test passes again!

A quick review of locator training syntax in Selenium.

driver.find_element(:xxx, "a").find_element(:yyy, "b")

--

--