Locator

This Gem aims to extract common html element selection from testing tools such as Webrat, Capybara or Steam. At its core it constructs XPath objects using a simple boolean expression engine in order to locate elements in a Nokogiri DOM. It provides a bunch of Element classes that are targeted at implementing a Webrat-style DSL for convenience.

See below for why I strongly believe that this behavior should be implemented as a stand-alone library.

Usage

Of course you can use the underlying implementation, too, but there are three main public methods which are supposed to give you access to all you need:

  • Locator.locate(html, *args)
  • Locator.within(*args)
  • Locator.xpath(*args)

The following code examples all assume that you include the Locator module like so:

include Locator

All the examples should work without doing so though. You should be able to statically call the same methods on the Locator module instead.

Locating an element

There are three APIs for locating elements:

  • using a locator type (e.g. :link) and/or required node attributes (e.g. :id => 'foo') and/or a selector (e.g. "The link")
  • using an xpath
  • using a css selector

In all cases you can use the Locator.locate method which wants you to pass the HTML code as the first argument:

locate(html, *args)

Locating elements using a locator type

If you pass a Symbol as a first argument, then Locator will interpret this as a locator type. For a list of supported locator types see below, these sometimes but do not always equal HTML tag node names.

E.g. the :form tag will simply locate HTML form tags. Locator types like :button, :field and :link bake in some more knowledge about how we want to build a DSL (e.g. a helper locator_button could use the Button locator type and match both HTML button tags and clickable input tags such as submit input tags).

So this will simply return the first link tag on the page:

locate(html, :link)

Locating elements using a selector

You can (alternatively or additionally) specify a selector. Selectors will be matched against matchable values depending on the locator type, see below for details.

E.g. to find a link that has the text "Click here!" you could use any of the following calls:

locate(html, :link, "Click here!")
locate(html, :link, "Click")
locate(html, :link, "here!")
locate(html, "Click")

And so on. Note that Locator will pick the outermost element with the shortest matching value, see below for details about matchable values.

Locating elements using attributes

You can (alternatively or additionally) specify required attributes. E.g. to find a link with the id "foo" you can use:

locate(html, :link, :id => "foo")

You can combine that with a selector. E.g. this will locate a link that has a text “click” and a class “foo”:

locate(html, :link, "click", :class => "foo")

All attributes are matched by equality. I.e. when you pass :id => "foo" then only elements are matched that have the exact id attribute “foo”. The only exception from this is the :class attribute. When you pass :class => "foo" then this is semantically equivalent to saying “an element that has the class ‘foo’”.

(Technically the class attribute is matched using an xpath *[contains(concat(' ', @class, ' '), concat(' ', "foo", ' '))], i.e. leading and tailing spaces are added to the element’s class attribute value and the requested value, and then it checks that the requested value is contained in the actual class attribute value.)

Locating elements using an XPath or CSS selector

Instead using the API described above you can also specify an XPath or CSS selector like so:

locate(html, :xpath => "//div[@id='foo']")
locate(html, :xpath => "div#foo")

These both would lookup the same div element with the id “foo”.

Scoping to an element

You can scope the element lookup to certain parts of your HTML Dom. E.g. this is useful when you have a couple of forms with the same input elements and you want to fill in a particular one.

You can use both the Locator.locate and Locator.within methods for this:

within(:form, "login_form") { locate(html, :field, "password") }

Or:

locate(html, :form, "login_form") { locate(html, :field, "password") }

Appendix

Supported locator types

All types will match the id attribute, all form elements (input, textarea etc.) additionally match the name attribute. Some locators additionally match the value attribute and/or the node content (inner_text). Also see below for matchable values.

All form element locator types (like :field, :checkbox, :file etc.) can additionally be located through their label tag. E.g. when you have a label tag “Name” that points to a text input tag then you can locate the text input using the text selector “Name”. E.g.:

locate(html, :field, "Name")

Here’s a list of supported locator types and any additional matchable values:

Type Locates Extra matchable
:button a button element or a submit, button or image input tag content (for buttons), value (for inputs)
:check_box a checkbox input tag
:field an input element (see below) or a textarea
:file a file input tag
:form a form tag
:hidden_field a hidden input tag
:input an input tag with a type of text, password , email, url, search, tel or color
:label a label tag content
:link a link (i.e. an :a tag with an href attribute) content
:radio_button a radio input tag
:select a select box
:select_option a select option tag value, content
:text_area a textarea tag

Matchable values

When given a selector Locator (as in locate("The link")) will match it against different node values depending on the locator type. Node attributes like id, name and value need to equal the given selector. The node content needs to include the selector text (case sensitive).

Locator will then pick the outermost element with the shortest matching value.

Matchable Looks at Match type
content the element’s full inner text contained in content
id the elements id attribute equals
name the elements name attribute equals
value the elements value attribute equals

E.g. locate(html, :link, "link") will find both a link with the text "link" as well as "The link" but it will select and return the first one because it has the shorter matching value (i.e. "link").

Locator regards the inner text of an element as its content. That means that the given HTML structure looks like this:

<div>
  <div>
    Some <span>text</span>
  </div>
</div>

… then locate(:div, "Some text") will locate the outer div because it a) ignores the nested span so that the content (inner text) is "Some text" and b) the contents of both div elements are the same.

The same locator will return the inner div though when given the following HTML structure:

<div>
  <div>
    Some <span>text</span>
  </div>
  here
</div>

… because the content of the outer div now is "Some text here" which is longer than "Some text" and Locator returns the outermost element with the *shortest*@ matching value.

This might seem complicated at first but it is required and quite consistent if you think about it.

One obvious reason for returning the outermost element is that one wants locate and within behave the same way. If within would refer to the innermost element though then the following locator would not find any element in the HTML above which would be highly confusing:

within(:div) { within(:div) { locate(:span) } }

And an obvious reason for returning an element with the shortest matching value is that in the following case one will want locate("The Link") to return the second element, not the first one:

<a href="#">The link with extra text</a>
<a href="http://www.some-very-long-url.com">The link!</a>

Why another library?

In my opinion each of the libraries mentioned above do way to much. Amongst many other things they all implement tools for locating HTML elements from a Dom in some way. When we look at the details of the implementation they all do it differently though. So moving a test suite from Webrat to Capybara or Steam might be easy if only the simplest and most common helpers are used. But it can also be a huge pita because of all the tiny differences in those libraries: the APIs are simply not the same even if they seem to be at the first glimpse.

Thus, having a library that does nothing but locating elements but does this one thing well and can then be used by other solutions that add other features is the way to go.

By now Locator has been integrated to Steam (which already very much streamlined Steam’s API). Obviously I would be humbled if other solutions would pick up Locator, too, and I do offer my help for working on this.

TODO

  • add notes about using Locator::Element classes directly