Skip to main content

Selectors

The WebDriver Protocol provides several selector strategies to query an element. WebdriverIO simplifies them to keep selecting elements simple. Please note that even though the command to query elements is called $ and $$, they have nothing to do with jQuery or the Sizzle Selector Engine.

While there are so many different selectors available, only a few of them provide a resilient way to find the right element. For example, given the following button:

<button
id="main"
class="btn btn-large"
name="submission"
role="button"
data-testid="submit"
>
Submit
</button>

We do and do not recommend the following selectors:

SelectorRecommendedNotes
$('button')🚨 NeverWorst - too generic, no context.
$('.btn.btn-large')🚨 NeverBad. Coupled to styling. Highly subject to change.
$('#main')⚠️ SparinglyBetter. But still coupled to styling or JS event listeners.
$(() => document.queryElement('button'))⚠️ SparinglyEffective querying, complex to write.
$('button[name="submission"]')⚠️ SparinglyCoupled to the name attribute which has HTML semantics.
$('button[data-testid="submit"]')✅ GoodRequires additional attribute, not connected to a11y.
$('aria/Submit')✅ GoodGood. Resembles how the user interacts with the page. It is recommended to use translation files so your tests don't break when translations are updated. On WebDriver BiDi sessions this uses the browser accessibility tree. On Classic sessions it falls back to XPath and can be slower on large pages.
$('button=Submit')✅ AlwaysBest. Resembles how the user interacts with the page and is fast. It is recommended to use translation files so your tests don't break when translations are updated.

Strict Mode​

As of v10 the $ command is strict: it represents exactly one element. If the selector matches more than one element, the command throws a StrictSelectorError instead of silently picking the first match:

// there are 12 buttons on the page
await $('button').click()
// StrictSelectorError: strict mode violation: `$("button")` resolved to 12 elements, expected 1.

This is the same behavior as Playwright locators. Cypress differs: its queries may resolve to several elements, and it is the action commands such as .click() that reject a multi-element subject by default. Strict mode surfaces selectors that are too broad, which would otherwise silently interact with the wrong element as soon as the page grows.

The rule applies to every step of a chain and to every selector type $ accepts — string selectors (including ones that pierce the shadow DOM), JS functions, mobile selectors and custom strategy references.

What is not affected​

  • $$ keeps returning zero or many elements, as an ElementArray. Await the list (or its .length) before you read the count or use for...of. for await works on the list directly.
  • The dedicated helper commands custom$, shadow$ and react$ are not strict — they still return their first match, as do their $$ counterparts.
  • A selector that matches nothing still returns a lazily-resolved element, so waitForExist and the auto-waiting behavior are unchanged.
  • Passing an element reference, e.g. $(await browser.getActiveElement()), always refers to a single node and is never checked.
Migrating to v10

For how to audit your suite for strict-mode violations, narrow or opt out of individual queries, and disable strict mode project-wide, see the v10 migration guide.

CSS Query Selector​

If not indicated otherwise, WebdriverIO will query elements using the CSS selector pattern, e.g.:

selectors/example.js
loading...

To get an anchor element with a specific text in it, query the text starting with an equals (=) sign.

For example:

selectors/example.html
loading...

You can query this element by calling:

selectors/example.js
loading...

To find a anchor element whose visible text partially matches your search value, query it by using *= in front of the query string (e.g. *=driver).

You can query the element from the example above by also calling:

selectors/example.js
loading...

Note: You can't mix multiple selector strategies in one selector. Use multiple chained element queries to reach the same goal, e.g.:

const elem = await $('header h1*=Welcome') // doesn't work!!!
// use instead
const elem = await $('header').$('*=driver')

Element with certain text​

The same technique can be applied to elements as well. Additionally, it is also possible to do a case-insensitive matching using .= or .*= within the query.

For example, here's a query for a level 1 heading with the text "Welcome to my Page":

selectors/example.html
loading...

You can query this element by calling:

selectors/example.js
loading...

Or using query partial text:

selectors/example.js
loading...

