3. Locating an Element - Relationships
3.1 Based on its ancestors
Any element that contains the target element is considered an “ancestor”. Ancestor elements can be used to reduce the scope of where Watir searches for an element. The element type method will only look for the element within the html of the calling object.
- When the calling object is the browser, the entire page will be searched.
- When the calling object is an element, only the html of that element will be searched.
As an example, say a page lists a project’s team members by their role.
<span>Project Managers:</span>
<ul id="project_managers">
<li>Alicia Mcbride</li>
<li>Jake Woods</li>
</ul>
<span>Developers:</span>
<ul id="developers">
<li>Jack Greer</li>
<li>Cora Williamson</li>
<li>Albert Gross</li>
</ul>
<span>Testers:</span>
<ul id="testers">
<li>Owen Francis</li>
<li>Luis Leonard</li>
</ul>
Consider a test that needs to retrieve the name of one of the testers. It is not possible to locate the tester li elements directly as there are no properties to differentiate them from the other roles. However, the ul element does have an identifying attribute - the id specifies the role being listed. In other words, the test needs to get the first li element within the ul element with id of “testers”. This is done by chaining element type methods that identify the unique ancestors.
browser.ul(:id => 'testers').li.text
#=> "Owen Francis"
In this code, Watir will look for an ul element with id “tester” anywhere within the browser. Then, only within the ul element found, Watir will look for the first li element.
3.2 Based on its decendants
There are several approaches that can be used to locate an element based on its decendants, which are the elements within the target element.
For example, consider locating the li elements in the following html. The li elements do not have any distinguishing attributes. However, they do have child link elements with unique attributes - the data-componentcode.
<ul>
<li id="ctl00_EnabledComponentListItem">
<a id="ctl00_Component" data-componentcode="ItemA" href="#">Item A</a>
<a href="#">X</a>
</li>
<li id="ctl01_EnabledComponentListItem">
<a id="ctl01_Component" data-componentcode="ItemB" href="X">Item B</a>
<a href="#">X</a>
</li>
<li id="ctl02_EnabledComponentListItem">
<a id="ctl02_Component" data-componentcode="ItemC" href="#">Item C</a>
<a href="#">X</a>
</li>
</ul>
Using find
One approach is to iterate through the li elements until one is found with specified link.
browser.lis(:id, /EnabledComponentListItem/).find do |li|
li.a(:data_componentcode, 'ItemC').exists?
end
Navigate from descendant to parent
If the html structure is stable, you can locate the descendant link (using standard locators) and then traverse the DOM up to the parent (ie li element).
browser.a(:data_componentcode, 'ItemC').parent
Using xpath
Using xpath, it is possible to get the element directly.
browser.li(:xpath, "//li[.//a[@data-componentcode='ItemC'']]")
3.3 Based on its siblings
Locating an element by its siblings is often seen when working with tables. For example, in the below table, you might need to find the colour of a specific creature. A user knows which colour belongs to the creature because they are in the same row. In terms of the html, the colour td element is a sibling of the creature td element since they share the same parent element (tr element).
Table:
| Creature | Colour |
|---|---|
| Elf | Blue |
| Dragon | Green |
| Imp | Black |
HTML:
<table>
<tbody>
<tr>
<th>Creature</th>
<th>Colour</th>
</tr>
<tr>
<td>Elf</td>
<td>Blue</td>
</tr>
<tr>
<td>Dragon</td>
<td>Green</td>
</tr>
<tr>
<td>Imp</td>
<td>Black</td>
</tr>
</tbody>
</table>
Sibling to parent to element
To find an element based on its sibling, the general strategy is:
- Locate the unique sibling element.
- Get the parent element using the parent method.
- Locate the required element within the scope of the parent.
Applying this strategy to the table, the colour of the dragon can be obtained by:
#Get the unique element
unique_element = browser.table.td(:text => 'Dragon')
#Get the parent element
parent_element = unique_element.parent
#Get the actual element
parent_element.td(:index => 1).text
#=> "Green"
Parent by descendent to element
When the unique element is a cousin (ie a descendent of the sibling element), it is easier to locate the parent based on its descendents. The reason being that it is less fragile - ie ensuring that the correct number of “parent” methods are called becomes more difficult.
parent_row = browser.table.trs.find do |tr|
tr.td(:text => 'Dragon').present?
end
parent_row.td(:index => 1).text
#=> "Green"
3.4 DOM Traversal
Most often elements are located by traversing down the DOM. However, there are also methods for when travelling up or horizontally is required.
Parent
The DOM can be traversed upwards to an element’s parent.
In the html:
<div class="parent">
<span class="child">Target</span>
</div>
The parent element of the span tag can be retrieved by using the parent method:
child = browser.span(:class => 'child')
parent = child.parent
parent.tag_name
#=> "div"
Previous/next sibling
The DOM can also be traversed horizontally between sibling elements. For example, the following page has 4 sibling elements in the body - 2 h1 and 2 ul elements.
<html>
<body>
<h1>Heading 1</h1>
<ul>
<li>Item 1a</li>
</ul>
<h1>Heading 2</h1>
<ul>
<li>Item 2a</li>
<li>Item 2b</li>
</ul>
</body>
</html>
Watir-Webdriver
Xpath has a following-sibling axis that can be used to find the next sibling of a given Watir element. This example shows that the next sibling of “Heading 2” is the unordered list with 2 items.
h1 = browser.h1(:text => 'Heading 2')
ul = h1.element(:xpath => './following-sibling::*')
puts ul.lis.length
#=> 2
The preceding-sibling axis can find previous sibling of the Watir element. The [1] is added to get the adjacent sibling. Otherwise, you would get the first sibling in the tree, which is the “Heading 1” h1 element.
h1 = browser.h1(:text => 'Heading 2')
ul = h1.element(:xpath => './preceding-sibling::*[1]')
puts ul.lis.length
#=> 1
Note that the element type returned must match what you are looking for. In the above examples, we looked for any element type. You can replace the * and use a specific watir element to look for a specific type. For example, to get the preceding h1:
start_h1 = browser.h1(:text => 'Heading 2')
end_h1 = start_h1.h1(:xpath => './preceding-sibling::h1[1]')
puts end_h1.text
#=> "Heading 1"
Watir-Classic
For some reason, the same xpath solution produces a UnknownObjectException in Watir-Classic. As an alternative, you can use the nextSibling property of the OLE object.
h1 = browser.h1(:text => 'Heading 2')
ul = browser.ul(:ole_object => h1.document.nextSibling)
puts ul.lis.length
#=> 2
Similarly, you can use the previousSibling property to get the preceding element.
h1 = browser.h1(:text => 'Heading 2')
ul = browser.ul(:ole_object => h1.document.previousSibling)
puts ul.lis.length
#=> 1
3.5 Frames and Iframes
In HTML, frames (<frame> and <iframe> elements) allow a page to be embedded in a page. This special behaviour provides some complications in Watir.
When using a browser’s developer tools to inspect elements in a frame, you will see the hosting page (html element), the frame and then the nested page (another html element):
<html>
<head></head>
<body>
<div>content before frame</div>
<iframe src="frame_content.htm" id="frame_content">
#document
<html>
<head></head>
<body>
<div id="in_frame">content in frame</div>
</body>
</html>
</iframe>
<div>content after frame</div>
</body>
</html>
If you take the normal approach to locating elements within the frame, an exception will occur:
browser.div(id: 'in_frame').text
#=> Watir::Exception::UnknownObjectException
From the browser’s perspective, it only sees the hosting page’s source. In other words, the frame contents look empty:
<html>
<head></head>
<body>
<div>content before frame</div>
<iframe src="frame_content.htm" id="iframe_content"></iframe>
<div>content after frame</div>
</body>
</html>
To locate elements within a frame, Watir must be explicitly told to do so:
browser.iframe(id: 'frame_content').div(id: 'in_frame').text
#=> "content in frame"