or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

browser-contexts.mdbrowser-management.mdelement-handling.mdindex.mdinput-simulation.mdlocators-selectors.mdnetwork-management.mdpage-interaction.md

locators-selectors.mddocs/

0

# Locators and Selectors

1

2

Advanced element location strategies including CSS selectors, XPath, text content, custom query handlers, and the modern Locator API.

3

4

## Capabilities

5

6

### Locator Interface

7

8

Modern element location API providing chainable, flexible element targeting with built-in waiting and error handling.

9

10

```typescript { .api }

11

interface Locator<T> {

12

/** Click the located element */

13

click(options?: LocatorClickOptions): Promise<void>;

14

/** Fill input with value */

15

fill(value: string, options?: LocatorFillOptions): Promise<void>;

16

/** Focus the element */

17

focus(options?: LocatorOptions): Promise<void>;

18

/** Hover over the element */

19

hover(options?: LocatorHoverOptions): Promise<void>;

20

/** Scroll element into view */

21

scroll(options?: LocatorScrollOptions): Promise<void>;

22

/** Select options from select element */

23

select(values: string | string[], options?: LocatorOptions): Promise<void>;

24

/** Take screenshot of element */

25

screenshot(options?: LocatorScreenshotOptions): Promise<Buffer>;

26

/** Wait for element to exist and return it */

27

wait(options?: LocatorWaitOptions): Promise<T>;

28

/** Wait for element to be in specified state */

29

waitFor(options?: LocatorWaitOptions): Promise<void>;

30

/** Transform locator result */

31

map<U>(mapper: (value: T) => Promise<U>): Locator<U>;

32

/** Filter locator results */

33

filter<U extends T>(predicate: (value: T) => Promise<boolean>): Locator<U>;

34

/** Chain with another locator */

35

locator(selector: string): Locator<ElementHandle>;

36

/** Get element count */

37

count(): Promise<number>;

38

/** Get nth element */

39

nth(index: number): Locator<T>;

40

/** Get first element */

41

first(): Locator<T>;

42

/** Get last element */

43

last(): Locator<T>;

44

/** Clone locator */

45

clone(): Locator<T>;

46

/** Get element handle */

47

elementHandle(): Promise<T>;

48

/** Get all element handles */

49

elementHandles(): Promise<T[]>;

50

/** Evaluate function with element */

51

evaluate<R>(pageFunction: (element: T, ...args: any[]) => R, ...args: any[]): Promise<R>;

52

/** Get text content */

53

textContent(): Promise<string>;

54

/** Get inner text */

55

innerText(): Promise<string>;

56

/** Get inner HTML */

57

innerHTML(): Promise<string>;

58

/** Get attribute value */

59

getAttribute(name: string): Promise<string | null>;

60

/** Get input value */

61

inputValue(): Promise<string>;

62

/** Check if element is checked */

63

isChecked(): Promise<boolean>;

64

/** Check if element is disabled */

65

isDisabled(): Promise<boolean>;

66

/** Check if element is editable */

67

isEditable(): Promise<boolean>;

68

/** Check if element is enabled */

69

isEnabled(): Promise<boolean>;

70

/** Check if element is hidden */

71

isHidden(): Promise<boolean>;

72

/** Check if element is visible */

73

isVisible(): Promise<boolean>;

74

/** Set checked state */

75

setChecked(checked: boolean, options?: LocatorOptions): Promise<void>;

76

/** Tap element (mobile) */

77

tap(options?: LocatorTapOptions): Promise<void>;

78

/** Type text into element */

79

type(text: string, options?: LocatorTypeOptions): Promise<void>;

80

/** Uncheck element */

81

uncheck(options?: LocatorOptions): Promise<void>;

82

/** Check element */

83

check(options?: LocatorOptions): Promise<void>;

84

/** Clear input */

85

clear(options?: LocatorOptions): Promise<void>;

86

/** Drag to another locator */

87

dragTo(target: Locator<ElementHandle>, options?: LocatorDragOptions): Promise<void>;

88

/** Press key */

89

press(key: string, options?: LocatorPressOptions): Promise<void>;

90

}

91

92

interface LocatorClickOptions {

93

/** Mouse button */

94

button?: "left" | "right" | "middle";

95

/** Click count */

96

clickCount?: number;

97

/** Delay between mousedown and mouseup */

98

delay?: number;

99

/** Force click even if not actionable */

100

force?: boolean;

101

/** Modifiers to press */

102

modifiers?: ("Alt" | "Control" | "Meta" | "Shift")[];

103

/** Position relative to element */

104

position?: { x: number; y: number };

105

/** Timeout */

106

timeout?: number;

107

/** Trial run */

108

trial?: boolean;

109

}

110

111

interface LocatorFillOptions {

112

/** Force fill even if not editable */

113

force?: boolean;

114

/** Timeout */

115

timeout?: number;

116

}

117

118

interface LocatorHoverOptions {

119

/** Force hover even if not actionable */

120

force?: boolean;

121

/** Modifiers to press */

122

modifiers?: ("Alt" | "Control" | "Meta" | "Shift")[];

123

/** Position relative to element */

124

position?: { x: number; y: number };

125

/** Timeout */

126

timeout?: number;

127

/** Trial run */

128

trial?: boolean;

129

}

130

131

interface LocatorScrollOptions {

132

/** Position to scroll to */

133

position?: { x: number; y: number };

134

/** Timeout */

135

timeout?: number;

136

}

137

138

interface LocatorScreenshotOptions {

139

/** Animation handling */

140

animations?: "disabled" | "allow";

141

/** Caret handling */

142

caret?: "hide" | "initial";

143

/** Image quality */

144

quality?: number;

145

/** Screenshot type */

146

type?: "png" | "jpeg";

147

/** Timeout */

148

timeout?: number;

149

}

150

151

interface LocatorWaitOptions {

152

/** State to wait for */

153

state?: "attached" | "detached" | "visible" | "hidden";

154

/** Timeout */

155

timeout?: number;

156

}

157

158

interface LocatorOptions {

159

/** Force action even if not actionable */

160

force?: boolean;

161

/** Timeout */

162

timeout?: number;

163

}

164

165

interface LocatorTapOptions {

166

/** Force tap even if not actionable */

167

force?: boolean;

168

/** Modifiers to press */

169

modifiers?: ("Alt" | "Control" | "Meta" | "Shift")[];

170

/** Position relative to element */

171

position?: { x: number; y: number };

172

/** Timeout */

173

timeout?: number;

174

/** Trial run */

175

trial?: boolean;

176

}

177

178

interface LocatorTypeOptions {

179

/** Delay between keystrokes */

180

delay?: number;

181

/** Timeout */

182

timeout?: number;

183

}

184

185

interface LocatorDragOptions {

186

/** Force drag even if not actionable */

187

force?: boolean;

188

/** Source position */

189

sourcePosition?: { x: number; y: number };

190

/** Target position */

191

targetPosition?: { x: number; y: number };

192

/** Timeout */

193

timeout?: number;

194

/** Trial run */

195

trial?: boolean;

196

}

197

198

interface LocatorPressOptions {

199

/** Delay between keydown and keyup */

200

delay?: number;

201

/** Timeout */

202

timeout?: number;

203

}

204

```

