Full Path Is Not an Option

Let's look at the web page below and think of good XPATH expressions for UserName input field.

<form>
  <div>
    <p>
      <input name="usrName" id="UserName" class="textbox" autocomplete="off" type="text">
    </p>
    <p>
      <input name="pwd" id="Password" class="textbox" autocomplete="off" type="password">
    </p>
  </div>
</form>

Obviously

    //input[@id='UserName']

is better than

    /form/div/p/input

because full expression that enumerates nodes from root of a DOM tree to one of it's leaves gets broken after almost any update of a web page.

If there is no ID then

    //input[1]

is still better than

    /form/div/p/input

A Tree or Not a Tree

Here is another example.

<div class="mainPanel">
  <a>All vendors</a>
  <a>General ledger</a>
</div>
<div role="tree">
  <div role="group" title="Favorities" aria-expanded="false">Favorities</div>
  <div role="group" title="Recent" aria-expanded="true">Recent</div>
  <div role="treeitem"><a title="Accounts payable > Vendors">All vendors</a></div>
  <div role="treeitem"><a title="Procurement and sourcing > Purchase orders">All purchase orders</a></div>
  <div role="group" title="Workspaces" aria-expanded="false">Workspaces</div>
  <div role="group" title="Modules" aria-expanded="true">Modules</div>
  <a role="treeitem" title="Cash and bank management">Cash and bank management</a>
  <a role="treeitem" title="Cost management">Cost management</a>
  <a role="treeitem" title="General ledger">General ledger</a>
</div>

This is a three level tree but nodes of second and third levels are both children of the tree root. So there is no relationship parent-child between second and third level nodes.

First level of the tree is presented by a single root node with role tree. Second level consists of nodes with role group. And third includes nodes with role treeitem. Also if a second level node has aria-expanded="false" then it's descendants are not present in the DOM tree (Favorities and Workspaces are examples of such nodes).

How to reliably navigate to a third level node?

If the node is visible it can be found as

  //a[text()='All vendors']
  //a[text()='General ledger']

But such XPATH expressions will return more than one result. So instead of a tree item we may click on a link in mainPanel.

Better search for the text from the tree root

  //div[@role='tree']//a[text()='General ledger']
  //div[@role='tree']//a[text()='All vendors']

And if the required result should be always a node with role="treeitem" we can use

  //*[text()='All vendors']/ancestor-or-self::*[@role='treeitem']
  //*[text()='General ledger']/ancestor-or-self::*[@role='treeitem']

If the second level node is collapsed (it can be determined from the attribute aria-expanded) then our expressions will not work. So to reliably reach an arbitrary node in the tree we need to know a path to it from the root. The algorithm of finding a node in pseudo notation will look like

  1. Find second level node by its name.
  2. If it is collapsed then expand it.
  3. Find third level node by its name.

So knowing XPATH expression of a node may be insufficient to find and interact with it.

Another example of this is a node that appears only when mouse is hovered over a UI control, it can be a sub menu item or even a simple link.

XPATH Building Blocks

Now when you have an idea of how reliable identification of elements in Web UI looks like we present a list of syntactic examples you can use for building resilient XPATH expressions.

Element with ID

Element with id attribute set to a given value.

  //input[@id='UserName']

Element by Index

Second input element on the page.

  //input[2]

Element with Text

Link with a given text.

  //a[text()='Log In']

Sometimes text contains extra spaces or carriage return symbols. To filter them out use normalize-space function.

  //a[normalize-space(text())="Book Management"]

Element with Class

Sometimes an element may belong to several classes.

  <div class="column columnHeader sortable"></div>

To match a specific class with XPATH use contains, concat and normalize-space functions.

  //div[contains(concat(' ', normalize-space(@class), ' '), ' columnHeader ')]

Notice spaces in ' columnHeader ' - second argument to contains function.

Element with Attribute

Find an element which contains a specific attribute. For example, to find all divelements which have id attribute do:

//div[@id]

Element without Attribute

Find an element which does not contain a specific attribute. In this example we need a div element with role set to main and without style attribute.

  //div[@role='main' and not(@style)]//div[@data-target="selectionArea"]

Element with Child Element with Specific Text

Select an element which contains a child element with a given text.

  <div role="tab">
    <header>
      <h1>Vendor</h1>
    </header>
    <header>
      <h1>Details</h1>
    </header>
  </div>
  //div[@role='tab']/header[h1/text()='Vendor']

Specific Parent of Element

Select parent node of an element with a given text.

  <div role="treeitem">
    <a>All vendors</a>
  </div>
  //*[text()='All vendors']/ancestor-or-self::*[@role='treeitem']

Find a Cell with Specific Text in a Given Column of a Table

Let's assume there is a table of standard structure and we want to search column 2 for a cell with text Window.

//table[@id="data"]//tr/td[2][normalize-space(.//text())='Window']

Click in Last Row of a Table

To click in a cell of a last row in a table (for example, it may be a row for inserting a new record and you want to click on Insert button) use last() function.

//table[@id='books']/tbody/tr[last()]/td[7]/div/button[2]