Using Semantic Locators in your web UI tests

Semantic Locators let you specify HTML elements in code similar to how you might describe them to a human. For example, a create button might have a semantic locator of {button 'Create'}.

Semantic Locators are stable, readable, enforce accessibility, and can be auto-generated.

This codelab teaches you about why you might want to use Semantic Locators, how to generate or write them, and how to integrate them in your tests.

Prerequisites

An accessible website, or a site you'd like to make more accessible

Semantic locators rely on HTML accessibility (a11y) features to find elements. Therefore, if your app is inaccessible to a screen reader it is unlikely to work with semantic locators. If you're looking to improve a11y, introducing semantic locators can be a great strategy!

Most modern web frameworks are accessible by default, and if you use semantic HTML (e.g., <button>, <input> and <li> rather than <div> for everything) your app will benefit from native a11y features. So you may not have any additional work to do.

No strong cross-language requirements for your tests

...or a way to access localized strings from your tests.

Semantic locators use the text that a screen reader would read to a human, so are usually locale-specific. If you want to test in multiple languages, you will most likely have to adapt your locators.

See the FAQ for a discussion of this requirement and potential solutions.

As the name suggests, Semantic Locators find elements based on their semantics. This has a number of benefits over other types of locators.

Semantics

First we should define the term "semantics".

The semantics of an element describe its meaning to a user. Is it a button or a checkbox? Will it submit or cancel an operation? When using assistive technologies like screen readers, the semantics of an element determine how it is described to users.

There are many semantically equivalent ways to implement OK buttons. The following elements are all matched by the semantic locator {button 'OK'}.

<button>OK</button>
<button aria-label="OK">...</button>
<div role="button">OK</div>
<input type="submit" aria-label="OK">
<button aria-labelledby="other_element">...</button><div id="other_element">OK</div>
<button id="element_id">...</button><label for="element_id">OK</label>

To be precise, button refers to the ARIA role expressed by the element. 'OK' refers to the accessible name of the element.

What benefits does finding elements by their semantics provide?

Stability

Semantic locators are less brittle to user-invisible changes. Matching semantics abstracts away implementation details. For example if

<div><span><div><div><div role="button" aria-label="Send">

changes to

<div><button>Send

then {button 'Send'} will still work as a locator.

Accessibility

Semantic locators can help surface missing or broken semantics, as locators won't exist for inaccessible elements. For example, if your submit button has an incorrect accessible name, the error from Semantic Locators can reveal the bug:

> findBySemanticLocator('{button "Submit"}');

Error:
Didn't find any elements matching semantic locator {button "Submit"}.
1 element with an ARIA role of button was found.
However it didn't have an accessible name of "Submit".
Accessible names found: ["right_arrow.png"].

Readability

Semantics are meant for human consumption, and so are semantic locators. They're very similar to how a screen-reader would announce elements to a non-sighted user.

Cheap to produce

Semantic Locators can be automatically generated or easily written with the help of browser dev tools.

We'll explore how to easily write semantic locators in the next step.

Semantic locators have one required part, the ARIA role, and two optional parts, accessible name and ARIA attributes. The role and name are almost always enough to identify elements.

In the locator {button 'OK'} the role is button and the accessible name is OK.

Usually you won't have to write semantic locators by hand as they can be easily auto-generated from a Chrome extension, an interactive playground, or from code.

Chrome Extension

An easy way to create Semantic Locators for your app is to auto-generate them with a Chrome Extension.

Install the extension and click the icon next to the URL bar to get started.

Playground

The interactive semantic locator playground auto-generates semantic locators for elements in some HTML you enter. It can be useful for:

  • Writing locators for many elements at once
  • Sharing HTML snippets with their a11y data
  • Debugging the semantics of an element

Generate locators from code

Locator generation is available from the semantic locator libraries. If you already have some other types of locators you can generate semantic locators for these elements by temporarily adding generation code to existing tests.

The following example logs generated semantic locators to the console. However, you could go further, for example, automatically re-writing your tests to use semantic locators. We'd love to see what you build in this space!

Java

import com.google.semanticlocators.BySemanticLocator;
...

WebElement targetElement = driver.findElement(By.xpath("//div[@aria-label='Cheese']"));
System.out.println("Semantic locator: " + BySemanticLocator.closestPreciseLocatorFor(targetElement));

