# Selectors & Locators
CraftDriver provides multiple ways to locate elements on the page.
## CSS Selectors
The most common way to find elements is using CSS selectors as strings:
> String selectors are always parsed as CSS selectors. Craftdriver does not
> support Playwright-style selector strings such as `text=Save` or `role=button`.
> For semantic queries, use `By.*` or `browser.getBy*()`.
```typescript
// By ID
await browser.click('#submit-button');
// By class
await browser.click('.btn-primary');
// By tag and attribute
await browser.click('input[type="email"]');
// Complex selectors
await browser.click('form.login #username');
await browser.click('ul.nav > li:first-child a');
```
## By Locators
The `By` helper provides semantic locator strategies:
```typescript
import { By } from 'craftdriver';
```
### By.css(selector)
Locate by CSS selector (equivalent to passing a string).
```typescript
browser.find(By.css('#my-button'));
```
### By.id(id)
Locate by element ID.
```typescript
browser.find(By.id('username'));
// Equivalent to: browser.find('#username')
```
### By.className(name)
Locate by CSS class name.
```typescript
browser.find(By.className('btn-primary'));
// Equivalent to: browser.find('.btn-primary')
```
### By.name(name)
Locate by the `name` attribute (matches the canonical Selenium
`By.name` locator).
```typescript
browser.find(By.name('email'));
// Finds:
```
### By.tagName(tag)
Locate by HTML tag name.
```typescript
browser.find(By.tagName('h1'));
// Finds the first
element
```
### By.attr(name, value)
Locate by an arbitrary attribute and value.
```typescript
browser.find(By.attr('role', 'dialog'));
// Equivalent to: browser.find('[role="dialog"]')
```
### By.dataAttr(name, value)
Locate by a `data-*` attribute. Pass the suffix only — `data-` is added
for you.
```typescript
browser.find(By.dataAttr('state', 'open'));
// Equivalent to: browser.find('[data-state="open"]')
```
### By.aria(name, value)
Locate by an `aria-*` attribute. Pass the suffix only — `aria-` is added
for you. Use `By.role()` / `getByRole()` for role + accessible-name
matching; use this helper when you need a specific ARIA state or
property.
```typescript
browser.find(By.aria('expanded', 'true'));
// Equivalent to: browser.find('[aria-expanded="true"]')
```
### By.testId(value)
Locate by `data-testid` attribute.
```typescript
browser.find(By.testId('submit-button'));
// Equivalent to: browser.find('[data-testid="submit-button"]')
```
### By.text(text, options?)
Locate by visible text content. Defaults: `exact: true`,
`caseSensitive: true`, `trim: true`.
```typescript
// Exact match (whitespace-normalised)
browser.find(By.text('Submit Order'));
// Substring match — equivalent to By.partialText('Submit')
browser.find(By.text('Submit', { exact: false }));
// Case-insensitive
browser.find(By.text('submit order', { caseSensitive: false }));
```
> **`By.text` and `getByText` are the same vocabulary.**
> `getByText(text, opts)` is just `By.text(text, opts)`. Pass
> `{ exact: false }` on either to get substring matching;
> `By.partialText` remains as the lower-level entry point if you prefer
> to be explicit.
### By.partialText(substring, options?)
Locate the innermost element whose visible text contains `substring`.
Equivalent to `By.text(substring, { exact: false })`. Defaults:
`caseSensitive: true`, `trim: true`.
```typescript
browser.find(By.partialText('Submit'));
browser.find(By.partialText('error', { caseSensitive: false }));
```
### By.altText(text, options?)
Locate ``, ``, or `` by `alt` text.
Defaults: `exact: true`.
```typescript
browser.find(By.altText('Company logo'));
browser.find(By.altText('logo', { exact: false }));
```
### By.title(text, options?)
Locate by the `title` attribute (browser tooltip text). Defaults:
`exact: true`.
```typescript
browser.find(By.title('Open in new tab'));
browser.find(By.title('Open', { exact: false }));
```
### By.xpath(expression)
Locate using XPath expression.
```typescript
browser.find(By.xpath('//button[@data-testid="submit"]'));
browser.find(By.xpath('//div[contains(@class, "error")]'));
```
## Playwright-Style Locators
CraftDriver also supports Playwright-style semantic locators directly on the browser:
### getByRole(role, options?)
Locate by ARIA role.
```typescript
browser.getByRole('button', { name: 'Submit' });
browser.getByRole('textbox', { name: 'Email' });
browser.getByRole('checkbox', { name: 'Remember me' });
browser.getByRole('link', { name: 'Learn more' });
```
Options:
| Option | Type | Default | Description |
| --------------- | --------- | ------- | ----------------------------------------------------- |
| `name` | `string` | — | Accessible name (`aria-label` or visible text). |
| `exact` | `boolean` | `true` | If `false`, substring-match the accessible name. |
| `includeHidden` | `boolean` | `false` | Match elements with `hidden` or `aria-hidden="true"`. |
```typescript
// Default: skip elements marked hidden / aria-hidden
browser.getByRole('button', { name: 'Close' });
// Include hidden elements (e.g., off-canvas menus, collapsed sections)
browser.getByRole('button', { name: 'Close', includeHidden: true });
```
### getByText(text, options?)
Locate by visible text.
```typescript
// Exact match (default)
browser.getByText('Welcome to our site');
// Partial match
browser.getByText('Welcome', { exact: false });
```
> **Heads-up for Playwright users.** Craftdriver's `getByText` defaults
> to **exact** matching, while Playwright's defaults to substring. Pass
> `{ exact: false }` for substring behaviour.
### getByLabel(text, options?)
Locate form controls by their associated label.
```typescript
browser.getByLabel('Email address');
browser.getByLabel('Password');
```
### getByPlaceholder(text, options?)
Locate inputs by placeholder text.
```typescript
browser.getByPlaceholder('Enter your email');
browser.getByPlaceholder('Search...');
```
### getByTestId(testId)
Locate by `data-testid` attribute.
```typescript
browser.getByTestId('submit-button');
browser.getByTestId('user-profile');
```
## Selector Best Practices
### Prefer Stable Selectors
```typescript
// ✅ Good - specific and stable
browser.find('#login-button');
browser.find('[data-testid="submit"]');
browser.getByRole('button', { name: 'Login' });
// ❌ Avoid - fragile, depends on structure
browser.find('div > div > button:nth-child(3)');
browser.find('.btn.mt-4.px-6'); // CSS utility classes change
```
### Use Test IDs for Complex UIs
Add `data-testid` attributes to elements that are hard to select:
```html
```
```typescript
await browser.find('[data-testid="checkout-button"]').click();
// or
await browser.getByTestId('checkout-button').click();
```
### Semantic Locators for Accessibility
Using role-based locators ensures your UI is accessible:
```typescript
// This only works if the button is properly labeled
browser.getByRole('button', { name: 'Add to cart' });
```
## Examples
### Finding Multiple Elements
```typescript
// Click the first matching element
await browser.find('.product-card .add-to-cart').click();
// For multiple elements, use specific selectors
await browser.find('.product-card:nth-child(1) .add-to-cart').click();
await browser.find('.product-card:nth-child(2) .add-to-cart').click();
```
## Locators (lazy & chainable)
`browser.locator(selector)` returns a `Locator` — a lazy, re-resolving handle that
supports composition, filtering, and indexed access.
Use `find()` for a single one-shot element, `findAll()` for the simple array case,
and `locator()` when you need composition or want to pick from a list.
```typescript
import { Browser } from 'craftdriver';
// Count matching elements
const n = await browser.locator('.product').count();
// Pick by index (0-based)
await browser.locator('.buy-btn').first().click();
await browser.locator('.buy-btn').last().click();
await browser.locator('.buy-btn').nth(2).click();
// Filter by text content
await browser.locator('.product').filter({ hasText: 'Pro' }).locator('.buy-btn').click();
// Filter by presence of a child locator
const hasPromo = browser.locator('.badge');
const promoCards = browser.locator('.card').filter({ has: hasPromo });
await promoCards.first().click();
// Chain into a child element
await browser.locator('.card').nth(0).locator('button').click();
// Get all matching elements as snapshot handles
const handles = await browser.locator('.product').all();
const texts = await Promise.all(handles.map(h => h.text()));
// Simple array shortcut (no filtering)
const allButtons = await browser.findAll('.buy-btn');
```
### Assertions on locators
```typescript
await browser.locator('#result').expect().toHaveText('Done');
await browser.locator('.error').expect().not.toBeVisible();
```
### Form Controls
```typescript
// By label
await browser.getByLabel('Email').fill('test@example.com');
await browser.getByLabel('Password').fill('secret');
// By placeholder
await browser.getByPlaceholder('Search products...').fill('laptop');
// By role
await browser.getByRole('button', { name: 'Search' }).click();
```
### Navigation Links
```typescript
await browser.getByRole('link', { name: 'Products' }).click();
await browser.getByText('Contact Us').click();
await browser.find('nav a[href="/about"]').click();
```
### Buttons by Text
```typescript
await browser.getByRole('button', { name: 'Submit' }).click();
await browser.getByText('Cancel').click();
await browser.find('button:contains("Save")').click();
```