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