Building Tests /

Elements and Locators

Writing abstractions to interact with web pages to save time.

Previous: Anatomy of a test Next: Browser automation using elements

Elements on a page are found in your tests (or pageobjects) using one of three methods (with a couple of bonus methods);

There are other by() methods exposed in by Selenium, such as tagName, linkText, and className. They're useful to know about but they don't show up in testing very often.

Your test runner might add a few more methods for element location. For example, Protractor users also get;

All of these locators are available in by() to use with both element() and element.all(). It's also possible to add your own locators by using Protractor's addLocator() method, but that's not covered here yet.

The simplest locator strategy is finding elements in a page by using a CSS selector.

    element(by.css('h1')); //returns first H1 tag on a page 
    element(by.css('div.navbar')); //returns the first div tag with a class of navbar on a page 
    element(by.css('h3:nth-of-type(3)')); //returns the third h3 element on a page 

The test runner passes the CSS selector to the browser where it's used in exactly the same way as if it was styling an element or attaching an event listener. The browser is running the code. There's no need to worry that your test runner won't understand the CSS selector you use; it's doesn't care, it just passes it on.

The next locator is selecting by ID. This is actually simpler than locating elements by CSS, because you're just sending a string to match against elements with the same ID. Behind the scenes the browser is using it's getElementById() method, so whatever you send in your test will match whatever would have been matched if you'd written the same thing in JavaScript.

    element(by.id('userlogin')); //returns first element with an ID of 'userlogin' 

This is probably the first point where it's clear that tests can change your HTML and CSS code if you're not already writing well-formed valid markup. If your page has two or more elements with the same ID then this will only get the first one. Your code should only use IDs once, and they should be unique within a page. Consequently, if you're reusing IDs then your tests will highlight that.

Sometimes you do actually want to query a page and find more than one element. You might have a list of items that you're creating dynamically and need to test that they're appearing in the DOM correctly, or you might have a navigation menu that you don't want to give separate IDs to. In these cases you can't reasonably select an element on it's own, so you need something that can get every element that matches a locator.

To do this we need to use element.all().

    element.all(by.css('div')); //returns all the div tags on a page
    element.all(by.css('a[href=""]')); //returns all the anchor tags with an empty href on a page

As element() methods return promises we can't use the values in the returned array by element.all() straight away.It's necessary to wait until all the promises are completed. Fortunately we can just map() the values, and that will automatically wait until the promises complete.

var elements = element.all(by.css('a')).map(function(el) {
    return el;
});
elements.then(function(result) {
    //do some tests 
});

This will give you an array of elements that you can do tests on. If any of the promises reject() rather than resolve() you'll need to handle that properly.

The final method for locating elements in a page that's exposed by Selenium's by() function is finding things by using their XPath notation. XPath is a bit of a throwback to the days of XML parsers and XSL transformations. It's hard to learn, but if you do then it's quite useful, especially if you're testing things that you can't modify the source code for. One thing to note: if you have any other choice you should use that instead. XPath is slower than all the other locators, and if your markup changes then it'll break.

An XPath selector looks like a string of HTML tags separated by forward slashes, with array-like parameters when there are lists of similar child elements. For example, reading an XPath selector like /html/body/article/section[2]/ul[1]/li[3]/p from right to left shows that it refers to the paragraph child element of the third list item in the first unordered list in the second section of the article of the body of the html tag. That's a bit of a cumbersome sentence, but it is quite clear once you read it a couple of times.

To use that XPath selector in a test or a pageobject you'd need to pass it to the by.xpath() method.

    element(by.xpath('/html/body/article/section[2]/ul[1]/li[3]/p'));

To work out the correct XPath selector for a given element that you want to test by hand is a pain. Fortunately though Chrome's devtools panel gives us a way to cheat. If you inspect the element in question, and then right click on it in the element inspector panel, there's an option for "Copy XPath" in the Copy submenu. Bingo! Free XPath selectors without any of the hassle of working them out. Where this is a massive help is in selectors that need to look back up the DOM tree - you can't do that yet (coming in CSS4) but you can with XPath. This means that finding the parent element of a selector is possible. For strange edge cases in testing that's absolutely brilliant.

The last way to access elements on a page is to write your own custom locator. This sounds scary but it really isn't. It's really just a callback that loops through all the elements of the page and runs a comparator function against each one to see if it matches.

For example, if you regularly use a specific structure of HTML to define a link to a page that contains more information, you could write a locator to find anchors with the text "Read More".

// Add the custom locator. 
by.addLocator('findByAnchorText', function(anchorText, opt_parentElement, opt_rootSelector) {
 
    var using = opt_parentElement || document, anchors = using.querySelectorAll('a');
 
    return Array.prototype.filter.call(anchors, function(anchor) {
        return anchor.textContent === anchorText;
    });
 
});
 
element.all(by.findByAnchorText('Read More'));

There are some things to be wary of with custom locator functions. The first is that they can be awfully slow on large pages. If you're looping through a DOM with thousands of elements, which isn't uncommon in modern web applications, you will find that your tests can take a long time to run.

Previous: Anatomy of a test Next: Browser automation using elements