Locators
Introduction
Locators in CSTesting represent a way to find one or more elements on the page. You create a locator with browser.locator(selector) or browser.getByAttribute(attribute, value), then perform actions (click, type, check, etc.) or read state (textContent, getAttribute, isVisible) on the matched element(s).
By default, locators are strict: if the selector matches more than one element, actions that target a single element (like click or type) will throw. Use .first(), .last(), or .nth(index) when you expect multiple matches and want to target a specific one. Each time you use a locator for an action, the element is resolved at that moment, so if the DOM changes between calls, the new matching element is used.
CSTesting does not have built-in getByRole, getByText, getByLabel, getByPlaceholder, getByTestId, or getByTitle. You use locator(selector) (CSS or XPath) and getByAttribute(attr, value) instead. Selector shorthand (e.g. #id, name="x", data-testid="submit") makes common cases concise.
You will learn
- How to create locators (locator, getByAttribute)
- Selector syntax (CSS, XPath, shorthand)
- Handling multiple matches (first, last, nth)
- Locator actions and state
- Strictness and best practices
- What is not supported (getByRole, filter, etc.)
Quick guide
By CSS or XPath:
await browser.locator('button[type="submit"]').click();
await browser.locator('#username').type('John');
await browser.locator('[name="password"]').type('secret');
By attribute (convenience for one attribute):
await browser.getByAttribute('data-testid', 'submit').click();
await browser.getByAttribute('aria-label', 'User Name').type('John');
When multiple elements match, pick one:
await browser.locator('button').first().click();
await browser.locator('li').nth(1).click();
await browser.locator('li').last().click();
Creating locators
browser.locator(selector)
Pass a CSS selector or XPath string. CSTesting auto-detects XPath when the string starts with / or (// or ./. Everything else is treated as CSS. Before use, shorthand forms are resolved to CSS (see Selector shorthand).
const submitBtn = browser.locator('button[type="submit"]');
await submitBtn.click();
await browser.locator('#email').type('user@test.com');
await browser.locator('.btn-primary').click();
XPath examples:
await browser.locator('//button[text()="Sign in"]').click();
await browser.locator('(//div[@class="item"])[2]').click();
browser.getByAttribute(attribute, value)
Creates a locator for elements with the given attribute and value: [attribute="value"]. Useful for data-testid, aria-label, name, etc.
await browser.getByAttribute('data-testid', 'directions').click();
await browser.getByAttribute('aria-label', 'User Name').type('John');
await browser.getByAttribute('name', 'password').type('secret');
This is equivalent to browser.locator(`[attribute="value"]`) but avoids quoting and escaping in the selector string.
Selector shorthand
When you use locator(selector) or direct actions like browser.click(selector), the string is normalized: XPath is left unchanged; other forms may be converted to CSS before querying.
| Shorthand | Resolved to | Example |
|---|---|---|
#id, .class, button, etc. | Passed through as CSS | #email, .btn-primary |
name="x" | [name="x"] | name="userName" |
id="x" | [id="x"] | id="submit" |
class="x" | .x (with escaping) | class="btn primary" |
attr="value" | [attr="value"] | data-testid="submit", placeholder="Enter name" |
XPath (starts with / or (// or ./) | Unchanged | //button, (//div)[1] |
So you can write:
await browser.locator('name="userName"').type('John');
await browser.locator('data-testid="submit"').click();
await browser.locator('#login-form .btn').click();
Locator API (actions and state)
Once you have a locator, you can call:
| Method | Description |
|---|---|
| click() | Click the element |
| doubleClick(), rightClick() | Double-click or right-click |
| hover() | Hover over the element |
| type(text) | Type text into the element (input, textarea, contenteditable) |
| select(option) | Select option(s) in a <select> (e.g. { label: 'Blue' }, { value: '1' }) |
| check(), uncheck() | Check or uncheck checkbox/radio |
| dragTo(targetSelector) | Drag this element to the target |
| pressKey(key) | Press a key (e.g. Enter, Tab) |
| textContent() | Get the element's text content |
| getAttribute(name) | Get an attribute value |
| isVisible(), isDisabled(), isEditable(), isSelected() | Element state |
| first(), last(), nth(n) | When multiple match: use first, last, or nth (0-based) element |
Example:
await browser.locator('[name="userName"]').type('John');
await browser.locator('[name="password"]').type('secret');
await browser.locator('button[type="submit"]').click();
const text = await browser.locator('h1').textContent();
expect(text).toContain('Welcome');
Multiple elements: first(), last(), nth()
If a selector matches more than one element, a single-element action (click, type, etc.) will throw by default (strictness). Use .first(), .last(), or .nth(index) to target one of them.
// Exactly one button → OK
await browser.locator('button[type="submit"]').click();
// Several buttons → which one? Use first/last/nth
await browser.locator('button').first().click();
await browser.locator('button').last().click();
await browser.locator('li').nth(1).click(); // second item (0-based)
When to use: Prefer a selector that matches a single element (e.g. by id, data-testid, or a unique combination of attributes). Use first() / last() / nth() when the order is stable and you really need "first button" or "second row"; be aware that layout changes can make the wrong element match.
Locators inside frames
To interact with elements inside an iframe, get a frame handle first, then use frame.locator(selector) or frame.getByAttribute(attr, value). The same locator API applies inside the frame.
const frame = browser.frame('iframe#login');
await frame.locator('#username').type('John');
await frame.locator('button[type="submit"]').click();
See Frames for nested frames and the full frame API.
Simulating getByRole, getByLabel, getByText, getByTestId
CSTesting does not provide getByRole, getByLabel, getByText, getByPlaceholder, getByAltText, getByTitle, or getByTestId. You can get similar behavior with locator() and getByAttribute():
| Playwright-style | CSTesting equivalent |
|---|---|
| getByRole('button', { name: 'Sign in' }) | locator('button:has-text("Sign in")') not available; use locator('//button[text()="Sign in"]') (XPath) or a unique selector like locator('[aria-label="Sign in"]') or getByAttribute('aria-label', 'Sign in') |
| getByLabel('User Name') | Label is often associated with input via for/id or wrapping. Use locator('#user-name') if the input has that id, or locator('[aria-label="User Name"]') / getByAttribute('aria-label', 'User Name') |
| getByPlaceholder('name@example.com') | locator('[placeholder="name@example.com"]') or getByAttribute('placeholder', 'name@example.com') |
| getByText('Welcome, John') | No direct API. Use locator('text=Welcome, John') only if your tool supports it (CSTesting does not); use XPath e.g. locator('//*[contains(text(),"Welcome, John")]') or a more specific selector |
| getByAltText('logo') | locator('img[alt="logo"]') or getByAttribute('alt', 'logo') |
| getByTitle('Issues count') | locator('[title="Issues count"]') or getByAttribute('title', 'Issues count') |
| getByTestId('submit') | locator('[data-testid="submit"]') or getByAttribute('data-testid', 'submit') |
Examples:
// By data-testid (test id)
await browser.getByAttribute('data-testid', 'directions').click();
// By placeholder
await browser.locator('[placeholder="name@example.com"]').type('user@test.com');
// By label text (if input has aria-label)
await browser.getByAttribute('aria-label', 'Password').type('secret');
// By button text (XPath)
await browser.locator('//button[text()="Sign in"]').click();
Strictness
Locators are strict: operations that need a single target (click, type, getAttribute, etc.) expect exactly one matching element. If zero or two or more match, CSTesting throws.
- One match — Action runs on that element.
- Zero matches — Throws (element not found).
- Multiple matches — Throws unless you narrow with .first(), .last(), or .nth(index).
So this will throw if there are several buttons:
await browser.locator('button').click(); // strict: must be exactly one
This is OK when you explicitly choose one:
await browser.locator('button').first().click();
Best practice: write selectors that uniquely identify the element (id, data-testid, or a stable combination of attributes) instead of relying on order with nth().
What is not supported (current version)
| Feature | Supported? | Workaround |
|---|---|---|
| locator(selector) (CSS/XPath + shorthand) | Yes | — |
| getByAttribute(attr, value) | Yes | — |
| first(), last(), nth(n) | Yes | — |
| Strictness (one match or throw) | Yes | — |
| getByRole(role, options) | No | Use locator with [role="button"], or getByAttribute('aria-label', name). For "name" use XPath e.g. //button[text()="Sign in"]. |
| getByText(text), getByLabel, getByPlaceholder | No | Use locator or getByAttribute: e.g. [placeholder="..."], [aria-label="..."], XPath for text. |
| getByAltText, getByTitle | No | Use locator('img[alt="..."]') or getByAttribute('alt', '...'), same for title. |
| getByTestId | No | Use getByAttribute('data-testid', 'id') or locator('[data-testid="id"]'). |
| locator.filter({ hasText }), has / hasNot | No | Narrow with a more specific selector or XPath (e.g. ancestor + text). |
| locator.and(), locator.or() | No | Use a single selector or XPath that expresses the condition. |
| locator.count(), locator.all() | No | Use evaluate to run document.querySelectorAll(selector).length or iterate in the page. |
| locator.evaluateAll(fn) | No | Use browser.evaluate() with a function that runs in the page and returns an array. |
| Custom test id attribute (e.g. data-pw) | No | Use getByAttribute('data-pw', 'id') or locator('[data-pw="id"]'). |
| Shadow DOM | Not documented | Queries run in the main document (or frame document). Shadow roots may require evaluate to pierce. |
| Chaining locators (e.g. dialog.locator(button)) | No | Use a combined selector (e.g. #dialog button) or XPath. |
Summary
- Use browser.locator(selector) with CSS or XPath, and browser.getByAttribute(attribute, value) for attribute-based lookup.
- Selector shorthand is supported:
#id,.class,name="x",attr="value", and XPath unchanged. - Locators support click, type, select, check, uncheck, hover, dragTo, pressKey, textContent, getAttribute, isVisible, isDisabled, isEditable, isSelected.
- When multiple elements match, use .first(), .last(), or .nth(n) so actions target a single element.
- Locators are strict: single-element actions require exactly one match (or use first/last/nth).
- There are no getByRole, getByText, getByLabel, getByTestId, or filter APIs; use locator and getByAttribute (and XPath for text) instead.