205

206

**Usage Examples:**

207

208

```typescript

209

import puppeteer from "puppeteer-core";

210

211

const browser = await puppeteer.launch({ executablePath: "/path/to/chrome" });

212

const page = await browser.newPage();

213

await page.goto("https://example.com");

214

215

// Basic locator usage

216

const button = page.locator("#submit-button");

217

await button.click();

218

219

const input = page.locator("input[name='username']");

220

await input.fill("john.doe");

221

222

const dropdown = page.locator("select#country");

223

await dropdown.select("US");

224

225

// Chaining locators

226

const form = page.locator("form#login-form");

227

const usernameInput = form.locator("input[name='username']");

228

const passwordInput = form.locator("input[name='password']");

229

const submitButton = form.locator("button[type='submit']");

230

231

await usernameInput.fill("user@example.com");

232

await passwordInput.fill("password123");

233

await submitButton.click();

234

235

// Locator filtering and mapping

236

const allButtons = page.locator("button");

237

const enabledButtons = allButtons.filter(async (btn) => {

238

return await btn.isEnabled();

239

});

240

const firstEnabledButton = enabledButtons.first();

241

242

// Wait for elements

243

const dynamicContent = page.locator("#dynamic-content");

244

await dynamicContent.waitFor({ state: "visible" });

245

246

// Element state checks

247

const checkbox = page.locator("#agree-terms");

248

const isChecked = await checkbox.isChecked();

249

if (!isChecked) {

250

await checkbox.check();

251

}

252

253

// Text content operations

254

const heading = page.locator("h1");

255

const headingText = await heading.textContent();

256

console.log("Page heading:", headingText);

257

258

await browser.close();

259

```

