Selectors
Testplane provides multiple ways to locate elements on a web page.
Recommendations for using selectors
We recommend locating elements the same way users interact with the application being tested.
When choosing a selector, consider 3 factors:
- User-centric — the selector reflects what the user sees and interacts with
- Stability — the selector doesn't break during refactoring, CSS changes, or page structure changes
- Uniqueness — the selector unambiguously identifies the required element
| Selector / Selection method | Recommendation | Notes |
|---|---|---|
By visible content — text, role, ... (getByRole, getByText, ...) | ✅ Best | Locate elements the same way users do: by text, role, label — a stable and "honest" way to find elements. |
By data-testid attribute (e.g., [data-testid="submit"]) | ✅ Good | The data-testid attribute is designed specifically for automation: it doesn't affect styles/accessibility, is stable, and clearly indicates the element's purpose, but requires manual markup. |
By ID (e.g., #main) | ⚠️ Rarely | IDs are more stable than classes, but can still depend on JS logic or markup changes. Use only if the ID is guaranteed to be stable. |
By attribute type (e.g., input[type="text"]) | ⚠️ Rarely | Works if the attribute type is semantically meaningful and stable. However, it can be too generic (many elements of the same type). |
By attributes (except data-testid, e.g., [name="email"]) | ⚠️ Rarely | Depends on HTML attribute semantics. Can be unstable (attributes may change or be removed). |
XPath (e.g., (//button)[2]) | 🔴 Poor | Element position can change, and XPath selectors are generally hard to maintain. However, they can be useful as they provide maximum flexibility for locating elements. |
By class (e.g., .sidebar .list-item.state-opened-qU1azF) | 🔴 Poor | Not related to user behavior and doesn't reflect semantics, with a hard dependency on hierarchy. |
WebdriverIO
Testplane supports element location compatible with WebdriverIO syntax: CSS selectors, XPath, element text, and other methods described below.
By text selectors
This type of selector allows you to find elements by the text they contain:
="text"— exact text match*="text"— partial text matcha*=text— text match inside an<a>tagdiv.=text— case-insensitive text match inside a<div>tag
describe("Link Text selector", () => {
it("Finding an element by exact text match", async ({ browser }) => {
await browser.url("https://testplane.io/");
// Exact text match for a link
const docsLink = await browser.$("=Documentation");
const isDocsLinkFound = await docsLink.isExisting();
console.log(`Element with exact text "Documentation" found: ${isDocsLinkFound}`);
// Partial text match for a link
const partialLink = await browser.$("*=Docum");
const isPartialLinkFound = await partialLink.isExisting();
console.log(`Element with partial text "Docum" found: ${isPartialLinkFound}`);
// Partial match with specified <a> tag
const tagPartialLink = await browser.$("a*=Document");
const isTagPartialLinkFound = await tagPartialLink.isExisting();
console.log(`<a> element with partial text "Document" found: ${isTagPartialLinkFound}`);
// Case-insensitive search with div tag
const divCaseInsensitive = await browser.$("div.=testplane");
const isDivCaseInsensitiveFound = await divCaseInsensitive.isExisting();
console.log(
`<div> element with case-insensitive text "testplane" found: ${isDivCaseInsensitiveFound}`,
);
});
});
Use this approach if:
- the element text is stable
- you need the test to closely mimic real user scenarios
CSS selectors
By the data-testid attribute
This approach is suitable for finding elements that are marked with testing attributes.
describe("CSS selector by the data-testid attribute", () => {
it("Finding an element by data-testid", async ({ browser }) => {
// Open the page and wait for it to load
await browser.openAndWait("https://testplane.io/");
// Find element by the data-testid attribute
const element = await browser.$('[data-testid="main-content"]');
// Check if the element exists in the DOM
const isExisting = await element.isExisting();
console.log("Element with data-testid exists:", isExisting);
});
});
Use this approach if:
- you are creating selectors specifically for testing
- you need selector stability regardless of UI/style changes
By class
To find an element by class, use the ".class-name" selector.
describe("CSS selector by class", () => {
it("Finding an element on the main page", async ({ browser }) => {
await browser.openAndWait("https://testplane.io/");
// Find element by class "navbar"
const navbar = await browser.$(".navbar");
// Check if the element is displayed on the page
const isDisplayed = await navbar.isDisplayed();
console.log("Navbar is displayed:", isDisplayed);
});
});
Use this approach if:
- the class is stable and not generated dynamically
- you need a quick and simple way to find an element
- the class semantically describes the element (e.g.,
.error-message,.success-banner) - you are working with component libraries where classes are part of the API
By ID
To find an element by id, use a selector of the form "#id".
describe("CSS selector by ID", () => {
it("Finding an element by ID on the main page", async ({ browser }) => {
await browser.openAndWait("https://testplane.io/");
// Find element by ID "__docusaurus"
const main = await browser.$("#__docusaurus");
// Check if the element is displayed on the page
const isDisplayed = await main.isDisplayed();
console.log("Element is displayed:", isDisplayed);
});
});
Use this approach if:
- you need a quick and simple way to find elements
- the
idis part of the component's public API - you need maximum selector performance (
idis the fastest selector)
By attribute type
To find an element by attribute, use a selector of the form input[type="name"].
describe("CSS selector by attribute type", () => {
it("Finding an element by attribute type", async ({ browser }) => {
await browser.openAndWait("https://testplane.io/");
// Find a button by the attribute type="button"
// Selector format: element[type="value"]
const button = await browser.$('button[type="button"]');
// Check if the element exists in the DOM
const isExisting = await button.isExisting();
console.log("Button exists:", isExisting);
});
});
Use this approach if:
- you need to find all elements of a certain type (all checkboxes, all radio buttons)
- you need to work with semantic HTML5 types (
email,tel,url,date)
XPath selectors
By element text
To find an element by the text it contains, use the selector //element[text()="text"].
// Exact text match
describe("XPath selector by element text", () => {
it("Finding an element by text", async ({ browser }) => {
await browser.openAndWait("https://testplane.io/");
// Find element by the text inside it
const link = await browser.$('//a[text()="Docs"]');
// Check if the element exists in the DOM
const isExisting = await link.isExisting();
console.log("Element with text exists:", isExisting);
});
});
Use this approach if:
- the element text is unique and stable (button labels, headings)
- other element-finding strategies are not applicable
By attributes
To find an element by attribute, use a selector of the form //element[@type="attribute"].
describe("XPath selector by attribute", () => {
it("Finding an element by attribute", async ({ browser }) => {
await browser.openAndWait("https://testplane.io/");
// Find element by the type attribute
const button = await browser.$('//button[@type="button"]');
// Check if the element exists in the DOM
const isExisting = await button.isExisting();
console.log("Element with attribute exists:", isExisting);
});
});
Use this approach if:
- you need complex search conditions (combinations of attributes)
- you are working with dynamic attributes (
dataattributes with variable values) - you need flexibility in searching (partial matches, start/end of string)
- CSS selectors cannot express the required logic
- you need to find an element by the absence of an attribute
DOM navigation
Using XPath, you can navigate through the DOM tree.
// Direct parent
const parentDiv = await browser.$("//input[@name='email']/..");
// Ancestor with condition
const formContainer = await browser.$("//input[@name='email']/ancestor::form[@id='registration']");
// Following sibling
const errorLabel = await browser.$(
"//input[@class='invalid']/following-sibling::span[@class='error'][1]",
);
// Preceding sibling
const label = await browser.$("//input[@name='password']/preceding-sibling::label[1]");
// All descendants
const allInputs = await browser.$$("//form[@id='checkout']//input");
// Direct children
const directChildren = await browser.$$("//ul[@class='menu']/li");
// Finding an "uncle" of the element (parent -> parent's sibling)
const siblingSection = await browser.$("//h2[text()='Контакты']/../following-sibling::section[1]");
This type of DOM tree navigation is not recommended due to its fragility, but it is possible.
XPath: indices and positions
XPath allows you to select elements by their position in the result set.
describe("XPath selector: indices and positions", () => {
it("Finding an element by index", async ({ browser }) => {
await browser.openAndWait("https://testplane.io/");
// Find the third link element in the navigation (index starts at 1)
// Selector format: (//element)[index]
const thirdLink = await browser.$("(//a)[3]");
// Wait for the element to appear and be displayed
await thirdLink.waitForDisplayed({ timeout: 5000 });
// Check if the element exists in the DOM
const isExisting = await thirdLink.isExisting();
console.log("Third element exists:", isExisting);
// Get the element's text
const text = await thirdLink.getText();
console.log("Text of the third element:", text);
});
});
Use this approach if:
- you need to access an element by its position in the result set
- you are testing pagination or lists with a specific order
- you are working with tables and need a specific row
- you need the first or last element among several identical ones
- you are testing sorting (verifying that an element is in the correct position)
Shadow DOM selectors
Shadow DOM selectors allow you to work with elements inside the Shadow DOM — an encapsulated part of the DOM tree. For example, if you have a custom element my-custom-element, you can find a button inside its Shadow DOM using shadow$("button").
// Simple access to Shadow DOM
const customElement = await browser.$("my-custom-element");
const button = await customElement.shadow$("button");
await button.click();
// Multiple elements in Shadow DOM
const slotElements = await customElement.shadow$$(".slot-item");
Use this approach if:
- you are working with Web Components and Custom Elements
- the application uses Shadow DOM to encapsulate styles
- you are testing components from third‑party libraries (Lit, Stencil, native Web Components)
- you need access to elements inside the shadow root
- you are working with a design system based on Web Components
Testing Library
Testing Library allows you to find elements the way users do — by text, element type, or other attributes that don't depend on your layout details.
To use this type of selector, you need to install Testing Library.
ByRole
getByRole is the main method in Testing Library that lets you find elements by their ARIA roles. For example, if you use the method browser.getByRole("button", { name: /submit/i }), you will find a button with text containing submit.
describe("getByRole", () => {
it("Finding a button using the getByRole method", async ({ browser }) => {
await browser.url("https://testplane.io/");
const button = await browser.getByRole("button", { name: "Get started" });
await button.click();
});
});