Case Study: Locator Chaining in Selenium WebDriver
Cleaner and more readable locators in Selenium tests with Locator Chaining
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 div
s’ 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:
- Finding all elements with a notification badge (regardless of whether they are displayed or not)
- Filter out non-displayable ones
- Get the first displayed notification badge
- 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!
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")