260

261

### Selector Strategies

262

263

Various methods for selecting elements with different strategies:

264

265

```typescript { .api }

266

interface SelectorStrategies {

267

/** CSS selector */

268

css(selector: string): Locator<ElementHandle>;

269

/** XPath expression */

270

xpath(expression: string): Locator<ElementHandle>;

271

/** Text content matching */

272

text(text: string | RegExp, options?: TextSelectorOptions): Locator<ElementHandle>;

273

/** Attribute-based selection */

274

attribute(name: string, value?: string | RegExp): Locator<ElementHandle>;

275

/** Role-based selection (accessibility) */

276

role(role: string, options?: RoleSelectorOptions): Locator<ElementHandle>;

277

/** Label-based selection */

278

label(text: string | RegExp): Locator<ElementHandle>;

279

/** Placeholder-based selection */

280

placeholder(text: string | RegExp): Locator<ElementHandle>;

281

/** Test ID selection */

282

testId(id: string): Locator<ElementHandle>;

283

/** Title-based selection */

284

title(title: string | RegExp): Locator<ElementHandle>;

285

/** Alt text selection */

286

alt(text: string | RegExp): Locator<ElementHandle>;

287

}

288

289

interface TextSelectorOptions {

290

/** Exact match */

291

exact?: boolean;

292

/** Case sensitivity */

293

ignoreCase?: boolean;

294

}

295

296

interface RoleSelectorOptions {

297

/** Accessible name */

298

name?: string | RegExp;

299

/** Checked state */

300

checked?: boolean;

301

/** Disabled state */

302

disabled?: boolean;

303

/** Expanded state */

304

expanded?: boolean;

305

/** Include hidden elements */

306

includeHidden?: boolean;

307

/** Level (for headings) */

308

level?: number;

309

/** Pressed state */

310

pressed?: boolean;

311

/** Selected state */

312

selected?: boolean;

313

}

314

```

315

316

**Usage Examples:**

317

318

```typescript

319

// CSS selectors

320

const element = page.locator("div.container > p:nth-child(2)");

321

const complexSelector = page.locator("table tbody tr:has(.status.active) .name");

322

323

// XPath selectors

324

const byXPath = page.locator("//div[@class='content']//span[contains(text(), 'Hello')]");

325

const followingSibling = page.locator("//label[text()='Name']/following-sibling::input");

326

327

// Text-based selection

328

const byText = page.getByText("Click me");

329

const byPartialText = page.getByText(/Submit.*Form/);

330

const exactText = page.getByText("Exact Match", { exact: true });

331

332

// Attribute selection

333

const byAttribute = page.locator("[data-testid='user-card']");

334

const byMultipleAttributes = page.locator("input[type='email'][required]");

335

336

// Role-based selection (accessibility)

337

const submitButton = page.getByRole("button", { name: "Submit" });

338

const mainHeading = page.getByRole("heading", { level: 1 });

339

const checkedCheckbox = page.getByRole("checkbox", { checked: true });

340

341

// Label-based selection

342

const usernameInput = page.getByLabel("Username");

343

const emailField = page.getByLabel(/email/i);

344

345

// Placeholder selection

346

const searchInput = page.getByPlaceholder("Search...");

347

348

// Test ID selection (data-testid)

349

const userProfile = page.getByTestId("user-profile");

350

351

// Title attribute selection

352

const helpIcon = page.getByTitle("Help information");

353

```

354

355

### Custom Query Handlers

356

357

Create and use custom query handlers for specialized element selection:

358

359