Python

from semantic_locators import closest_precise_locator_for
...

target_element = driver.find_element(By.XPATH, "//div[@aria-label='Cheese']");
print("Semantic locator: " + closest_precise_locator_for(target_element));

JavaScript/TypeScript

import {closestPreciseLocatorFor} from 'semantic-locators/gen'
...

const targetElement = document.getElementById('cheese');
console.log('Semantic locator: ' + closestPreciseLocatorFor(targetElement));

Developer Console

If for some reason auto-generation doesn't work for you, the Accessibility tab of browser developer tools can help you easily write semantic locators.

  1. Open the Developer Console by pressing F12.
  2. [Chrome] Select the target element with the element picker (⌘+Shift+C or Ctrl+Shift+C) then navigate to the Accessibility tab.
    . [Firefox] Navigate to the Accessibility tab, click the picker icon (top left of Dev tools), then click the target element.
  3. Check the name and role of the element. For an element with the role button and name Create, the semantic locator is {button 'Create'}.

Screenshot of the accessibility tree in Chrome developer console. Thehighlighted element is described in the a11y tree as button&ldquo;create&rdquo;

Dynamic or very long accessible names

If the accessible name is dynamic, or is too long for your test, you can use a wildcard value. Values accept * as a wildcard (e.g., '* view', 'https://*.google.com/*').

Try it in the playground

<button aria-label="Today, 1st April">    <!-- {button 'Today*'} -->
  Today
</button>

Optional names

It's not always necessary to specify a name - some elements have no accessible name, or a completely dynamic one. {list} is a valid locator if you know there's only going to be one list on the page.

Try it in the playground

<ul>                                           <!-- {list} -->
  <li aria-label="Cheese">Cheese</li>          <!-- {listitem 'Cheese'} -->
  <li aria-label="Chocolate">Chocolate</li>    <!-- {listitem 'Chocolate'} -->
</ul>

Refining locators

Using the above strategies might still return multiple elements. In this case you can make a semantic locator more precise in a few ways.

Multiple Semantic Locator elements

Semantic locators can be combined, with later elements being descendants of earlier elements.

Try it in the playground

<ul>
  <li aria-label="Cheese">
    <button aria-label="Eat">   <!-- {listitem 'Cheese'} {button 'Eat'} -->
      Eat cheese
    </button>
  </li>
  <li aria-label="Chocolate">
    <button aria-label="Eat">   <!-- {listitem 'Chocolate'} {button 'Eat'} -->
      Eat chocolate
    </button>
  </li>
</ul>

Attributes

Semantic locators can locate elements based on attributes such as checked and disabled. Both native html (<button disabled>) and explicit semantics (aria-disabled="true") are included.

The source of truth for supported attributes is SUPPORTED_ATTRIBUTES.

Try it in the playground

<h1>Cheese</h1>
<label>
  <input type="checkbox">       <!-- {checkbox 'Edible' checked:false} -->
  Edible
</label>
<br>
<button disabled>Eat</button>   <!-- {button 'Eat' disabled:true} -->

Outer

Sometimes (e.g., when working with lists) nested elements may both match the same locator. In this case you can use the outer keyword to match only the outermost element.

Try it in the playground

<ul>                    <!-- outer {list} -->
  <li>                  <!-- outer {listitem} -->
    <ul>                <!-- {listitem} {list} -->
      <li>Cheese</li>   <!-- {listitem} {listitem} -->
    </ul>
  </li>
</ul>

In some environments libraries already exist to use semantic locators from your tests.

If no library exists yet for your language/environment, they are usually simple to write and contributions are very welcome! See the instructions below.

Libraries

Java (WebDriver)

Semantic locators can be used with Selenium WebDriver in a similar way to ByXPath or ByCssSelector.

Add the following to your pom.xml:

<dependency>
  <groupId>com.google.semanticlocators</groupId>
  <artifactId>semantic-locators</artifactId>
  <version>1.0.0</version>
  <scope>test</scope>
</dependency>

Once installed, use Semantic Locators as follows:

import com.google.semanticlocators.BySemanticLocator;
...

WebElement searchButton = driver.findElement(new BySemanticLocator("{button 'Google search'}"));
ArrayList<WebElement> allButtons = driver.findElements(new BySemanticLocator("{button}"));