The same works for id and class names:

selectors/example.html
loading...

You can query this element by calling:

selectors/example.js
loading...

Note: You can't mix multiple selector strategies in one selector. Use multiple chained element queries to reach the same goal, e.g.:

const elem = await $('header h1*=Welcome') // doesn't work!!!
// use instead
const elem = await $('header').$('h1*=Welcome')

Tag Name​

To query an element with a specific tag name, use <tag> or <tag />.

selectors/example.html
loading...

You can query this element by calling:

selectors/example.js
loading...

Name Attribute​

For querying elements with a specific name attribute, use a CSS selector such as [name="some-name"]. On a mobile session that same shorthand is sent with Appium's name locator strategy:

selectors/example.html
loading...
selectors/example.js
loading...

Note: The name locator strategy is an Appium locator. Desktop sessions keep [name="some-name"] on the CSS strategy.

xPath​

It is also possible to query elements via a specific xPath.

An xPath selector has a format like //body/div[6]/div[1]/span[1].

selectors/xpath.html
loading...

You can query the second paragraph by calling:

selectors/example.js
loading...

You can use xPath to also traverse up and down the DOM tree:

selectors/example.js
loading...

Accessibility Name Selector​

Query elements by their accessible name. The accessible name is what is announced by a screen reader when that element receives focus. The value of the accessible name can be both visual content or hidden text alternatives.

On WebDriver BiDi sessions (Chrome, Edge, Firefox and other BiDi-capable browsers) WebdriverIO first uses browsingContext.locateNodes with an accessibility locator. That queries the browser accessibility tree directly and is typically much faster than the XPath approximation. If the accessibility locator finds nothing, WebdriverIO falls back to the Classic XPath heuristic so existing aria/ queries keep matching.

info

You can read more about this selector in our release blog post

Fetch by aria-label​

selectors/aria.html
loading...
selectors/example.js
loading...

Fetch by aria-labelledby​

selectors/aria.html
loading...
selectors/example.js
loading...

Fetch by content​

selectors/aria.html
loading...
selectors/example.js
loading...

Fetch by title​

selectors/aria.html
loading...
selectors/example.js
loading...

Fetch by alt property​

selectors/aria.html
loading...
selectors/example.js
loading...

Role Selector​

Query elements by their ARIA role and accessible name, the way a screen reader describes them: "the Add to cart button". A role plus a name keeps matching when class names, test ids or the DOM structure change.

await $('role/button[name="Add to cart"]').click()
await expect($('role/heading[name="Order summary"]')).toBeDisplayed()

// role only
const rows = await $$('role/row')

// scoped to a parent element
const dialog = $('role/dialog[name="Checkout"]')
await dialog.$('role/button[name="Pay now"]').click()

The syntax is role/<role> or role/<role>[name="<accessible name>"]. Single quotes work too, and a quote inside the name is escaped with a backslash: role/button[name="Say \"hi\""].

  • The name has to match the full accessible name.
  • The role has to be an ARIA role. A typo fails with the closest valid role, for example "buton" is not an ARIA role. Did you mean "button"?.
  • img and its ARIA 1.3 name image are the same role.
  • The selector follows the strict mode of $ like every other selector.

On a WebDriver BiDi session, WebdriverIO passes role and name to browsingContext.locateNodes. The browser computes both itself, the same way assistive technology sees the page. Elements inside open shadow roots and inside frames, including frames from another origin, are found. If the browser finds no element, there is no fallback to a heuristic. Note that the browser decides the role: for example, a <table> without headers or caption can be a layout table, and its rows then have no row role.

On a WebDriver Classic session, and when a browser does not support the role locator, WebdriverIO computes role and accessible name in the page with dom-accessibility-api, the implementation Testing Library uses. A text field without a label is named by its placeholder, as browsers do. The role selector is not available in a native mobile app context. Use an accessibility id there.

ARIA - Role Attribute​

For querying elements based on ARIA roles, you can directly specify role of the element like [role=button] as selector parameter. This selector approximates the role from the element name and attributes. Prefer the role selector, which uses the role the browser computes and can also match the accessible name:

selectors/aria.html
loading...
selectors/example.js
loading...

ID Attribute​

Locator strategy "id" is not supported in WebDriver protocol, one should use either CSS or xPath selector strategies instead to find elements using ID.

However some drivers (e.g. Appium You.i Engine Driver) might still support this selector.

Current supported selector syntaxes for ID are:

//css locator
const button = await $('#someid')
//xpath locator
const button = await $('//*[@id="someid"]')
//id strategy
// Note: works only in Appium or similar frameworks which supports locator strategy "ID"
const button = await $('id=resource-id/iosname')

JS Function​

You can also use JavaScript functions to fetch elements using web native APIs. Of course, you can only do this inside a web context (e.g., browser, or web context in mobile).

Given the following HTML structure:

selectors/js.html
loading...

You can query the sibling element of #elem as follows:

selectors/example.js
loading...

Deep Selectors​

warning

Starting with v9 of WebdriverIO there is no need for this special selector as WebdriverIO automatically pierces through the Shadow DOM for you. It is recommended to migrate off this selector by removing the >>> in front it.

Many frontend applications heavily rely on elements with shadow DOM. It is technically impossible to query elements within the shadow DOM without workarounds. The shadow$ and shadow$$ have been such workarounds that had their limitations. With the deep selector you can now query all elements within any shadow DOM using the common query command.

Given we have an application with the following structure:

Chrome Example

With this selector you can query the <button /> element that is nested within another shadow DOM, e.g.:

selectors/example.js
loading...

Mobile Selectors​

For hybrid mobile testing, it's important that the automation server is in the correct context before executing commands. For automating gestures, the driver ideally should be set to native context. But to select elements from the DOM, the driver will need to be set to the platform's webview context. Only then can the methods mentioned above can be used.

For native mobile testing, there is no switching between contexts, as you have to use mobile strategies and use the underlying device automation technology directly. This is especially useful when a test needs some fine-grained control over finding elements.

Android UiAutomator​

Android’s UI Automator framework provides a number of ways to find elements. You can use the UI Automator API, in particular the UiSelector class to locate elements. In Appium you send the Java code, as a string, to the server, which executes it in the application’s environment, returning the element or elements.

const selector = 'new UiSelector().text("Cancel").className("android.widget.Button")'
const button = await $(`android=${selector}`)
await button.click()

Android DataMatcher and ViewMatcher (Espresso only)​

Android's DataMatcher strategy provides a way to find elements by Data Matcher

const menuItem = await $({
"name": "hasEntry",
"args": ["title", "ViewTitle"]
})
await menuItem.click()

And similarly View Matcher

const menuItem = await $({
"name": "hasEntry",
"args": ["title", "ViewTitle"],
"class": "androidx.test.espresso.matcher.ViewMatchers"
})
await menuItem.click()

Android View Tag (Espresso only)​

The view tag strategy provides a convenient way to find elements by their tag.

const elem = await $('-android viewtag:tag_identifier')
await elem.click()

iOS UIAutomation​

When automating an iOS application, Apple’s UI Automation framework can be used to find elements.

This JavaScript API has methods to access to the view and everything on it.

const selector = 'UIATarget.localTarget().frontMostApp().mainWindow().buttons()[0]'
const button = await $(`ios=${selector}`)
await button.click()

You can also use predicate searching within iOS UI Automation in Appium to refine element selection even further. See here for details.

iOS XCUITest predicate strings and class chains​

With iOS 10 and above (using the XCUITest driver), you can use predicate strings:

const selector = `type == 'XCUIElementTypeSwitch' && name CONTAINS 'Allow'`
const switch = await $(`-ios predicate string:${selector}`)
await switch.click()

And class chains:

const selector = '**/XCUIElementTypeCell[`name BEGINSWITH "D"`]/**/XCUIElementTypeButton'
const button = await $(`-ios class chain:${selector}`)
await button.click()

Accessibility ID​

