CSS Selector Strategies for Automated Browser Testing

"Change breaks the brittle." — Jan Houtema

I love this quote, though I'm not quite sure if "Jan Houtema" is a real person. It may be a Paul Graham pseudonym... But in any case, yes, change breaks brittle things and one of the challenges of automated testing is to mitigate that effect as much as possible while you design tests for an ever-changing application. This is especially true with end-to-end testing that's being done in a web browser because you're interacting with your application through its GUI and touching various layers.

I talk more about the "maintenance tax" associated with automated testing in a previous post titled "Where Does End-to-End Testing Fit into a Comprehensive Testing Approach?" In this post I'm going to cover one way that maintenance tax can be reduced — through the use of durable, well-designed CSS selectors to target elements in our web application.

I don't intent to simply explain CSS syntax here. I'm also going to focus on when and where each of these different CSS options make sense within your tests. Mozilla's Developer Network has thorough documentation on CSS selectors that I highly recommend bookmarking. For this post, I'm going to start off with a brief overview of what CSS selectors are in my own words.

What are CSS selectors and what are they used for?

A CSS selector is a string designed to target one or more elements on a web page by specifying attributes and/or hierarchy (parents in the DOM) of the element(s). In other words, it's a way to locate one or more elements on the page. Locating elements is necssary for a number reasons which include styling them with CSS, accessing them with JavaScript — or in our case, interacting with them in an automated browser test. Every element that you interact with in an automated test (whether it's clicking, typing, asserting, etc.) needs to be uniquely identified so that it can be located during the test run.

There's no magical method for easily pinning down an element in every situation. You're essentially building a technical "description" so that the element, or elements, can be pinpointed. In some cases, the element may have a unique attribute (like an ID) that makes this easy. In other cases, the attributes are sparse or broad and need to be chained together. Occasionally hierarchy needs to be used as well.

Think of it like this... You're looking at a crowd full of 1,000 people. A few of these people are wearing a sticker with an ID number on it. A few others are wearing a name tag. Most don't have either. You're asked to verbally identify some of the people in this crowd. How do you do that? For the folks with an ID or name tag, it's straightforward. But for those without? You need to build a list of attributes, and possibly surroundings, to identify them: "The tall, male in the blue suit next to the woman in the red dress." In this analogy, the crowd full of 1,000 people is the DOM of a web page (full of 1,000 elements) and the verbal description is your CSS selector for targeting them. It's a little bit like this old classic:

Guess Who?

What specific methods does CSS provide for targeting?

CSS provides a syntax for designing these selector strings by specifying attributes and hierarchy of elements so that you can match exactly the element(s) you intend. I will walk through some of the more common options, explain the syntax, and offer thoughts on when they're useful.

Targeting using IDs

The ID attribute is, in most cases, the most ideal way to match an element when it is available. It's defined like this:

<input id="email" value="">

It can be matched using this syntax in your selector:

#email

I'm just putting a # in front of whatever the id attribute is set to. Super easy.

In theory, IDs are unique on the page and remain static from page load to page load. This means that there should be only one element on the page with its id set to email. It also means that if I refresh the browser, that same element will still have an id set to email. When that's the case (and it usually is), then an ID is the perfect selector to easily and clearly pinpoint an element. You should pretty much always use it when it's available.

Barcode being scanned

But, of course, there are exceptions... If you or your engineers are being naughty (tsk tsk), you may end up with more than one element on the page with the same ID. This is generally "wrong", but I've seen it happen. Sometimes multiple responsive views are duplicated in the DOM which are hidden or shown depending on screen size. Other times, I've seen multiple login forms duplicated in the DOM which share the same IDs on their form inputs. Again, these scenarios "shouldn't happen" but they do, so just be aware.

Another situation to be aware of is "dynamic" IDs, meaning IDs that change each time the page is loaded. That makes them useless for targeting since they're different every time. From an engineering perspective, this isn't an ideal use of IDs in my opinion, but I see it quite a bit. Things like this:

<input id="email-28742" value="">

That -28742 portion is a pseudo-random number or string that changes each time the page loads... If I specify my selector as #email-28742 and run a test, the ID may be something like email-32472 when the page loads so my selector won't find anything. Womp womp.