```typescript { .api }

360

interface CustomQueryHandler {

361

/** Query function to find single element */

362

queryOne?: (element: Element, selector: string) => Element | null;

363

/** Query function to find multiple elements */

364

queryAll?: (element: Element, selector: string) => Element[];

365

}

366

367

interface QueryHandlerManager {

368

/** Register custom query handler */

369

registerQueryHandler(name: string, handler: CustomQueryHandler): void;

370

/** Unregister query handler */

371

unregisterQueryHandler(name: string): void;

372

/** Get registered query handlers */

373

queryHandlers(): Map<string, CustomQueryHandler>;

374

/** Clear all custom query handlers */

375

clearQueryHandlers(): void;

376

}

377

```

378

379

**Usage Examples:**

380

381

```typescript

382

// Register custom query handler

383

puppeteer.registerQueryHandler('dataQA', {

384

queryOne: (element, selector) =>

385

element.querySelector(`[data-qa="${selector}"]`),

386

queryAll: (element, selector) =>

387

Array.from(element.querySelectorAll(`[data-qa="${selector}"]`))

388

});

389

390

// Use custom query handler

391

const customElement = await page.$("dataQA/login-button");

392

const customElements = await page.$$("dataQA/menu-item");

393

394

// Text-based custom handler

395

puppeteer.registerQueryHandler('text', {

396

queryOne: (element, selector) => {

397

const xpath = `.//*[contains(text(), "${selector}")]`;

398

return document.evaluate(xpath, element, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue as Element;

399

},

400

queryAll: (element, selector) => {

401

const xpath = `.//*[contains(text(), "${selector}")]`;

402

const result = document.evaluate(xpath, element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

403

const elements = [];

404

for (let i = 0; i < result.snapshotLength; i++) {

405

elements.push(result.snapshotItem(i) as Element);

406

}

407

return elements;

408

}

409

});

410

411

// Use text handler

412

const textElement = await page.$("text/Click here");

413

414

// Component-based handler

415

puppeteer.registerQueryHandler('component', {

416

queryOne: (element, selector) => {

417

return element.querySelector(`[data-component="${selector}"]`);

418

}

419

});

420

421

const headerComponent = await page.$("component/app-header");

422

```

423

424

### Built-in Query Handlers

425

426

Puppeteer provides several built-in query handlers for different selection strategies:

427

428

```typescript { .api }

429

interface BuiltInQueryHandlers {

430

/** CSS selector (default) */

431

css: CustomQueryHandler;

432

/** XPath expression */

433

xpath: CustomQueryHandler;

434

/** Text content matching */

435

text: CustomQueryHandler;

436

/** ARIA label matching */

437

aria: CustomQueryHandler;

438

/** Pierce through shadow DOM */

439

pierce: CustomQueryHandler;

440

}

441

```

442

443

**Usage Examples:**

444

445

```typescript

446

// XPath with xpath/ prefix

447

const xpathElement = await page.$("xpath///div[@class='content']");

448

const xpathElements = await page.$$("xpath///button[contains(@class, 'primary')]");

449

450

// Text content with text/ prefix

451

const textElement = await page.$("text/Submit Form");

452

const partialTextElement = await page.$("text/Click");

453

454

// Pierce shadow DOM

455

const shadowElement = await page.$("pierce/#shadow-button");

456

457

// ARIA label

458

const ariaElement = await page.$("aria/Submit");

459

```

460

461

### Locator Patterns and Best Practices

462

463

Common patterns for reliable element location:

464

465

```typescript { .api }

466

interface LocatorPatterns {

467

/** Wait and retry pattern */

468

waitAndRetry<T>(locator: Locator<T>, maxRetries?: number): Promise<T>;

469

/** Safe interaction pattern */

470

safeInteraction(locator: Locator<ElementHandle>, action: string): Promise<boolean>;

471

/** Multiple strategy fallback */

472

multiStrategyLocator(strategies: string[]): Locator<ElementHandle>;

473

/** Conditional locator based on page state */

474

conditionalLocator(condition: () => Promise<boolean>, trueLocator: string, falseLocator: string): Locator<ElementHandle>;

475

}

476

```

477

478

**Usage Examples:**

479

480

```typescript

481

// Reliable element waiting

482

