1. Introduction
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.
2. Why Semantic Locators?
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.
3. Writing Semantic Locators
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.
- Open the Developer Console by pressing F12.
- [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. - Check the name and role of the element. For an element with the role
button
and nameCreate
, the semantic locator is{button 'Create'}
.
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/*'
).
<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.
<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.
<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
.
<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.
<ul> <!-- outer {list} -->
<li> <!-- outer {listitem} -->
<ul> <!-- {listitem} {list} -->
<li>Cheese</li> <!-- {listitem} {listitem} -->
</ul>
</li>
</ul>
4. Integrating with your tests
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:
README.md
explaining how a user can install and use the library.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 inwrapper.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.
5. FAQ
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.