EmberJS is a notorious culprit here. Their framework seems to leverage dynamic IDs a lot... I'm sure they have a good reason for it... right? right?! Fortunately, you can work around this problem. In the case of our product, Ghost Inspector's recorder has some logic and options built-in to avoid dynamic IDs. You can also use the "partial matching" attribute selectors that I talk about below, or just avoid the ID entirely and locate the element using other methods.

Targeting using attributes

An attribute is essentially any property of an element. Those familiar with CSS tend to think of things like the name of an input element. For example, consider this element:

<input name="email" value="">

Attributes can be specified in your selector by taking the attribute assignment and dropping it into square brackets, like so:

[name="email"]

That selector will look for any element on the page with a name attribute that's set to email. This can be great way to target form elements in your test since name attributes are often (but not always) unique on the page.

Name Tag

Attribute selectors can be used for far more than names, however. In fact, the ID identifiers that we covered earlier are attributes themselves. The syntax in the section above is essentially shorthand. We could also match an ID using this selector [id="identifier-here"]. It's the same as #identifier-here.

Apart from the name attribute that I've mentioned, other useful attributes include input type and value. For instance, to match a submit button like <input type="submit" value="Send"> or a checkbox like <input type="checkbox" value="true">, you could use [type="submit"] and [type="checkbox"], respectively (though additional identifiers may also be necessary).

The value attribute can also be useful. For instance, assume we have this in the DOM:

<input type="radio" name="point" value="1">
<input type="radio" name="point" value="2">
<input type="radio" name="point" value="3">

Suppose we want to click the radio input the value of 2. This selector might work nicely:

[name="point"][value="2"]

Hey, look! I've got 2 sets of brackets in there! I'm matching by both name and value. Yes, you can do that. You can combine as many attributes as you want — and, you don't need to stick to just attribute selectors either. As we explore more options and syntaxes below, you'll see that we can do things like input[name="something"].some-class.another-class which combines element tag, attributes and classes. For now though, let's continue talking about attributes.

In some cases, I've also found it useful to target elements using their placeholder attribute, which specifies the label that appears inside of an input element when it's empty. For an element like <input name="email" placeholder="Email Address">, you might consider using the selector [placeholder="Email Address"], which is nice and semantic. I've seen situations where using data attributes, as in [data-something="xyz"], can be useful as well. You can get a little creative here!

So, attribute selectors are very useful when working with form elements — but they don't stop there. They can also be used for other things, like matching the href property of anchor tags (links) on the page. A link like <a href="/docs/">Docs</a> could be matched with [href="/blog/"]. You can use attribute selectors to match any property on any type of element.

Ok, great. On to the next thing? Oooh no.

Vizzini: Just wait till I get going!

One of coolest features that attribute selectors provide (and the most underutilized, in my opinion) is the different operators that are available. The operator is that = in [name="email"] which looks for a name attribute that is equal to exactly email. CSS provides a number of other operators that we can use to do more advanced things, like just matching a portion of the attribute.

The 3 that I use the most are ^= (match at the beginning of the attribute), $= (match at the end of the attribute), and *= (match anywhere in the attribute). Let's look at how this works:

<a href="/user/12345/edit/">Edit User</a>

I want to target that link with a selector. The problem is, the 12345 portion is different every time because it's a new user ID that I'm creating during the automated test itself. This means using a[href="/user/12345/edit/"] won't work. How do I get around it? I match the beginning and end of the link URL, like this:

a[href^="/user/"][href$="/edit/"]

See what I did there? We've "weeded out" the dynamic part. Now I'm just looking for /user/ at the front of the URL using ^= and /edit/ at the end using $=. This is very, very powerful especially in automated testing! Let's look at another example:

<a href="https://staging.company.com/dashboard/?session=3284792">Dashboard</a>

Now we've got a URL with a domain in front and a dynamic session ID at the end. Obviously I can't have that session ID in my selector, since it changes. But, to add another twist, suppose I'm building an automated test that I want to run on multiple environments — not just staging.company.com but also www.company.com and others. I want to take that domain out of the selector as well. I can do this:

a[href*="/dashboard/"]
This time I'm using *= to just look for /dashboard/ anywhere in the URL.

Attribute selectors can also be used deal with the dynamic ID problem that I mentioned above. The example element I used <input id="email-28742" value=""> could be matched with [id^="email"] to ignore the dynamic portion.

This is about as close as CSS gets to "regex" type matching so make a note of this functionality and use it wisely. It's very useful for making your anchor selectors more durable, among other things.

Targeting using class names

Elements can have a class attribute that contains a space-separated list of defined "classifications" that apply to the element. This is most often used for styling elements in the stylesheet — but in some cases, classes can be useful for targeting elements in our browser tests. The class attribute is used like this:

<div class="some-class another-class">...</div>

To match that element using a class name, we use this syntax in our selector: .some-class. We could also require both classes for the match, combining them like this: .some-class.another-class. As with IDs, classes are just another type of attribute, so the .class-name syntax is essentially shorthand for this: [class*="class-name"]

We need to be picky about using classes in selectors because, in most cases, they're designed to apply to groups of elements — not just one. Since we're generally interested in matching a specific element with our selector, adding broad class names which apply to lots of elements (for example, Bootstrap's .form-control class which gets applied to all form inputs) isn't very helpful. It just clutters up the selector without making it any more specific.

You'll also want to avoid classes that look temporary or are very style-oriented. For instance, a "grid" class like .col-md-1 could easily change to .col-md-2, so it's probably not a great idea to use it in your selector. Some JavaScript frameworks will clutter up classes with dynamic stuff as well. For instance, AngularJS adds a ton of .ng-whatever type class names. I'd recommend avoiding those in your selectors as well.

With that said, some classes can be useful especially when they provide a good amount of specificity or semantic value. If you're interacting with a form on a page that has a number of buttons, but just one with a .btn-submit class, that might make a good selector. If you're interacting with a large form that has multiple sections with different class names, using something like .intro-section within your selector might help with specificity (see the hierarchy section below).

Targeting using element tags

By element tag, I mean input for an input field, a for an anchor, h1 for a header and so on. These can be placed at the front of your selector, like so a[href="/docs/"]. It's rare to use just the element tag as the selector, but it could make sense in some situations. For instance, suppose your page has a single <h1>...</h1> element and you want to assert the contents. If you don't expect any more <h1> elements to be added, then sure, use h1 as the selector.

You may also have some kind of scenario like in this (contrived) example:

<div class="username">Justin</div>
<span class="username">Justin</span>

In this case, using .username will match both elements, since both have that class. You could use div.username or span.username to pin it down to the one you want to target.

Ok, so beyond those simple scenarios, when is this useful?

"Any fool can write code that a computer can understand. Good programmers write code that humans can understand." — Martin Fowler

In most cases, the usefulness of adding this has less to do with the targeting process and more to do with conveying clarity to anyone looking at your selectors. For example, consider this selector: [name="level"]. Now, if there's only one element with the name level on the page then this selector is perfectly adequate for targeting purposes. But, if I'm a new engineer looking at that selector, I don't know whether it's targeting an <input> field, a <select> box, a <textarea>, or something else entirely... Personally, I like the idea of seeing select[name="level"], not because it necessarily helps with targeting, but because it tells me something useful about the element (and probably the action being performed).

You could make a case that this makes the selector slightly more fragile... Perhaps an <input> field that you're targeting gets changed to a <textarea> in an application update, so the selector breaks because you've used input[name="address"] and now it needs to be updated to textarea[name="address"]. That's a fair argument... so I'd say to use your own judgment and preferences here. I like to include the tag when I feel like it conveys something useful to people working with the test. In my case, that's typically <input>, <textarea>, <select>, <button>, <a> and occasionally a few others depending on the situation, like <li>.

Targeting using hierarchy

Hierarchy in your CSS selectors should be used with caution because it immediately increases the complexity. With that said, using hierarchy is necessary in many situations — and can actually improve clarity in a few.

Hierarchy

Here's a simple DOM illustrating hierarchy where we've got <label> elements that are nested inside of (children of) <div> elements:

<div class="name">
    <label>Justin</label>
</div>
<div class="email">
    <label>test@email.com</label>
</div>

Let's assume we want to target the <label> that contains "Justin". How do we do that? We can't use just label as our selector because that will match both <label> elements. We need to narrow down the parent element first. That selector would look like this:

.name label

I match the parent element using its .name class. I add a space afterwards. This indicates that I'm going to target an element contained within the element matched by the first string. After the space, I match the inner element. In this case, I'm just using the label tag name. This selector essentially says, "Find the <label> element somewhere inside of an element with a .name class."

I can define as many levels of hierarchy as I'd like in a CSS selector using multiple spaces. In a large DOM, I may end up with something like this:

#supportForm .comments textarea

In that selector I'm looking for a <textarea> element, which is inside of an element with a .comments class, which is then inside of an element with an ID set to #supportForm. Of course, if the <textarea> element had a unique id or name attribute, I could make this much simpler... but that's not always going to be the case.

In addition to using a space between child elements, I can also use an angle bracket like this: .comments > textarea. When I use the angle bracket, it means that the element is a "direct child" of the parent element. In other words, it's in precisely the next "level" of nested elements — not just anywhere inside. Let's illustrate this with another (contrived) DOM:

<div class="comments">
    <textarea></textarea>
</div>
<div class="comments">
    <div class="something">
        <textarea></textarea>
    </div>
</div>

If I use the selector .comments textarea, it will match both <textarea> elements. However, if I use .comments > textarea, it will only match only the first, since the second block has a <div> nested in between the two which breaks the "direct child" relationship.

The angle bracket can be useful for precision, but can also make selectors more and more brittle because you're effectively taking away the flexibility of the selector. Adding an extra <div> layer (as in the example above) is a common occurrence, so you're more prone to breakage when application updates occur. When hierarchy is a must, I'd avoid using the angle bracket unless you really need to (which shouldn't be often). Sticking to spacing and broader relationships will help to keep your selectors more durable as your application's DOM changes.