The accessibility id locator strategy is designed to read a unique identifier for a UI element. This has the benefit of not changing during localization or any other process that might change text. In addition, it can be an aid in creating cross-platform tests, if elements that are functionally the same have the same accessibility id.

  • For iOS this is the accessibility identifier laid out by Apple here.
  • For Android the accessibility id maps to the content-description for the element, as described here.

For both platforms, getting an element (or multiple elements) by their accessibility id is usually the best method. It is also the preferred way over the deprecated name strategy.

const elem = await $('~my_accessibility_identifier')
await elem.click()

Class Name​

The class name strategy is a string representing a UI element on the current view.

  • For iOS it is the full name of a UIAutomation class, and will begin with UIA-, such as UIATextField for a text field. A full reference can be found here.
  • For Android it is the fully qualified name of a UI Automator class, such android.widget.EditText for a text field. A full reference can be found here.
  • For Youi.tv it is the full name of a Youi.tv class, and will being with CYI-, such as CYIPushButtonView for a push button element. A full reference can be found at You.i Engine Driver's GitHub page
// iOS example
await $('UIATextField').click()
// Android example
await $('android.widget.DatePicker').click()
// Youi.tv example
await $('CYIPushButtonView').click()

Chain Selectors​

If you want to be more specific in your query, you can chain selectors until you've found the right element. If you call element before your actual command, WebdriverIO starts the query from that element.

For example, if you have a DOM structure like:

<div class="row">
<div class="entry">
<label>Product A</label>
<button>Add to cart</button>
<button>More Information</button>
</div>
<div class="entry">
<label>Product B</label>
<button>Add to cart</button>
<button>More Information</button>
</div>
<div class="entry">
<label>Product C</label>
<button>Add to cart</button>
<button>More Information</button>
</div>
</div>

And you want to add product B to the cart, it would be difficult to do that just by using the CSS selector.

With selector chaining, it's way easier. Simply narrow down the desired element step by step:

await $('.row .entry:nth-child(2)').$('button*=Add').click()

Appium Image Selector​

Using the -image locator strategy, it is possible to send an Appium an image file representing an element you want to access.

Supported file formats jpg,png,gif,bmp,svg

Full reference can be found here

const elem = await $('./file/path/of/image/test.jpg')
await elem.click()

Note: The way how Appium works with this selector is that it will internally make a (app)screenshot and use the provided image selector to verify if the element can be found in that (app)screenshot.

Be aware of the fact that Appium might resize the taken (app)screenshot to make it match the CSS-size of your (app)screen (this will happen on iPhones but also on Mac machines with a Retina display because the DPR is bigger than 1). This will result in not finding a match because the provided image selector might have been taken from the original screenshot. You can fix this by updating the Appium Server settings, see the Appium docs for the settings and this comment on a detailed explanation.

React Selectors​

WebdriverIO provides a way to select React components based on the component name. To do this, you have a choice of two commands: react$ and react$$.

These commands allow you to select components off the React VirtualDOM and return either a single WebdriverIO Element or an array of elements (depending on which function is used).

Note: The commands react$ and react$$ are similar in functionality, except that react$$ will return all matching instances as an array of WebdriverIO elements, and react$ will return the first found instance.

The commands work with React 16 to 19, for an app that starts with createRoot or with ReactDOM.render. They read the components of the current render, so they also find components that a state change added. If React has not rendered a root of the page yet, they wait up to 5 seconds for it.

Basic example​

// index.jsx
import React from 'react'
import { createRoot } from 'react-dom/client'

function MyComponent() {
return (
<div>
MyComponent
</div>
)
}

function App() {
return (<MyComponent />)
}

createRoot(document.querySelector('#root')).render(<App />)

In the above code there is a simple MyComponent instance inside the application, which React is rendering inside a HTML element with id="root".

With the browser.react$ command, you can select an instance of MyComponent:

const myCmp = await browser.react$('MyComponent')

Now that you have the WebdriverIO element stored in myCmp variable, you can execute element commands against it.

Filtering components​

