Skip to content

radqut/testing-library-locators

Repository files navigation

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)
  • Extendable - 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-locators
yarn add -D testing-library-locators
pnpm add -D testing-library-locators

Peer dependencies

Please note that @testing-library/dom and @testing-library/user-event are peer dependencies, meaning you should 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("user@example.com");

// 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 clicks

getByRole

Creates a way to locate 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 // Take care because 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 a element (normally an ) 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, 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 sugar 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 sugar for nth(-1).

<input /> <input /> <input />
page.getByRole("textbox").last();

has

This options narrows down the selector to match elements that contain other elements matching 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 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(); // ✅ null

elements

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(); // ✅ HTMLInputElement

findAll

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 signal 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

Click on 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

Hover an element.

await page.getByRole("button").hover();

See also

unhover

Unhover an element.

await page.getByRole("button").unhover();

See also

clear

Clears the input element content.

await page.getByRole("textbox").clear();

See also

type

Sets the value of the current input, textarea or contenteditable element.

await page.getByRole("textbox").type("Mr. Bean");

See also

selectOptions

Select the given options in an HTMLSelectElement 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

Deselect the given options in an HTMLSelectElement 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 built-in locators API by defining an object of locator factories. These methods will exist as methods on the page object and any created locator.

These locators can be useful if built-in locators are not enough. For example, when you use a custom framework for your UI.

import { buildQueries } from "@testing-library/dom";
import { locators, createQuerySelector, type Locator } from "testing-library-locators";

// Use the navite buildQueryies 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
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();

License

MIT © radqut

Acknowledgments

Resources