A lightweight, robust package that decouples your UI selectors from your test code. By externalizing locators into a central JSON repository, you make your test automation framework cleaner, easier to maintain, and accessible to non-developers. Supports both Playwright (web) and Appium/WebdriverIO (mobile) through a unified API.
Install the package via your preferred package manager:
npm i @civitas-cerebrum/element-repositoryPeer Dependencies:
For web testing, install @playwright/test or playwright. For mobile/platform testing, install webdriverio.
- Zero Hardcoded Selectors: Keep your Page Objects and Step Definitions completely free of complex DOM queries.
- Platform-Agnostic Element API: A unified
Elementinterface with interaction, state, extraction, querying, and waiting methods that work identically across Playwright and WebDriverIO. - Dynamic Parsing: Automatically converts your JSON configuration into platform-native selectors — CSS, XPath, ID, Text, Test ID, Role, Placeholder, and Label for web; Accessibility ID, UIAutomator, Predicate, Class Chain, and more for mobile.
- Smart Locators: Built-in methods for handling arrays, randomized element selection (great for catalog/PLP testing), text-filtering, attribute-filtering, and visibility checks.
- Soft Waiting: Seamlessly waits for elements to attach and become visible before returning them to prevent flake.
Create a JSON file in your project to hold your selectors. The file must adhere to the standard schema:
locators.json
{
"pages": [
{
"name": "HomePage",
"elements": [
{
"elementName": "search-input",
"selector": { "css": "input[name='search']" }
},
{
"elementName": "submit-button",
"selector": { "id": "btn-submit" }
}
]
},
{
"name": "ProductList",
"elements": [
{
"elementName": "product-cards",
"selector": { "xpath": "//article[@class='product']" }
}
]
}
]
}Use the platform field to define platform-specific selectors for the same page. Pages without a platform field default to web.
{
"pages": [
{
"name": "LoginPage",
"platform": "web",
"elements": [
{ "elementName": "submitButton", "selector": { "css": "button.web-submit" } }
]
},
{
"name": "LoginPage",
"platform": "android",
"elements": [
{ "elementName": "submitButton", "selector": { "accessibility id": "SubmitBtn" } }
]
},
{
"name": "LoginPage",
"platform": "ios",
"elements": [
{ "elementName": "submitButton", "selector": { "predicate": "label == \"Submit\"" } }
]
}
]
}The platform field on each page object determines which selector format is used. If platform is omitted, it defaults to web.
| Key | Resolves To | Example |
|---|---|---|
css |
css=<value> |
"css": "button.primary" |
xpath |
xpath=<value> |
"xpath": "//button[@id='submit']" |
id |
#<value> |
"id": "btn-submit" |
text |
text=<value> |
"text": "Submit" |
testid |
[data-testid='<value>'] |
"testid": "login-btn" |
role |
[role='<value>'] |
"role": "button" |
placeholder |
[placeholder='<value>'] |
"placeholder": "Search..." |
label |
[aria-label='<value>'] |
"label": "Close" |
Note: The
testidkey uses the standarddata-testidattribute.
| Key | camelCase Alias | Resolves To | Example |
|---|---|---|---|
accessibility id |
accessibilityId |
~<value> |
"accessibility id": "LoginBtn" |
xpath |
— | <value> (raw) |
"xpath": "//android.widget.Button" |
id |
— | #<value> |
"id": "submit-btn" |
css |
— | css=<value> |
"css": "button.primary" |
uiautomator |
androidUIAutomator |
android=<value> |
"uiautomator": "new UiSelector().text(\"Go\")" |
predicate |
iOSNsPredicateString |
-ios predicate string:<value> |
"predicate": "label == \"Login\"" |
class chain |
iOSClassChain |
-ios class chain:<value> |
"class chain": "**/XCUIElementTypeButton" |
class name |
className |
<value> (raw) |
"class name": "android.widget.EditText" |
tag name |
tagName |
<value> (raw) |
"tag name": "button" |
name |
— | <value> (raw) |
"name": "username" |
android data matcher |
androidDataMatcher |
-android datamatcher:<value> |
"androidDataMatcher": "{\"name\":\"Title\"}" |
android view matcher |
androidViewMatcher |
-android viewmatcher:<value> |
"androidViewMatcher": "{\"id\":\"btn\"}" |
android view tag |
androidViewTag |
-android viewtag:<value> |
"androidViewTag": "my-tag" |
text |
— | Platform-specific | "text": "Submit" |
Note: The
textkey resolves toandroid=new UiSelector().text("...")on Android,-ios predicate string:label == "..."on iOS, and the raw value on other platforms.Note: All strategy keys that contain spaces also accept a camelCase alias (e.g.,
"accessibilityId"instead of"accessibility id"). Both forms produce identical selectors.
You can initialize the ElementRepository either by passing the file path to your JSON, or by passing the parsed JSON object directly.
import { ElementRepository } from '@civitas-cerebrum/element-repository';
// Option A: Pass the path to your JSON (relative to your project root)
const repo = new ElementRepository('tests/data/locators.json', 15000);
// Option B: Import the JSON directly (requires resolveJsonModule in tsconfig)
import locatorData from '../data/locators.json';
const repo = new ElementRepository(locatorData, 15000);
// Option C: Platform-specific repository (for mobile/Appium)
const androidRepo = new ElementRepository('tests/data/locators.json', 15000, 'android');
const iosRepo = new ElementRepository(locatorData, 15000, 'ios');The third parameter (platform) defaults to 'web'. When set to a non-web platform, getSelector() automatically returns Appium-formatted selectors, and get() returns PlatformElement wrappers instead of WebElement.
The repository exposes clean, asynchronous methods that return unified Element objects, ready for interaction regardless of the underlying platform.
test('Search and select random product', async ({ page }) => {
await page.goto('/');
// 1. Get a standard element
const searchInput = await repo.get(page, 'HomePage', 'search-input');
await searchInput.fill('Trousers');
const submitBtn = await repo.get(page, 'HomePage', 'submit-button');
await submitBtn.click();
// 2. Select a random element from a list
const randomProduct = await repo.getRandom(page, 'ProductList', 'product-cards');
await randomProduct?.click();
// 3. Find a specific element by text within a list
const specificProduct = await repo.getByText(page, 'ProductList', 'product-cards', 'Blue Chinos');
await specificProduct?.click();
// 4. Find an element by HTML attribute
const activeCard = await repo.getByAttribute(page, 'ProductList', 'product-cards', 'data-status', 'active');
await activeCard?.click();
// 5. Get a specific element by index
const thirdProduct = await repo.getByIndex(page, 'ProductList', 'product-cards', 2);
await thirdProduct?.click();
// 6. Get the first visible element (filters out hidden duplicates)
const visibleModal = await repo.getVisible(page, 'HomePage', 'modal');
await visibleModal?.click();
// 7. Filter elements by ARIA role
const navLink = await repo.getByRole(page, 'HomePage', 'nav-links', 'link');
await navLink?.click();
});Returns a single Element. For web, waits for the selector to attach to the DOM based on your configured timeout. For platform, returns a lazy PlatformElement that resolves on interaction.
Returns an array of Element objects. Useful when you need to iterate over multiple elements.
Counts the matching elements and randomly selects one. Safely waits for the specific randomized element to become visible.
Returns the first Element matching the mapped selector that also contains the desiredText.
Returns the first Element whose HTML attribute matches the given value. Iterates through all matching elements and checks the specified attribute.
Options:
exact(boolean, default:true) — Iftrue, requires an exact attribute match. Iffalse, matches when the attribute contains the value.strict(boolean, default:false) — Iftrue, throws an error when no matching element is found.
// Exact match (default)
const active = await repo.getByAttribute(page, 'Dashboard', 'cards', 'data-status', 'active');
// Partial (contains) match
const dashLink = await repo.getByAttribute(page, 'Nav', 'links', 'href', '/dashboard', { exact: false });Returns the Element at the specified zero-based index from the list of matching elements. Returns null (or throws in strict mode) if the index is out of bounds.
const thirdCard = await repo.getByIndex(page, 'ProductList', 'product-cards', 2);Returns the first visible element matching the selector. Unlike get(), which returns the locator after a basic wait, this method explicitly filters to only visible elements — useful when hidden duplicates exist in the DOM.
const visibleModal = await repo.getVisible(page, 'Dashboard', 'modal');Filters elements by their explicit role HTML attribute and returns the first match.
const navButton = await repo.getByRole(page, 'Header', 'navItems', 'button');Returns a platform-appropriate selector string. For web platforms, returns Playwright-formatted selectors (e.g., "css=input[name='search']"). For non-web platforms (android, ios), returns Appium-formatted selectors (e.g., "~LoginBtn", "android=new UiSelector().text(\"Submit\")"). This is a synchronous method useful for debugging, custom logging, or passing raw selector strings directly into native APIs.
// Web
const webRepo = new ElementRepository(data);
webRepo.getSelector('LoginPage', 'submitButton'); // "css=button.web-submit"
// Android
const androidRepo = new ElementRepository(data, undefined, 'android');
androidRepo.getSelector('LoginPage', 'submitButton'); // "~SubmitBtn"Returns the raw selector strategy and value as an object, without any platform-specific formatting. Useful when you need the original strategy name and value from the JSON.
const { strategy, value } = repo.getSelectorRaw('HomePage', 'search-input');
// { strategy: 'css', value: "input[name='search']" }Updates the default timeout (in milliseconds) for all subsequent element retrievals.
All get* methods that return Element | null accept an optional strict parameter (default: false):
strict: false— logs a warning and returnsnullwhen no match is found.strict: true— throws anErrorwhen no match is found.
// Non-strict (default): returns null on failure
const card = await repo.getByText(page, 'ProductList', 'product-cards', 'Missing Item');
// card === null
// Strict: throws an error on failure
const card = await repo.getByText(page, 'ProductList', 'product-cards', 'Missing Item', true);
// Error: Element 'product-cards' on 'ProductList' with text "Missing Item" not found.All get* methods return an Element — a platform-agnostic interface that wraps either a Playwright Locator (via WebElement) or a WebDriverIO element (via PlatformElement). You can interact with elements directly without caring about the underlying driver.
| Method | Description |
|---|---|
click() |
Clicks the element. |
fill(text) |
Clears the input and fills it with the given text. |
clear() |
Clears the element's value. |
check() |
Checks a checkbox or radio button. |
uncheck() |
Unchecks a checkbox. |
hover() |
Hovers over the element. |
doubleClick() |
Double-clicks the element. |
scrollIntoView() |
Scrolls the element into the visible area. |
pressSequentially(text, delay?) |
Types text one character at a time. |
setInputFiles(filePath) |
Sets the value of a file input. Web only. |
dispatchEvent(event) |
Dispatches a DOM event on the element. Web only. |
| Method | Description |
|---|---|
isVisible() |
Returns true if the element is visible. |
isEnabled() |
Returns true if the element is enabled. |
isChecked() |
Returns true if a checkbox/radio is checked. |
| Method | Description |
|---|---|
textContent() |
Returns the text content, or null if empty. |
getAttribute(name) |
Returns the value of an HTML attribute, or null. |
inputValue() |
Returns the current value of an input/textarea/select. |
| Method | Description |
|---|---|
locateChild(selector) |
Locates a descendant element matching the selector. |
count() |
Returns the number of matched elements. |
all() |
Returns an array of all matched elements. |
first() |
Returns the first matched element. |
nth(index) |
Returns the element at the given zero-based index. |
filter({ hasText }) |
Filters matched elements by text content. |
| Method | Description |
|---|---|
waitFor(options?) |
Waits for the element to reach a state: "visible" (default), "hidden", "attached", or "detached". Accepts an optional timeout in ms. |
Use the ElementType enum and type guards to narrow to the concrete implementation when you need driver-specific access:
import { Element, WebElement, PlatformElement, isWeb, isPlatform } from '@civitas-cerebrum/element-repository';
const el = await repo.get(page, 'LoginPage', 'submitButton');
if (isWeb(el)) {
// el is WebElement — access the underlying Playwright Locator
await el.locator.click();
}
if (isPlatform(el)) {
// el is PlatformElement — access the WebDriverIO driver and selector
console.log(el.selector); // the Appium selector string
await el.click();
}// Primary class
export { ElementRepository } from '@civitas-cerebrum/element-repository';
// Element types and type guards
export { Element, WebElement, PlatformElement, ElementType, isWeb, isPlatform };
// Schema types
export { Selector, ElementDefinition, PageObject, PageRepository, Page };
// Formatter type
export type { SelectorFormatter };
// Utility functions
export { pickRandomIndex, pickRandomMember };MIT