In the previous article, we created a Login Page object and used it to create a login and logout test. In this one, we'll take a look XPath locators.

This article assumes that you've installed and configured Composer, Codeception, PhpUnit, MODX, Java, WebDriver, and ChromeDriver as described in earlier articles.
MODX Manager Code
It's almost impossible to create non-trivial tests of operations in the MODX Manager without using XPath locators. Much of the Manager's underlying HTML code is created with ExtJS/ModExt, which doles out element CSS Ids on the fly.
Whenever the Manager is modified (even by the addition of a CMP), these Ids can change. So any Functional or Acceptance test that relies on them is likely to fail eventually. Luckily, we can use XPath locators that will still work when that happens. Unluckily, XPath locators are tricky to write and there are always many different ways to write a working XPath locator for a given element.
In this article, we'll take a look at how XPath locators work, and see some examples, but this won't be an exhaustive description of them.
XPath Locators and the DOM
XPath locators are based on the structure of the DOM (Document Object Model). When the source HTML for a web page is processed, the DOM is parsed into a tree structure containing nodes, and their children (which are also nodes). If you think of the web page as an object, the DOM is just another way of representing that object.
The DOM is most often accessed by JavaScript, though that access is mostly hidden from you when using Codeception functional or acceptance tests. It's also largely hidden when you use JQuery to access the DOM. The DOM can be accessed by any number of languages (in theory, an infinite number of languages). No matter how it's accessed, it's the same DOM. We'll be strictly focusing on the HTML DOM since we're dealing with HTML web pages.
DOM nodes start at the root (the html
tag). They all have one parent, and potentially children and siblings, each of which can also have children and siblings.
The freeCodeCamp site has a nice introduction to the DOM here where you can see the HTML of a web page and a graphical representation of the DOM nodes.
Every part of the DOM is a node, and some of those nodes are also elements. Elements are nodes like div
, input
, p
, span
etc. that can have attributes like a class and/or an id.
XPath
XPath is just a language that provides access to the DOM, usually for the purpose of providing locators to be used for interacting with the DOM.
Let's start with the two XPath locators we used in the previous article. Here's a slightly modified version of the Manager code they're based on:
<ul id="modx-user-menu"> <li id="limenu-user" class="top"> <a href="javascript:;"><span id="user-avatar"> <img src="https://www.gravatar.com/avatar/###"/></span> <span id="user-username">Default Admin User</span></a> <ul class="modx-subnav"> <li id="profile"> <a href="?a=security/profile">Edit Account <span class="description"> Update account email, password or info</span> </a> </li> <li id="messages"> <a href="?a=security/message">Messages <span class="description">View and send messages</span> </a> </li> <li id="logout"> <a href="?a=security/logout" onclick=" MODx.logout(); return false; ">Logout<span class="description"> Log out of the Manager</span> </a> </li> </ul> </li> </ul>
The first line contains the element for the user drop-down menu. It's the username at the top right of the Manager panel. This one has a stable Id, so we can hover over it or click on it to make the menu appear with $I->moveMouseOver('#modx-user-menu')
.
To log out, though, we need to click on this link, which has no Id:
<a href="?a=security/logout" onclick=" MODx.logout(); return false; ">Logout <span class="description"> Log out of the Manager </span> </a>
We used this locator for that link: "//a[contains(@href,'?a=security/logout')]"
. This XPath locator starts with "//" which is the root node of the DOM. This code says to examine the whole document looking for an a
tag that has a child with an href
node with the text, '?a=security/logout'
.
In order to work in a functional or acceptance test, your XPath locator must be unique. To make sure this is the case, fire up Dev. Tools with Ctrl-shift-i or F12 (I use Chrome for this).
Once the tool window appears, click on the "Elements" tab and type Ctrl-F to open the Find window at the bottom of the panel. Paste in your XPath locator (without the outside quotes). If you see no results, your XPath locator is not correct. If you see multiple results, your XPath locator is not unique. If you see exactly one result (and it's the element you want to use), you're in business.
Try it in the MODX manager with our locator: //a[contains(@href,'?a=security/logout')]
. You should see the correct element highlighted and only one result.
Finding XPath with Dev. Tools
You can get the XPath for an element in Chrome Dev. Tools very easily. Open Dev. Tools with F12 or Ctrl-shift-i. Click on the Inspector icon (the rectangle pierced by an arrow at the left end of the Dev. Tools toolbar (at the top of the Dev. Tools window), then click on the page element you want an XPath for. That will put the HTML for that element in the tool window. The element you want may be below that if it's inside another element, or in a drop-down menu that requires hovering. Find the element in the HTML code and right-click on it. Select Copy -> Copy XPath (near the bottom of the list). The XPath will be in the clipboard and can be pasted anywhere. Type Crtl-F and paste it in the Find window with Ctrl-v to make sure it's unique.
Doing that with our logout link results in this XPath which is probably better than the one we used, because the parent of our logout link has an ID:
//*[@id="logout"]/a
The XPath above says to search the whole document for an element with the Id "logout" and select its only child a
tag.
If we had selected "Copy Full XPath", we'd have gotten this lengthy version:
/html/body/div[2]/div[1]/div/ul[1]/li[1]/ul/li[3]/a
This is the full path through the tree. The XPath above says (left-to right), look at the second div
under the body
tag, go to its first child div
, then that div
's only child div
. Look in the first child ul
tag, then in its first li
tag and that tag's child ul
tag and that tag's third li
tag, and select its only child a
tag.
There are a number of browser tools to generate XPaths, but I've never found one as convenient or reliable as the Dev. Tools method described above, and some of them interfere with normal operations.
When you've discovered the XPath locator for an element, don't forget to put quotes around it when you paste it as a Codeception method parameter. If the XPath locator contains single quotes, be sure to enclose it in double quotes. If it contains double quotes, use single quotes around it.
The Yes Button
Our second XPath locator from the logout method (for the "Yes" button) requires a little more effort. The Yes/No dialog is popped up by JavaScript, so you won't see it in the Page source.
You can see the code if you open Dev. Tools, click on the logout link (to pop up the dialog), and then click on the on the Inspector icon and on the "Yes" button. At this point, you can right-click on the button code and select "Copy -> Copy XPath." It won't do you any good, though, because it will come up with the unreliable Id placed there by ExtJS/ModExt. You will see the code for the button, though in the Dev Tools window.
<button type="button" id="ext-gen222" class=" x-btn-text">Yes</button>
One solution is to select "Copy -> Copy Full XPath." That will produce the whopper below:
/html/body/div[17]/div[2]/div[2]/div/div/div/div[1]/table/tbody/tr/td[1]/table/tbody/tr/td[2]/span/em/button
Using that is fine if you're not up to composing your own XPath locator for it, but the one we've used will do as well:
//button[contains(text(), 'Yes')]
That says to search the whole document and select a button tag contains the text "Yes." Luckily, there's only one of those on the page at the time we need to look for it.
More Examples
XPath locators can be chained, usually by appending them after /
or //
, the added part will search relative to the preceding part. Using /
will look for an immediate child of the node identified by the previous part. Using //
will look for any descendent.
Consider the following HTML document (also available at GitHub, here):
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div class="firstDiv" id="first_div"> <img src="http://some/url" alt="Baseball Team"> <p class="something">Some Text</p> <p class="something">Some Other Text</p> <p class="something">Still Other Test</p> </div> <div class="SecondDiv"> <img src="http://some/other/url" alt="Some Text" height="42" width="42"> <p class="something">More Text</p> </div> </body> </html>
Suppose we want to select the p
tag containing "Some Other Text." Here's one solution:
//body/div/p[@class="something" and contains(text(),"Some Other Text")]
Here's a shorter version using an extra //
.
//body//p[@class="something" and contains(text(),"Some Other Text")]
We know it's in the body, so the first part (//body//
) says to search from the root for a body
tag, then search the whole body for a p
tag containing that text.
But what if we want to select that p
tag regardless of its content or class because either one might change? This XPath locator would do that:
//body/div[1]/p[2]
The XPath above says to look in the body
tag for the second p
tag in the first div
. The downside of this locator is that if someone adds an extra p
tag above our target, or an extra div
tag above the first one, we'll be selecting the wrong element.
Suppose we want a locator for the first img
tag. This one would do it:
//img[@src='http://some/url']
But what if the image changes often because the src
value is generated on the fly to show random photos of the team, so the src
tag is not reliable? You can select the img
by its alt
tag.
//img[contains(@alt,'Baseball Team')]
How about our second img
tag? Suppose that both the image path and the alt value change often, but the size remains the same (and is unique on the page). This XPath locator will always find it:
//img[@height=42 and @width=42]
Important: Any XPath that contains ext-gen
or ext-comp
will be unreliable because the number they contain may change. In those cases you may need to create your own custom XPath. Google is your friend here. Search for something like XPath select button by text
or XPath select href by link text
. The odds are good that you'll find a StackOverflow answer that you can modify to meet your needs.
Other Resources
There are many XPath tutorials on the web. It's difficult to find a good one because many are about XML rather then HTML. Others are for languages other than PHP, which is the language our tests are written in. This W3Schools XPath tutorial" is one of the better ones.
Once you have a basic grasp of XPath locators, this article is an excellent resource. It explains the difference between good and bad XPaths, which is helpful because there are many possible XPaths for a given element.
XPath's Downside
In addition to being somewhat difficult to create, XPath locators are prone to breaking when a web page's code changes. After struggling with a few, you'll appreciate the practice of creating a permanent, descriptive id
for every element you might want to interact with .
Still, there are situations such as the MODX Manager code where you are forced to use XPath locators. Try to create XPath locators that are unlikely to break. If any near (or not so near) ancestor of the element you want locator for has a unique Id, use that Id at the beginning of your XPath locator.
Coming Up
In the next article, we'll create a test that involves some more complex operations in the MODX manager, many of which will involve XPath locators.
For more information on how to use MODX to create a web site, see my web site Bob's Guides, or better yet, buy my book: MODX: The Official Guide.
Looking for high-quality, MODX-friendly hosting? As of May 2016, Bob's Guides is hosted at A2 hosting. (More information in the box below.)
Comments (0)
Please login to comment.