This is an excerpt from my new book The Web App Testing Guidebook
Last chapter, amid the talk of actions and assertions, we touched on selectors just a little bit. We used a 'partial-text' selector to pick out a button with the text "About Us" in it. Now it's time to take a full look at all the options we have when it comes to picking elements.
Let's get started with the official W3C list of the supported selector types:
- CSS selector
- Link text selector
- Partial link text selector
- Tag name
- XPath selector
Also, I should be calling all of these "Locator Strategies", which is how they're referenced in the W3C specification.
There are actually a few other "unofficial" strategies, but I don't think they're worth mentioning because they're pretty much obsolete these days.
Normally, when using one of these strategies, you need to specify which one you want to use. In many test libraries, you'll see something akin to
element(by.partialButtonText('About Us')). WebdriverIO helps us out here by inferring what type of strategy you want to use, so you don't have to do as much typing.
Selectors by Example
To showcase the different selectors strategies, let's use the following HTML:
<ul class="items" dropdown-menu id="main-menu"> <li class="item"> <a ng-click="showProjects()"> Projects <span class="icon"></span> </a> </li> <li class="item"> <a ng-click="showArchive()"> Archive <span class="icon"></span> </a> </li> </ul>
Note: If you're unfamiliar with HTML syntax structure, I recommend having a watch through this video tutorial teaching HTML basics before moving on.
The simplest type of selector in my mind would be 'tag name'. It selects any matching element with the tag name specified. So if we wanted to choose the
ul element, in WebdriverIO we'd do
$('ul'). If we wanted to get the first
li, we'd do
$('li'). But what if we wanted the second one?
There are actually several ways to achieve this, but the one we'll look at first is to select both
li's, then pick out the second one.
Similar to the
$ function, WebdriverIO also provides a
$$ function, which returns an array of all of the matching elements. Where
$ returns the first matching element,
$$ returns all of them.
If we use
Link text selectors and partial link text selectors are also pretty simple. To get the first link, we could do
$('=Projects'). To get the second
li using a partial link text selector, we can do
$('*=chive'). But if you remember well, we didn't target a link in our previous exercise... we were asking for a
button element instead. What gives?
Well, while Webdriver officially only supports 'link' based text selectors, WebdriverIO took it one step further and provides support for any type of element to be checked. It does this by converting what looks like a text selector into an XPath one (more on XPath in a second). Pretty neat.
CSS selectors have quite a legacy and feature set, and an entire course could be put together covering the various forms they can take. To use a CSS selector in WebdriverIO, just pass it into the
$$ function. Here's one that grabs the
ul by class name:
$('ul.items'), and one that grabs the two
li's by class as well:
If you're new to CSS, I highly recommend running through a few tutorials on selectors. Here are a few recommendations:
- TutsPlus "The 30 CSS Selectors You Must Memorize"
- Sauce Labs "CSS Selector Tips"
- CSS Tricks CSS Selector Almanac
- CSS Selector Strategies for Automated Browser Testing
Getting to know all your options with CSS selectors is quite valuable. The majority of styles out there rely on class or ID based selectors, but it can definitely be an advantage to know about other types as well. Here are couple more to know about:
Attributes are the properties of HTML elements (e.g. the "class" part of
<li class="item">). These attributes can be helpful for singling out specific elements.
Using CSS Attribute Selectors (shorter version), we can target the
ng-click attribute of the link, like so:
Notice how the selector includes both the attribute and its value. You don't have to include both, but for our needs we needed to target the element with that specific
We can also combine attribute selectors to be more specific with our link:
That selector asks for a link with an attribute of
ng-click and an attribute value of
showProjects() that is the child of an element with an attribute of
dropdown-menu (which we didn't include a value for since there was none).
Another method to find your element is to base it on the position in the HTML. The
first-child selector allows us to do this:
That will select any list item (
<li>) that is the first child of its parent. That's a pretty broad selector though, so we should narrow this down to our dropdown using the attribute selector from before:
What if we wanted to get the second child? We can use The
nth-child takes a numeric value, which corresponds to the position in the HTML (it is not zero-indexed, so counting starts at 1).
nth-child can get pretty complicated and is a very powerful selector. There are also related selectors like
nth-last-child. nthmaster.com and CSS-Tricks go in to more detail on it.
nth-child if you could just get all elements using
$$ and grab the one you want by index? Well, selecting multiple elements is a little slower than grabbing a single one, so using
nth-child can help keep our tests speedy. [TODO actually verify this assumption]
The final type of selector I'll mention is XPath. Traditionally XPath has been the predominant selector strategy for Selenium/Webdriver, so many folks in the testing industry are most familiar with it.
To use in WebdriverIO, you pass it in just like you would a CSS selector. So to grab that
ul by ID:
$('//ul[@id="main-menu"]'). And to get both
li's by class:
As I mentioned with CSS, if you're new to XPath, I recommend running through a few tutorials on its usage. Here are a couple recommendations:
So which one should you use, XPath or CSS selectors? Truthfully, either one works. I end up using both depending on circumstance. CSS selectors are normally more brief than XPath, but sometimes they're not as powerful. Which one you use is really a matter of what you're more comfortable with. Front-end developers are likely more familiar with CSS selectors, so I lean that way, but there are some things CSS can't do, which is when I'll choose XPath.
If you're looking to compare the two, this cheatsheet put together by devhints.io is invaluable.
There are two big tricks that XPath can do that CSS currently can't. The first is to select an element by its text content, which looks something like:
//a[contains(text(),"chive")]. The other is the ability to move "up" from an element. So you can use XPath to select a parent of an element, which isn't possible yet in CSS.
We can combine these two techniques to find the parent container of our 'Archive' link:
//a[contains(text(),"chive")]/ancestor::ul. This technique can be very helpful when you're limited in customizing the HTML of a page and need to rely on what the text says.
Regardless of CSS vs. XPath, you can use the Chrome Developer tools to help evaluate and validate XPath/CSS selectors you're working on. This is a useful tool for building and testing selectors.
Aside from the strategies we've touched on so far, there's one neat trick that I love to take advantage of (and no, doctors won't hate that you know this).
Many times you'll have a container element that you want to use as reference for future searches. With WebdriverIO, you can select your parent container, then search from within that element.
From our example, we can select the link we want using a text selector, then select the
span inside that link using a plain old tag selector:
$('=Projects').$('span'). It wouldn't work to do
$('=Projects span') since WebdriverIO would think you're looking for a link with the text
You can also search for multiple elements from a single parent. To get both
lis from the parent
$('ul').$$('li'). Of course you could just do
$$('ul li') in this case, but I needed to show off the other way so hush.
Custom Data Attributes for Testing
If you do have the ability to customize the HTML, I highly recommend adding custom HTML attributes to help identify the elements you need to select. This is a popular practice in the industry and can really help provide the specificity you need.
The way it works is you add a custom attribute to your HTML components which is used solely for testing selectors. HTML5 introduced a formal "data" attribute type that we can take advantage of in our tests.
For example, let's edit that HTML I showed earlier to use data attributes:
<ul class="items" dropdown-menu id="main-menu" data-qa-id="main-menu"> <li class="item" data-qa-id="main-menu-item"> <a ng-click="showProjects()" data-qa-id="main-menu-item-link"> Projects <span class="icon" data-qa-id="main-menu-item-icon"></span> </a> </li> <li class="item" data-qa-id="main-menu-item"> <a ng-click="showArchive()" data-qa-id="main-menu-item-link"> Archive <span class="icon" data-qa-id="main-menu-item-icon"></span> </a> </li> </ul>
In that example, I added a
data-qa-id attribute to all of the elements. It wasn't necessary for some (for example, the
id of the main menu is the same as the
data-qa-id), but useful overall. When targeting an element, we'll now rely on our data attribute:
qa-id is much less likely to be changed without warning.
We're going to take full advantage of this during our test writing, as the HTML currently avaiable on our test site doesn't quite allow us the specificity we'll need for long-term stability.
Avoiding Poorly Built Selectors
We've covered a fair amount of strategies for selectors, and for good reason. Poorly choosen selectors are a prime candidate for flakey tests.
Many times I see selectors that look like:
$('//*[@id="stream-item-1130810481587957760"]/div/div/div/a/span/strong'). The problem with this type of specificity is that almost any change to the HTML will break your tests. An extra HTML element thrown in or one removed will cause chaos in your code.
Instead, adding a custom data attribute as mentioned above can really limit the effect of an HTML retructuring. Even if an element is completely restructured, so long as the data-id stays in tact it should still work.
When it comes to selecting elements, I like to follow this general guidelines:
- If I can change the HTML, add a custom data attribute for testing purposes only
- If I don't have control of the HTML, come up with selectors that are specific, but not overly tied to the HTML structure. For example, using simple parent-child relationships,
:nth-childselectors or text based ones. It's not perfect, but it gets the job done.