As test automation engineers, we strive to use stable and self-documenting selectors whenever possible, but sometimes using an XPath-like selector is unavoidable. The :nth-child() CSS pseudo-class, which selects an element based on its position amongst its siblings, is often used within these longer selectors. But did you know there’s an alternative to :nth-child() which is a better choice in almost every circumstance?
Choosing good locators is an important aspect of creating stable automated tests, reducing false-positive error rates by ensuring the correct element continues to be selected even for pages under heavy development. There are well-documented locator strategies that you can follow when crafting these locators, but even if you’re following best-practices you may find yourself in situations where an optimal locator cannot be generated:
- Your web application uses a CSS-in-JS library: Libraries such as Styled Components and EmotionJS allow developers to define CSS styles alongside their web components instead of in separate CSS files. Defining the styling, template, and logic all inside a single JS file makes each component self-contained which helps maintainability and readability, but it comes at cost; the classes that are output by these libraries are auto-generated and lack any semantic meaning. This means that you’re going to have a bad time if you rely on these classes in your test automation. Worse yet, they have the potential to change with every new build.
- You’re not using data attributes as stable locators: Depending on your situation, adding attributes in your application-under-test that are specifically for use by your test automation scripts may be the best way to achieve stable locators. While this offers the greatest flexibility in terms of generating locators, many teams don’t have commit-access to the application-under-test, and asking developers to do this may not be an option.
- You want to target the ‘nth’ element in a list of elements: Even with great locators or the ability to add automation-specific data attributes, you may want to simply target a specific element among a list of elements. In cases like this it often doesn’t make sense to add a data-attribute since the attribute value may simply be an index value rather than a self-describing value like a product name or username.
In these and other situations, you’ll often find yourself using :nth-child() as part of your locator.
:nth-child() is a CSS pseudo-class that’s supported in all major browsers and in legacy browsers including IE9. MDN defines its behavior as “match[ing] elements based on their position in a group of siblings.” In addition to its original intended use in CSS stylesheets, it’s ubiquitous in test automation scripts the world over. In fact, if you’ll often see it in selectors generated using Chrome Dev Tools’ Copy selector command.
A classic example of using :nth-child() is selecting the nth item in a list. In situations like this, you don’t want to use a locator that targets on a text value, since the item associated with that text value may change positions or may not be present at all in the future. :nth-child() is a succinct way of expressing the intention to choose not based on an element’s value, but based on its position.
But problems can arise with this approach that are not at all obvious due to how this pseudo-class works.
Let’s assume you want to target the second element in the following list:
The selector that you’d typically write is
#list > .item:nth-child(2). If you’re using Chrome Dev Tools’ Copy selector feature to generate the selector for you, then it’s going to output the inferior
#list > div:nth-child(2) selector that targets against the child’s tag name instead of class name. But let’s suppose you wanted to choose the 2nd element even if the class or tag name changes, maybe to avoid the case where a developer changes the tag name and causes your test to break. You instead use the selector
#list > *:nth-child(2) which will select the 2nd element regardless of tag or class.
In this situation, neither selector used above will return the expected result!
#list > .item:nth-child(2) will return null because the nth child to #list is not a div!
#list > *:nth-child(2) doesn’t fare much better, returning the script element.
This is a contrived example, but you would be surprised how often I’ve seen this in the wild. The automated testing tool I co-created has a feature that auto-generates selectors, and for this reason it will never generate a selector containing :nth-child, but instead uses the :nth-of-type pseudo-class.
:nth-of-type(), like :nth-child(), is supported by all major browsers and has subtlely different behavior in that it “matches elements of a given type, based on their position among a group of siblings”. This behavior has the distinct advantage of being closer to representing how elements actually appear within the browser. Since :nth-of-type() allows you to target on the position of a given tag, you’ll never hit a situation where a non-visible tag like script causes your tests to fail, since it ignores any elements not matching that tag in the test. In the examples above, the selector
#list > .item:nth-of-type(2) matches the Item 2 element in the original example as well as the example where a script tag is the second child. This is because it ignores any elements that don’t include the item class.
In conclusion, by coupling :nth-of-type() with a stable sub-selector like .item, you get the benefit of a built-in way to target based on position (avoiding creating unnecessary data-attributes) while avoiding the potential pitfalls of :nth-child(). Consider migrating your tests over to use :nth-of-type()!
That’s all about :nth-of-type() in CSS Selector.