async function waitForElementSafely(page: Page, selector: string, timeout = 30000) {

483

try {

484

return await page.locator(selector).wait({ timeout });

485

} catch (error) {

486

console.log(`Element ${selector} not found within ${timeout}ms`);

487

return null;

488

}

489

}

490

491

// Multiple selector fallback

492

async function findElementWithFallback(page: Page, selectors: string[]) {

493

for (const selector of selectors) {

494

try {

495

const element = await page.locator(selector).wait({ timeout: 5000 });

496

return element;

497

} catch (error) {

498

console.log(`Selector ${selector} failed, trying next...`);

499

}

500

}

501

throw new Error("None of the selectors found an element");

502

}

503

504

// Usage

505

const button = await findElementWithFallback(page, [

506

"#submit-btn",

507

".submit-button",

508

"button[type='submit']",

509

"text/Submit"

510

]);

511

512

// Smart locator with multiple conditions

513

class SmartLocator {

514

constructor(private page: Page) {}

515

516

async findButton(identifier: string) {

517

// Try multiple strategies in order of preference

518

const strategies = [

519

`[data-testid="${identifier}"]`,

520

`#${identifier}`,

521

`.${identifier}`,

522

`button[aria-label="${identifier}"]`,

523

`text/${identifier}`

524

];

525

526

for (const strategy of strategies) {

527

try {

528

const element = await this.page.locator(strategy).wait({ timeout: 2000 });

529

if (await element.isVisible()) {

530

return element;

531

}

532

} catch (error) {

533

// Continue to next strategy

534

}

535

}

536

537

throw new Error(`Could not find button: ${identifier}`);

538

}

539

}

540

541

const smartLocator = new SmartLocator(page);

542

const submitButton = await smartLocator.findButton("submit");

543

```

544

545

### Advanced Locator Features

546

547

Advanced locator capabilities for complex scenarios:

548

549

```typescript { .api }

550

interface AdvancedLocatorFeatures {

551

/** Chain multiple locators */

552

chain(locators: Locator<ElementHandle>[]): Locator<ElementHandle>;

553

/** Create locator union */

554

union(...locators: Locator<ElementHandle>[]): Locator<ElementHandle>;

555

/** Conditional locator execution */

556

conditional(condition: boolean, trueLocator: Locator<ElementHandle>, falseLocator: Locator<ElementHandle>): Locator<ElementHandle>;

557

/** Locator with custom validation */

558

validated(locator: Locator<ElementHandle>, validator: (element: ElementHandle) => Promise<boolean>): Locator<ElementHandle>;

559

/** Cached locator for performance */

560

cached(locator: Locator<ElementHandle>, ttl?: number): Locator<ElementHandle>;

561

}

562

```

563

564

**Usage Examples:**

565

566

```typescript

567

// Locator chaining for complex interactions

568

const form = page.locator("form#checkout");

569

const shippingSection = form.locator(".shipping-section");

570

const addressInput = shippingSection.locator("input[name='address']");

571

572

await addressInput.fill("123 Main St");

573

574

// Dynamic locator based on conditions

575

async function getMenuLocator(page: Page) {

576

const isMobile = await page.evaluate(() => window.innerWidth < 768);

577

return isMobile

578

? page.locator("#mobile-menu")

579

: page.locator("#desktop-menu");

580

}

581

582

const menu = await getMenuLocator(page);

583

await menu.click();

584

585

// Locator with validation

586

async function getValidatedButton(page: Page, selector: string) {

587

const locator = page.locator(selector);

588

589

await locator.wait({ state: "attached" });

590

591

const element = await locator.elementHandle();

592

const isClickable = await element.evaluate((el) => {

593

const style = window.getComputedStyle(el);

594

return style.pointerEvents !== "none" &&

595

style.visibility !== "hidden" &&

596

style.display !== "none";

597

});

598

599

if (!isClickable) {

600

throw new Error(`Element ${selector} is not clickable`);

601

}

602

603

return locator;

604

}

605

606

// Performance optimization with element caching

607