String generated = BySemanticLocator.closestPreciseLocatorFor(searchButton); // {button 'Google search'}

Python (WebDriver)

Semantic locators can be used with Selenium WebDriver in a similar way to ByXPath or ByCssSelector.

Install with python -m pip install semantic-locators

Once installed, use Semantic Locators as follows:

from semantic_locators import (
    find_element_by_semantic_locator,
    find_elements_by_semantic_locator,
    closest_precise_locator_for,
)
...

search_button = find_element_by_semantic_locator(driver, "{button 'Google search'}")
all_buttons = find_elements_by_semantic_locator(driver, "{button}")

generated = closest_precise_locator_for(search_button); # {button 'Google search'}

JavaScript and TypeScript

Semantic locators can be used in JS (or TS) running in the browser.

$ npm install --save-dev semantic-locators

Once installed, use Semantic Locators as follows:

import {findElementBySemanticLocator, findElementsBySemanticLocator} from 'semantic-locators';
import {closestPreciseLocatorFor} from 'semantic-locators/gen'
...
const searchButton = findElementBySemanticLocator("{button 'Google search'}");
const allButtons = findElementsBySemanticLocator("{button}");

const generated = closestPreciseLocatorFor(searchButton); // {button 'Google search'}

Adding a new integration

Integrating Semantic Locators with a new language/environment/library is usually a straightforward task (<1 day of effort). The following instructions assume you want to contribute the code back to google/semantic-locators.

For an example see the implementation for Webdriver Java.

Create a README.md and DEVELOPING.md

Create the two markdown files required for new integrations:

  1. README.md explaining how a user can install and use the library.
  2. DEVELOPING.md explaining how a developer can test and deploy a new version of the library.

Wrapper Binary

javascript/wrapper_bin.js contains the compiled definition of Semantic Locators to be used from wrapper libraries. Avoid duplicating the file within this repo, instead it should be copied as part of a build script. For example see the copy-resources section in webdriver_java/pom.xml.

Execution

Semantic locator resolution is implemented in JavaScript, so you just need a way to execute JS from your test. Testing frameworks usually provide an API for this.

See BySemanticLocator.java for a reference implementation. The basic flow is:

  • Read wrapper_bin.js.
  • Execute the script to load Semantic Locators in the browser.
  • Execute return window.<function>.apply(null, arguments); where <function> is a function exported in wrapper.ts (e.g., return window.findElementBySemanticLocator.apply(null, arguments);).
  • Parse any failures and throw an appropriate exception.

Tests

All new code must be tested. There's no need to test the full behaviour of semantic locators, but please include smoke tests, and test things which might break on serializing/deserializing to send to the browser. See BySemanticLocatorTest.java for an example.

CI

We strongly recommended adding Continuous Integration to test and lint your code. We use GitHub actions - see the existing workflows in .github/workflows/*.yml.

Send a Pull Request

Open a pull request in google/semantic-locators so a project maintainer can review it.

Codelab

Please ask a Googler contributor to add code examples to this codelab.

How can I handle internationalization/localization (i18n/L10n)?

Semantic locators don't yet have built-in support for tests with strong cross-language requirements. In general you'll need a different locator for each locale, as {button 'Hello'} won't find <button>Bonjour</button>. Solutions to localized tests will be specific to your L10n method and test environment, but there are a few general approaches which may work.

One solution is to parameterize tests based on the locale, and get the localized string from an id and locale. A pseudocode example:

function myTest(@Parameter locale):
  submitMsg = readFromTranslationFile("SUBMIT", locale)
  searchButton = findElementBySemanticLocator("{button '" + submitMsg + "'}")

Some libraries (such as Closure ) perform L10n when compiling JavaScript. It may be possible to access L10n APIs from your test and inject the localized strings into locators. For example in Closure:

const searchButton = findElementBySemanticLocator(`{button '${goog.getMsg('Search')}'}`);

Which browsers are supported?

Semantic locators are tested on recent versions of:

  • Chrome
  • Firefox
  • Internet Explorer

Bugs and patches are accepted for other major browsers.

Please add support for XXX language/environment

See the section "Integrating with your tests". If you have difficulty adding an integration for a certain platform, feel free to file an issue on GitHub

I have more questions

Please file an issue on GitHub to get in touch.