Skip to content

Commit f2608d0

Browse files
authored
feat: support locators from element handles (#14147)
1 parent 5bedba6 commit f2608d0

File tree

8 files changed

+194
-26
lines changed

8 files changed

+194
-26
lines changed

docs/api/puppeteer.frame.locator.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,47 @@ func
9595
**Returns:**
9696

9797
[Locator](./puppeteer.locator.md)<Ret>
98+
99+
<h2 id="locator-2">locator(): Locator&lt;T&gt;</h2>
100+
101+
Creates a locator based on an ElementHandle. This would not allow refreshing the element handle if it is stale but it allows re-using other locator pre-conditions.
102+
103+
### Signature
104+
105+
```typescript
106+
class Frame {
107+
locator<T extends Node>(handle: ElementHandle<T>): Locator<T>;
108+
}
109+
```
110+
111+
## Parameters
112+
113+
<table><thead><tr><th>
114+
115+
Parameter
116+
117+
</th><th>
118+
119+
Type
120+
121+
</th><th>
122+
123+
Description
124+
125+
</th></tr></thead>
126+
<tbody><tr><td>
127+
128+
handle
129+
130+
</td><td>
131+
132+
[ElementHandle](./puppeteer.elementhandle.md)&lt;T&gt;
133+
134+
</td><td>
135+
136+
</td></tr>
137+
</tbody></table>
138+
139+
**Returns:**
140+
141+
[Locator](./puppeteer.locator.md)&lt;T&gt;

docs/api/puppeteer.frame.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,17 @@ Creates a locator for the provided function. See [Locator](./puppeteer.locator.m
371371
</td></tr>
372372
<tr><td>
373373

374+
<span id="locator">[locator(handle)](./puppeteer.frame.locator.md)</span>
375+
376+
</td><td>
377+
378+
</td><td>
379+
380+
Creates a locator based on an ElementHandle. This would not allow refreshing the element handle if it is stale but it allows re-using other locator pre-conditions.
381+
382+
</td></tr>
383+
<tr><td>
384+
374385
<span id="name">[name()](./puppeteer.frame.name.md)</span>
375386

376387
</td><td>

docs/api/puppeteer.page.locator.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,47 @@ func
9595
**Returns:**
9696

9797
[Locator](./puppeteer.locator.md)&lt;Ret&gt;
98+
99+
<h2 id="locator-2">locator(): Locator&lt;T&gt;</h2>
100+
101+
Creates a locator based on an ElementHandle. This would not allow refreshing the element handle if it is stale but it allows re-using other locator pre-conditions.
102+
103+
### Signature
104+
105+
```typescript
106+
class Page {
107+
locator<T extends Node>(handle: ElementHandle<T>): Locator<T>;
108+
}
109+
```
110+
111+
## Parameters
112+
113+
<table><thead><tr><th>
114+
115+
Parameter
116+
117+
</th><th>
118+
119+
Type
120+
121+
</th><th>
122+
123+
Description
124+
125+
</th></tr></thead>
126+
<tbody><tr><td>
127+
128+
handle
129+
130+
</td><td>
131+
132+
[ElementHandle](./puppeteer.elementhandle.md)&lt;T&gt;
133+
134+
</td><td>
135+
136+
</td></tr>
137+
</tbody></table>
138+
139+
**Returns:**
140+
141+
[Locator](./puppeteer.locator.md)&lt;T&gt;

docs/api/puppeteer.page.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,17 @@ Creates a locator for the provided function. See [Locator](./puppeteer.locator.m
832832
</td></tr>
833833
<tr><td>
834834
835+
<span id="locator">[locator(handle)](./puppeteer.page.locator.md)</span>
836+
837+
</td><td>
838+
839+
</td><td>
840+
841+
Creates a locator based on an ElementHandle. This would not allow refreshing the element handle if it is stale but it allows re-using other locator pre-conditions.
842+
843+
</td></tr>
844+
<tr><td>
845+
835846
<span id="mainframe">[mainFrame()](./puppeteer.page.mainframe.md)</span>
836847
837848
</td><td>

packages/puppeteer-core/src/api/Frame.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -520,17 +520,26 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
520520
*/
521521
locator<Ret>(func: () => Awaitable<Ret>): Locator<Ret>;
522522

523+
/**
524+
* Creates a locator based on an ElementHandle. This would not allow
525+
* refreshing the element handle if it is stale but it allows re-using other
526+
* locator pre-conditions.
527+
*/
528+
locator<T extends Node>(handle: ElementHandle<T>): Locator<T>;
529+
523530
/**
524531
* @internal
525532
*/
526533
@throwIfDetached
527-
locator<Selector extends string, Ret>(
528-
selectorOrFunc: Selector | (() => Awaitable<Ret>),
529-
): Locator<NodeFor<Selector>> | Locator<Ret> {
530-
if (typeof selectorOrFunc === 'string') {
531-
return NodeLocator.create(this, selectorOrFunc);
534+
locator<Selector extends string, Ret, T extends Node>(
535+
input: Selector | (() => Awaitable<Ret>) | ElementHandle<T>,
536+
): Locator<NodeFor<Selector>> | Locator<Ret> | Locator<T> {
537+
if (typeof input === 'string') {
538+
return NodeLocator.create(this, input);
539+
} else if (typeof input === 'function') {
540+
return FunctionLocator.create(this, input);
532541
} else {
533-
return FunctionLocator.create(this, selectorOrFunc);
542+
return NodeLocator.createFromHandle(this, input);
534543
}
535544
}
536545
/**

packages/puppeteer-core/src/api/Page.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,13 +1105,22 @@ export abstract class Page extends EventEmitter<PageEvents> {
11051105
* {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
11061106
*/
11071107
locator<Ret>(func: () => Awaitable<Ret>): Locator<Ret>;
1108-
locator<Selector extends string, Ret>(
1109-
selectorOrFunc: Selector | (() => Awaitable<Ret>),
1110-
): Locator<NodeFor<Selector>> | Locator<Ret> {
1111-
if (typeof selectorOrFunc === 'string') {
1112-
return NodeLocator.create(this, selectorOrFunc);
1108+
1109+
/**
1110+
* Creates a locator based on an ElementHandle. This would not allow
1111+
* refreshing the element handle if it is stale but it allows re-using other
1112+
* locator pre-conditions.
1113+
*/
1114+
locator<T extends Node>(handle: ElementHandle<T>): Locator<T>;
1115+
locator<Selector extends string, Ret, T extends Node>(
1116+
input: Selector | (() => Awaitable<Ret>) | ElementHandle<T>,
1117+
): Locator<NodeFor<Selector>> | Locator<Ret> | Locator<T> {
1118+
if (typeof input === 'string') {
1119+
return NodeLocator.create(this, input);
1120+
} else if (typeof input === 'function') {
1121+
return FunctionLocator.create(this, input);
11131122
} else {
1114-
return FunctionLocator.create(this, selectorOrFunc);
1123+
return NodeLocator.createFromHandle(this, input);
11151124
}
11161125
}
11171126

packages/puppeteer-core/src/api/locators/locators.ts

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
merge,
2323
mergeMap,
2424
noop,
25+
of,
2526
pipe,
2627
race,
2728
raceWith,
@@ -973,14 +974,29 @@ export class NodeLocator<T extends Node> extends Locator<T> {
973974
);
974975
}
975976

977+
static createFromHandle<T extends Node>(
978+
pageOrFrame: Page | Frame,
979+
handle: ElementHandle<T>,
980+
): Locator<T> {
981+
return new NodeLocator<T>(pageOrFrame, handle).setTimeout(
982+
'getDefaultTimeout' in pageOrFrame
983+
? pageOrFrame.getDefaultTimeout()
984+
: pageOrFrame.page().getDefaultTimeout(),
985+
);
986+
}
987+
976988
#pageOrFrame: Page | Frame;
977-
#selector: string;
989+
#selectorOrHandle: string | ElementHandle<T>;
978990

979-
private constructor(pageOrFrame: Page | Frame, selector: string) {
991+
private constructor(pageOrFrame: Page | Frame, selector: string);
992+
private constructor(pageOrFrame: Page | Frame, handle: ElementHandle<T>);
993+
private constructor(
994+
pageOrFrame: Page | Frame,
995+
selectorOrHandle: string | ElementHandle<T>,
996+
) {
980997
super();
981-
982998
this.#pageOrFrame = pageOrFrame;
983-
this.#selector = selector;
999+
this.#selectorOrHandle = selectorOrHandle;
9841000
}
9851001

9861002
/**
@@ -1009,21 +1025,27 @@ export class NodeLocator<T extends Node> extends Locator<T> {
10091025
};
10101026

10111027
override _clone(): NodeLocator<T> {
1012-
return new NodeLocator<T>(this.#pageOrFrame, this.#selector).copyOptions(
1013-
this,
1014-
);
1028+
return new NodeLocator<T>(
1029+
this.#pageOrFrame,
1030+
// @ts-expect-error TSC does cannot parse private overloads.
1031+
this.#selectorOrHandle,
1032+
).copyOptions(this);
10151033
}
10161034

10171035
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>> {
10181036
const signal = options?.signal;
10191037
return defer(() => {
1020-
return from(
1021-
this.#pageOrFrame.waitForSelector(this.#selector, {
1022-
visible: false,
1023-
timeout: this._timeout,
1024-
signal,
1025-
}) as Promise<HandleFor<T> | null>,
1026-
);
1038+
if (typeof this.#selectorOrHandle === 'string') {
1039+
return from(
1040+
this.#pageOrFrame.waitForSelector(this.#selectorOrHandle, {
1041+
visible: false,
1042+
timeout: this._timeout,
1043+
signal,
1044+
}) as Promise<HandleFor<T> | null>,
1045+
);
1046+
} else {
1047+
return of(this.#selectorOrHandle as HandleFor<T>);
1048+
}
10271049
}).pipe(
10281050
filter((value): value is NonNullable<typeof value> => {
10291051
return value !== null;

test/src/locator.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,24 @@ describe('Locator', function () {
127127
expect(text).toBe('clicked');
128128
});
129129

130+
it('should work with element handles', async () => {
131+
const {page} = await getTestState();
132+
133+
await page.setViewport({width: 500, height: 500});
134+
await page.setContent(`
135+
<button style="margin-top: 600px;" onclick="this.innerText = 'clicked';">test</button>
136+
`);
137+
using button = await page.$('button');
138+
if (!button) {
139+
throw new Error('button not found');
140+
}
141+
await page.locator(button).click();
142+
const text = await button?.evaluate(el => {
143+
return el.innerText;
144+
});
145+
expect(text).toBe('clicked');
146+
});
147+
130148
it('should work if the element becomes visible later', async () => {
131149
const {page} = await getTestState();
132150

0 commit comments

Comments
 (0)