Targeting using ordering

In some case (most often in lists) it can be useful to target elements based on their position in a sequence. Here's a simple DOM illustration:

<ul class="fruits">
    <li>Apple</li>
    <li>Orange</li>
    <li>Pear</li>
</ul>

I want to target the "Orange" entry. How do I do that? The <li> elments don't have any distinguishing attributes... Fortunately, CSS provides an :nth-of-type() feature to target elements by their position in a list. For this example, my selector would look like this:

.fruits li:nth-of-type(2)

I simply plug 2 into the :nth-of-type() function since I'm looking for the second <li> element in the list. Simple, right? CSS also provides a couple of other similar features that can be used. They're called pseudo-classes. These are handy for their respective use cases. Just be careful about using these features in situations that they're not meant for, or where the ordering of your elements may be changing frequently. Note that if the order of the items above changes, then my selector may no longer be targeting the "Orange" entry.

Targeting using text contents

If you look at the DOM example in the section above, wouldn't it be nice and semantic if we could target that <li> element using its "Orange" text? That would be nice, wouldn't it?... I wish I could tell you that CSS has a tool for that, but unfortunately, standard CSS does not. You may have seen something like this before:

li:contains("Orange")

In principle, I love that selector. It's clear to humans and it won't break if the order of the list elements changes — semantic and durable. Unfortunately, the :contains() pseudo-class is something that's only available in the Sizzle selector engine (popularized by jQuery). It's not available in your browser by default right now. Very sad, I know... Early versions of Selenium injected the Sizzle engine for you, so you could use this feature in your tests. However, with Selenium v2 and up that is no longer done. At Ghost Inspector, we've built this into our system — but it's only available in some of our browser options, not all.

The best solution I can offer right now is to consider mixing in XPath selectors which support matching by text contents. XPath is a method for element selection that has a different syntax than CSS, but is used in the same way. It deserves its own blog post in the future. In the meantime, the XPath selector for the example above would look like this:

//li[contains(text(), "Orange")]

XPath is supported by both Selenium and by Ghost Inspector, so this is a good option when matching an element by its text contents makes sense — which is fairly often.

How do I know when to use the different methods?

Welp, that's a tough one. The most obvious answer is "experience". As you work with HTML, CSS and the DOM more often (and in different situations), you'll start to get a handle on what might work for different scenarios. But experience takes time, so let me offer some other advice.

