testing-library-locators
v0.2.1
Published
> Chainable and reusable locators for Testing Library
Maintainers
Readme
Testing Library Locators
Chainable and reusable locators for Testing Library
A lightweight library that provides Testing Library-style query methods (like getByRole, getByText, getByLabelText) with chainable locator patterns, inspired by Vitest Browser Mode Locators and Playwright's Locator API.
Features
- Fully compatible with Testing Library - Use familiar queries like
getByRole,getByLabelText,getByText - Chainable API - Compose complex reusable selectors like
getByRole("list").getByRole("listitem").nth(2) - Extensible - Create custom locators to fit your project's needs
- Lightweight - Small bundle size with minimal overhead
- TypeScript support - Fully typed API with excellent IntelliSense
Installation
npm add -D testing-library-locatorsyarn add -D testing-library-locatorspnpm add -D testing-library-locatorsPeer dependencies
Please note that @testing-library/dom and @testing-library/user-event are peer dependencies, so ensure they are installed before installing testing-library-locators.
Quick Start
import { page } from "testing-library-locators";
// Find elements using accessible queries
const submitButton = page.getByRole("button", { name: "Submit" });
const emailInput = page.getByLabelText("Email address");
const welcomeText = page.getByText("Welcome back!");
// Chain locators to narrow down selection
const deleteButton = page.getByRole("article").getByRole("button", { name: "Delete" });
// Interact with elements
await submitButton.click();
await emailInput.type("[email protected]");
// Assert element presence
expect(submitButton.element()).toBeInTheDocument();
expect(welcomeText.query()).not.toBeInTheDocument();Core Concepts
Locators
A locator is a representation of an element or a number of elements. Locators are lazy - they don't find elements immediately but wait until an action is performed.
// This doesn't find the element yet
const button = page.getByRole("button", { name: "Click" });
// Element is located when you perform an action
await button.click(); // ✅ Finds element and clicksgetByRole
Locates an element by its ARIA role, ARIA attributes, and accessible name.
<h3>Sign up</h3>
<label>
Login
<input type="text" />
</label>
<label>
Password
<input type="password" />
</label>
<br />
<button>Submit</button>expect(page.getByRole("heading", { name: "Sign up" }).element()).toBeInTheDocument();
await page.getByRole("textbox", { name: "Login" }).type("admin");
await page.getByRole("textbox", { name: "Password" }).type("admin");
await page.getByRole("button", { name: /submit/i }).click();See also
getByLabelText
Creates a locator capable of finding an element that has an associated label.
// for/htmlFor relationship between label and form element id
<label for="username-input">Username</label>
<input id="username-input" />
// The aria-labelledby attribute with form elements
<label id="username-label">Username</label>
<input aria-labelledby="username-label" />
// Wrapper labels
<label>Username <input /></label>
// Wrapper labels where the label text is in another child element
<label>
<span>Username</span>
<input />
</label>
// aria-label attributes - Note that this is not a label that users can see on the page, // so the purpose of your input
must be obvious to visual users.
<input aria-label="Username" />page.getByLabelText("Username");See also
getByPlaceholderText
Creates a locator capable of finding an element that has the specified placeholder attribute.
<input placeholder="Username" />page.getByPlaceholderText("Username");See also
getByText
Creates a locator capable of finding an element that contains the specified text.
<a href="/about">About ℹ️</a>page.getByText(/about/i);See also
getByDisplayValue
Creates a locator capable of finding the input, textarea, or select element that has the matching display value.
<input type="text" id="lastName" value="Norris" />page.getByDisplayValue("Norris");See also
getByAltText
Creates a locator capable of finding an element (normally an <img>) that has the given alt text.
<img alt="Incredibles 2 Poster" src="/incredibles-2.png" />page.getByAltText(/incredibles.*? poster/i);See also
getByTitle
Creates a locator capable of finding an element that has the specified title attribute.
<span title="Delete" id="2"></span>page.getByTitle("Delete");See also
getByTestId
Creates a locator capable of finding an element that matches the specified test id attribute.
<div data-testid="custom-element" />page.getByTestId("custom-element");See also
nth
This method returns a new locator that matches only a specific index within a multi-element query result. It's zero-based, so nth(0) selects the first element.
<div aria-label="one"><input /><input /><input /></div>
<div aria-label="two"><input /></div>page.getByRole("textbox").nth(0); // ✅
page.getByRole("textbox").nth(4); // ❌first
This method returns a new locator that matches only the first index of a multi-element query result. It is shorthand for nth(0).
<input /> <input /> <input />page.getByRole("textbox").first();last
This method returns a new locator that matches only the last index of a multi-element query result. It is shorthand for nth(-1).
<input /> <input /> <input />page.getByRole("textbox").last();has
This option narrows down the selector to match elements that contain other elements matching the provided locator.
<article>
<div>First</div>
</article>
<article>
<div>Second</div>
</article>page.getByRole("article").has(page.getByText("First")); // ✅not
This option narrows down the selector to match elements that do not contain other elements matching the provided locator.
<article>
<div>First</div>
</article>
<article>
<div>Second</div>
</article>page.getByRole("article").not.has(page.getByText("First")); // ✅element
This method returns a single element matching the locator's selector. If no element matches the selector, an error is thrown.
<div>Hello World</div>page.getByText("Hello World").element(); // ✅ HTMLDivElement
page.getByText("Hello Everyone").element(); // ❌query
This method returns a single element matching the locator's selector or null if no element is found. If multiple elements match the selector, this method will throw an error.
<div>Hello World</div>page.getByText("Hello World").query(); // ✅ HTMLDivElement
page.getByText("Hello Everyone").query(); // ✅ nullelements
This method returns an array of elements matching the locator's selector.
This function never throws an error. If there are no elements matching the selector, this method will return an empty array.
<div>Hello <span>World</span></div>
<div>Hello</div>page.getByText("Hello World").elements(); // ✅ [HTMLDivElement]
page.getByText("Hello Everyone").elements(); // ✅ []find
This method returns a promise that resolves to the first element matching the locator's selector.
<input />await page.getByRole("textbox").find(); // ✅ HTMLInputElementfindAll
This method returns a promise that resolves to all elements matching the locator's selector.
<input /> <input /> <input />await page.getByRole("textbox").findAll(); // ✅ [HTMLInputElement, HTMLInputElement, HTMLInputElement]Methods
debug
This method prints a single element matching the locator's selector.
<input />page.getByRole("textbox").debug(); // <input />See also
setup
This method allows you to configure an instance of userEvent.
await page.setup({ delay: 200 }).getByRole("button").click();See also
click
Clicks an element.
await page.getByRole("button").click();See also
dblClick
Triggers a double click event on an element.
await page.getByRole("button").dblClick();See also
tripleClick
Triggers a triple click event on an element.
await page.getByRole("button").tripleClick();See also
hover
Hovers an element.
await page.getByRole("button").hover();See also
unhover
Unhovers an element.
await page.getByRole("button").unhover();See also
clear
Clears the input value.
await page.getByRole("textbox").clear();See also
type
Types text into an input, textarea, or contenteditable element.
await page.getByRole("textbox").type("Mr. Bean");See also
selectOptions
Selects options in a <select> element or listbox.
<select multiple>
<option value="1">A</option>
<option value="2">B</option>
<option value="3">C</option>
</select>await page.getByRole("listbox").selectOptions(["1", "C"]);See also
deselectOptions
Deselects options in a <select> element or listbox.
<select multiple>
<option value="1">A</option>
<option value="2" selected>B</option>
<option value="3">C</option>
</select>await page.getByRole("listbox").deselectOptions("2");See also
upload
<div>
<label htmlFor="file-uploader">Upload file:</label>
<input id="file-uploader" type="file" />
</div>const file = new File(["hello"], "hello.png", { type: "image/png" });
await page.getByLabelText(/upload file/i).upload(file);See also
Assertions
const button = page.getByRole("button", { name: "Click me" });
await expect(button.find()).resolves.toBeInTheDocument();
expect(button.element()).toBeInTheDocument();
expect(button.query()).not.toBeInTheDocument();Advanced Usage
Custom Locators
You can extend the built-in locators API by defining an object of locator factories. These methods will be available on the page object and any created locator.
These locators can be useful when built-in locators are not enough, for example, when using a custom UI framework.
import { buildQueries } from "@testing-library/dom";
import { locators, createQuerySelector, type Locator } from "testing-library-locators";
// Use the native buildQueries of the DOM Testing Library to create custom queries
const queryAllByDataCy = (container: HTMLElement, id: Matcher, options?: MatcherOptions | undefined) =>
queryHelpers.queryAllByAttribute("data-cy", container, id, options);
const getMultipleError = (_c: Element | null, dataCyValue: string) =>
`Found multiple elements with the data-cy attribute of: ${dataCyValue}`;
const getMissingError = (_c: Element | null, dataCyValue: string) =>
`Unable to find an element with the data-cy attribute of: ${dataCyValue}`;
const queries = [queryAllByDataCy, ...buildQueries(queryAllByDataCy, getMultipleError, getMissingError)];
// Creates a new query selector factory
const DataCyQuerySelector = createQuerySelector(queries);
// Extends the locators
locators.extend({
getByDataCy(dataCyValue: string) {
return new DataCyQuerySelector(this, dataCyValue);
},
});
// For types
declare module "testing-library-locators" {
interface LocatorSelectors {
getByDataCy<T extends HTMLElement = HTMLElement>(dataCyValue: string): Locator<T>;
}
}<button data-cy="submit">Submit</button>page.getByDataCy("submit");Complex Selectors
Build reusable, complex selectors:
// Reusable component locator
function getListboxOption(name: string) {
return page.getByRole('listbox').getByRole('option', { name }))
}
// Use in tests
await getListboxOption("First").click();Vitest
If you use vitest and @testing-library/jest-dom, you can add an extension for better locator support and built-in retry-ability.
Setup
Import the vitest extension in your vitest setup file:
// vitest.setup.ts
import "testing-library-locators/vitest";Usage
expect.element is based on expect.poll(() => element) from vitest and waitFor from @testing-library/dom. It works in exactly the same way.
import { page } from "testing-library-locators";
const promise = new Promise((res) => setTimeout(res, 500));
const TestComponent = () => {
use(promise);
return <button>Click me</button>;
};
render(
<Suspense>
<TestComponent />
</Suspense>,
);
await expect.element(page.getByRole("button")).toBeInTheDocument();We recommend to always use expect.element when working with page.getBy* locators to reduce test flakiness.
Note that expect.element accepts options from the waitFor from @testing-library/dom as a second option.
License
MIT © radqut
Acknowledgments
- Based on Testing Library principles
- Inspired by Vitest Browser Mode Locators
- Influenced by Playwright's locator API