You can filter your selection by the props and/or the state of the component. To do so, pass props and/or state in the second argument of the command.

// index.jsx
import React from 'react'
import ReactDOM from 'react-dom'

function MyComponent(props) {
return (
<div>
Hello { props.name || 'World' }!
</div>
)
}

function App() {
return (
<div>
<MyComponent name="WebdriverIO" />
<MyComponent />
</div>
)
}

ReactDOM.render(<App />, document.querySelector('#root'))

If you want to select the instance of MyComponent that has a prop name as WebdriverIO, you can execute the command like so:

const myCmp = await browser.react$('MyComponent', {
props: { name: 'WebdriverIO' }
})

If you wanted to filter our selection by state, the browser command would looks something like so:

const myCmp = await browser.react$('MyComponent', {
state: { myState: 'some value' }
})

A filter matches when each of its keys that the component also has matches. A key that the component does not have is ignored. A nested object matches the same way, and an array matches when it has one value in common with the array of the component. null, false and 0 match the same value. For a function component with hooks, the state is the state of the first hook (useState or useReducer): if the first hook is another hook, for example useRef, the state filter does not match. With both props and state, a component must match both.

Selector rules​

  • * matches one or more characters: browser.react$$('My*') finds MyComponent and MyOtherComponent.
  • Names separated by spaces find a component inside another one: browser.react$$('List Item') finds each Item inside a List.
  • The name of a component is its displayName, or else the name of its function or class. A component of React.memo has the name of its function (the development build of React 17 also gives it the displayName of the memo object). A component of React.forwardRef has no name, unless it has a displayName.
  • For a higher-order component with a name like withRouter(MyComponent), the name inside the parentheses is used: MyComponent.
  • Without an element scope, the commands search all React roots of the page, in the order of the document, also roots inside other roots and roots in open shadow roots. react$ gives the first match. To search one root only, call the command on its container or on an element of that root: $('#other-root').react$$('MyComponent').
  • The results come root after root. In a root, they come in the order of the component tree, level by level, not in the order of the document. react$$ gives each DOM node once.
  • For an app in a frame, call the command on the browsing context of the frame, or on an element of the frame: (await page.frame({ selector: 'iframe' })).react$$('MyComponent').

Known limits:

  • A component that renders only text gives a text node. With WebDriver Classic, a text node cannot be sent back, and the command fails with javascript error: circular reference.
  • While React hydrates a Suspense boundary of a server-rendered page, the components inside it do not exist yet. Wait until the page has finished to hydrate.

Dealing with React.Fragment​

When using the react$ command to select React fragments, WebdriverIO will return the first child of that component as the component's node. If you use react$$, you will receive an array containing all the HTML nodes inside the fragments that match the selector.

// index.jsx
import React from 'react'
import ReactDOM from 'react-dom'

function MyComponent() {
return (
<React.Fragment>
<div>
MyComponent
</div>
<div>
MyComponent
</div>
</React.Fragment>
)
}

function App() {
return (<MyComponent />)
}

ReactDOM.render(<App />, document.querySelector('#root'))

Given the above example, this is how the commands would work:

await browser.react$('MyComponent') // returns the WebdriverIO Element for the first <div />
await browser.react$$('MyComponent') // returns the WebdriverIO Elements for the array [<div />, <div />]

Note: If you have multiple instances of MyComponent and you use react$$ to select these fragment components, you will be returned an one-dimensional array of all the nodes. In other words, if you have 3 <MyComponent /> instances, you will be returned an array with six WebdriverIO elements.

Custom Selector Strategies​

If your app requires a specific way to fetch elements you can define yourself a custom selector strategy that you can use with custom$ and custom$$. For that register your strategy once in the beginning of the test, e.g. in a before hook:

queryElements/customStrategy.js
loading...

Given the following HTML snippet:

queryElements/example.html
loading...

Then use it by calling:

queryElements/customStrategy.js
loading...

Note: this only works in an web environment in which the execute command can be run.

Welcome! How can I help?

WebdriverIO AI Copilot