class LocatorCache {

608

private cache = new Map<string, { locator: Locator<ElementHandle>, timestamp: number }>();

609

private ttl = 5000; // 5 seconds

610

611

constructor(private page: Page) {}

612

613

getLocator(selector: string): Locator<ElementHandle> {

614

const cached = this.cache.get(selector);

615

const now = Date.now();

616

617

if (cached && (now - cached.timestamp) < this.ttl) {

618

return cached.locator;

619

}

620

621

const locator = this.page.locator(selector);

622

this.cache.set(selector, { locator, timestamp: now });

623

624

return locator;

625

}

626

627

clear() {

628

this.cache.clear();

629

}

630

}

631

632

const cache = new LocatorCache(page);

633

const button1 = cache.getLocator("#button1"); // Creates new

634

const button2 = cache.getLocator("#button1"); // Returns cached

635

```

636

637

### Error Handling and Debugging

638

639

Common locator errors and debugging strategies:

640

641

```typescript { .api }

642

class LocatorError extends Error {

643

constructor(message: string, selector?: string);

644

selector?: string;

645

}

646

647

class ElementNotFoundError extends LocatorError {

648

constructor(selector: string);

649

}

650

651

class ElementNotActionableError extends LocatorError {

652

constructor(selector: string, action: string);

653

}

654

655

interface LocatorDebugging {

656

/** Debug locator visibility */

657

debugVisibility(locator: Locator<ElementHandle>): Promise<VisibilityInfo>;

658

/** Debug locator state */

659

debugState(locator: Locator<ElementHandle>): Promise<ElementState>;

660

/** Highlight element for debugging */

661

highlight(locator: Locator<ElementHandle>, duration?: number): Promise<void>;

662

}

663

664

interface VisibilityInfo {

665

exists: boolean;

666

visible: boolean;

667

inViewport: boolean;

668

obstructed: boolean;

669

dimensions: { width: number; height: number };

670

}

671

672

interface ElementState {

673

tag: string;

674

attributes: Record<string, string>;

675

computedStyles: Record<string, string>;

676

textContent: string;

677

innerHTML: string;

678

}

679

```

680

681

**Usage Examples:**

682

683

```typescript

684

// Robust locator operations with error handling

685

async function safeClick(page: Page, selector: string) {

686

try {

687

const locator = page.locator(selector);

688

689

// Wait for element to exist

690

await locator.wait({ state: "attached", timeout: 10000 });

691

692

// Check if element is actionable

693

const isVisible = await locator.isVisible();

694

const isEnabled = await locator.isEnabled();

695

696

if (!isVisible) {

697

throw new ElementNotActionableError(selector, "click - element not visible");

698

}

699

700

if (!isEnabled) {

701

throw new ElementNotActionableError(selector, "click - element disabled");

702

}

703

704

await locator.click({ timeout: 5000 });

705

return true;

706

707

} catch (error) {

708

if (error instanceof ElementNotFoundError) {

709

console.log(`Element not found: ${selector}`);

710

} else if (error instanceof ElementNotActionableError) {

711

console.log(`Element not actionable: ${error.message}`);

712

} else {

713

console.log(`Unexpected error clicking ${selector}:`, error.message);

714

}

715

return false;

716

}

717

}

718

719

// Debugging locator issues

720

async function debugLocator(page: Page, selector: string) {

721

const locator = page.locator(selector);

722

723

try {

724

const element = await locator.elementHandle();

725

726

const info = await element.evaluate((el) => ({

727

tagName: el.tagName,

728

id: el.id,

729

className: el.className,

730

textContent: el.textContent?.trim(),

731

offsetWidth: el.offsetWidth,

732

offsetHeight: el.offsetHeight,

733

offsetLeft: el.offsetLeft,

734

offsetTop: el.offsetTop,

735

style: window.getComputedStyle(el).cssText

736

}));

737

738

console.log("Element debug info:", info);

739

740

} catch (error) {

741

console.log(`Could not debug element ${selector}:`, error.message);

742

743

// Try to find similar elements

744

const similarElements = await page.$$eval(

745

"*",

746

(elements, sel) => {

747

return elements

748

.filter(el => el.id?.includes(sel.replace("#", "")) ||

749

el.className?.includes(sel.replace(".", "")))

750

.map(el => ({ tag: el.tagName, id: el.id, class: el.className }))

751

.slice(0, 5);

752

},

753

selector

754

);

755

756

console.log("Similar elements found:", similarElements);

757

}

758

}

759

```