I've designed this post to (sort of) walk through selector options from most ideal (simple and durable) to least. There are exceptions to this logic, but I usually take this approach when building a selector:

  1. Is there an id attribute? Use that.
  2. Is there a name attribute? Use that.
  3. Is there some other unique attribute that I can use, like placeholder? Use that.
  4. Can I use attribute operators to match a portion and uniquely identify this element, like a link's href? Use that.
  5. At this point I'll start looking at class names to see if anything looks usable.
  6. If there's nothing specific enough, I'll take what I have so far (which may be a useful attribute or class that just isn't specific enough or may be just the element tag) and I'll start stepping out and looking at the hierarchy outside of the element. The same logic applies when targeting parent elements, so this logic is recursive. Just keep in mind that you don't need to use the "direct" parent. You could jump back a few levels to a parent element that has an id attribute or makes more sense semantically.

Another hugely helpful asset in deciding how to build these CSS selectors for your tests is your own knowledge of the application that you're testing. What do you know about the way the DOM is designed AND what do you know about the way it may change in the future? Are portions of the UI more active than others? What's likely to change? Are class names changed frequently? How does the engineering team think about the DOM? Ask them! Try to get a handle on how they go about changing the DOM and factor that into your own considerations.

Should I modify my application to make targeting easier in my tests?

This is a tricky question that I get asked quite a bit. Believe it or not, you can always match an element on the page uniquely with a CSS selector. However, that selector may end up being super ugly and brittle with no better alternatives available.

If you find yourself in a situation where there's no "good" selector available to locate an element, is it acceptable to have the engineering team add an id or some other attribute that you can use? This can be really handy. Obviously a unique id makes for an easy selector. I've also heard folks suggest the use of [data-testing="id-here"] or [qa="id-here"] type attributes specifically for targeting during testing.

I have mixed feelings about this approach. It can make your life much easier, but also comes with trade-offs... If you decide to go this route, make sure everyone understands those tradeoffs. Here are the two downsides that I see:

  1. By adding attributes to your application's markup which exist for the sole purpose of testing, in some ways, you're adding another level of "maintenance" outside of your test cases themselves.
  2. Adding attributes to your application's markup increases its size. Unless you're doing something really slick, this extra code is probably making it to production which means that you're technically slowing down your live application.

What tools can I use to help me generate CSS selectors?

If you're a Ghost Inspector user, we've got a Chrome recording extension that will generate CSS selectors for you as you record a test. However, those selectors are generated algorithmically and are meant to be reviewed afterwards. The recorder doesn't know anything about your application or what's dynamic, so they're often going to be a bit more rigid than necessary. In some dynamic cases, they simply won't work without modification. Chrome too has a built-in option for generating a CSS selector by clicking on the element in using its developer tools:

Chrome selector generation

But again, without any human knowledge, these selectors can sometimes be a bit ugly. There's really no substitute for your own skills and knowledge here. The one tool that's probably most useful is simply your browser's developer console, which allows you to query the DOM for your selectors using JavaScript, like this:

document.querySelectorAll(".my-selector");
Chrome Console: document.querySelectorAll

Get familiar with that line of code! It's incredibly useful for figuring out exactly what your CSS selector will match on the page, both when building selectors and when troubleshooting them. You can also use document.querySelector() to match a single element, but I recommend using document.querySelectorAll() during your development process to help ensure that you're only matching the element you expect, and not others (which will happen often). Sławomir Radzymiński has a blog post on finding and testing CSS selectors that outlines this process in a little more detail.

Tips for troubleshooting

A couple of quick tips if you're running into problems with a CSS selector:

Justin Klemm

Author: Justin Klemm

Justin is the founder and tech lead at Ghost Inspector. He's a seasoned developer with a passion for innovation. When he's not tinkering with the latest web frameworks, Justin enjoys world traveling, good eats and lots of outdoor activity.

Welcome to Our Blog!

Ghost Inspector is an automated browser testing and monitoring service. Here you'll find testing and QA related blog posts written by our team members. Subscribe to stay up to date with our latest posts!

Popular Blog Posts

See All Blog Posts